[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ncharset = utf-8\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: File a bug/issue\ntitle: \"bug: \"\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **Before** reporting an issue, make sure to read the [documentation](https://github.com/folke/snacks.nvim)\n        and search [existing issues](https://github.com/folke/snacks.nvim/issues).\n\n        Usage questions such as ***\"How do I...?\"*** belong in [Discussions](https://github.com/folke/snacks.nvim/discussions) and will be closed.\n  - type: checkboxes\n    attributes:\n      label: Did you check docs and existing issues?\n      description: Make sure you checked all of the below before submitting an issue\n      options:\n        - label: I have read all the snacks.nvim docs\n          required: true\n        - label: I have updated the plugin to the latest version before submitting this issue\n          required: true\n        - label: I have searched the existing issues of snacks.nvim\n          required: true\n        - label: I have searched the existing issues of plugins related to this issue\n          required: true\n  - type: input\n    attributes:\n      label: \"Neovim version (nvim -v)\"\n      placeholder: \"0.8.0 commit db1b0ee3b30f\"\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: \"Operating system/version\"\n      placeholder: \"MacOS 11.5\"\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Steps To Reproduce\n      description: Steps to reproduce the behavior.\n      placeholder: |\n        1.\n        2. \n        3.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Expected Behavior\n      description: A concise description of what you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Repro\n      description: Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua`\n      value: |\n        vim.env.LAZY_STDPATH = \".repro\"\n        load(vim.fn.system(\"curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua\"))()\n\n        require(\"lazy.minit\").repro({\n          spec = {\n            { \"folke/snacks.nvim\", opts = {} },\n            -- add any other plugins here\n          },\n        })\n      render: lua\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question\n    url: https://github.com/folke/snacks.nvim/discussions\n    about: Use Github discussions instead\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest a new feature\ntitle: \"feature: \"\nlabels: [enhancement]\nbody:\n  - type: checkboxes\n    attributes:\n      label: Did you check the docs?\n      description: Make sure you read all the docs before submitting a feature request\n      options:\n        - label: I have read all the snacks.nvim docs\n          required: true\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: Is your feature request related to a problem? Please describe.\n      description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: Describe the solution you'd like\n      description: A clear and concise description of what you want to happen.\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: Describe alternatives you've considered\n      description: A clear and concise description of any alternative solutions or features you've considered.\n  - type: textarea\n    validations:\n      required: false\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Description\n\n<!-- Describe the big picture of your changes to communicate to the maintainers\n  why we should accept this pull request. -->\n\n## Related Issue(s)\n\n<!--\n  If this PR fixes any issues, please link to the issue here.\n  - Fixes #<issue_number>\n-->\n\n## Screenshots\n\n<!-- Add screenshots of the changes if applicable. -->\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/labeler.yml",
    "content": "# .github/labeler.yml\n\n# Label for any files under the `doc/` directory\ndocs-vim:\n  - changed-files:\n      - any-glob-to-any-file: \"doc/**\"\n\n# Label for any files under the `docs/` directory and `README.md`\ndocs:\n  - changed-files:\n      - any-glob-to-any-file:\n          - \"docs/**\"\n          - \"README.md\"\n\ncore:\n  - changed-files:\n      - any-glob-to-any-file:\n          - \"lua/snacks/init.lua\"\n          - \"lua/snacks/health.lua\"\n          - \"plugins/**\"\n          - \"queries/**\"\n          - \"scripts/**\"\n\n# Dynamic labels for each module under `lua/snacks/`\nanimate:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/animate/**\"\nbigfile:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/bigfile.lua\"\nbufdelete:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/bufdelete.lua\"\ncompat:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/compat.lua\"\ndashboard:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/dashboard.lua\"\ndebug:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/debug.lua\"\ndim:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/dim.lua\"\nexplorer:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/explorer/**\"\ngit:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/git.lua\"\ngitbrowse:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/gitbrowse.lua\"\nimage:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/image/**\"\nindent:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/indent.lua\"\ninit:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/init.lua\"\ninput:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/input.lua\"\nlayout:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/layout.lua\"\nlazygit:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/lazygit.lua\"\nnotifier:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/notifier.lua\"\nnotify:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/notify.lua\"\npicker:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/picker/**\"\nprofiler:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/profiler/**\"\nquickfile:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/quickfile.lua\"\nrename:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/rename.lua\"\nscope:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/scope.lua\"\nscratch:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/scratch.lua\"\nscroll:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/scroll.lua\"\nstatuscolumn:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/statuscolumn.lua\"\nterminal:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/terminal.lua\"\ntoggle:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/toggle.lua\"\nwin:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/win.lua\"\nwords:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/words.lua\"\nzen:\n  - changed-files:\n      - any-glob-to-any-file: \"lua/snacks/zen.lua\"\n"
  },
  {
    "path": ".github/release-please-config.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json\",\n  \"packages\": {\n    \".\": {\n      \"extra-files\": [\"lua/snacks/init.lua\"],\n      \"release-type\": \"simple\"\n    }\n  }\n}\n"
  },
  {
    "path": ".github/release-please-manifest.json",
    "content": "{\n  \".\": \"2.31.0\"\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main, master]\n  pull_request:\n\njobs:\n  ci:\n    uses: folke/github/.github/workflows/ci.yml@main\n    secrets: inherit\n    with:\n      plugin: snacks.nvim\n      repo: folke/snacks.nvim\n"
  },
  {
    "path": ".github/workflows/labeler.yml",
    "content": "name: \"PR Labeler\"\non:\n  - pull_request_target\n\njobs:\n  labeler:\n    uses: folke/github/.github/workflows/labeler.yml@main\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: PR Title\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n      - reopened\n      - ready_for_review\n\npermissions:\n  pull-requests: read\n\njobs:\n  pr-title:\n    uses: folke/github/.github/workflows/pr.yml@main\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Stale Issues & PRs\n\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  stale:\n    if: contains(fromJSON('[\"folke\", \"LazyVim\"]'), github.repository_owner)\n    uses: folke/github/.github/workflows/stale.yml@main\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/update.yml",
    "content": "name: Update Repo\n\non:\n  workflow_dispatch:\n  schedule:\n    # Run every hour\n    - cron: \"0 * * * *\"\n\njobs:\n  update:\n    if: contains(fromJSON('[\"folke\", \"LazyVim\"]'), github.repository_owner)\n    uses: folke/github/.github/workflows/update.yml@main\n    secrets: inherit\n"
  },
  {
    "path": ".gitignore",
    "content": "*.log\n/.repro\n/.tests\n/build\n/debug\n/doc/tags\nfoo.*\nnode_modules\ntt.*\n"
  },
  {
    "path": ".markdownlint-cli2.yaml",
    "content": "config:\n  MD013: false\n  MD033: false\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [2.31.0](https://github.com/folke/snacks.nvim/compare/v2.30.0...v2.31.0) (2026-03-20)\n\n\n### Features\n\n* **gh:** added `Start Review`. Closes [#2463](https://github.com/folke/snacks.nvim/issues/2463) ([ac5f497](https://github.com/folke/snacks.nvim/commit/ac5f49700527ee5fd14473c5a759460c58d2789d))\n* **gh:** added cycle win and scrolling to scratch buffers when opened in a picker. Closes [#2480](https://github.com/folke/snacks.nvim/issues/2480) ([021855c](https://github.com/folke/snacks.nvim/commit/021855cf29eae9e41107ba28786e5e68b748e8c3))\n* **gh:** make copilot authors as bots ([c6ab189](https://github.com/folke/snacks.nvim/commit/c6ab18964b587edab0e4046719fbc590a59ee042))\n* **gh:** make markview.nvim play nicely with snacks. See [#2467](https://github.com/folke/snacks.nvim/issues/2467) ([deeb1e0](https://github.com/folke/snacks.nvim/commit/deeb1e03e22d83a18c04d1230e628d98a490b6ec))\n* **gh:** open scratch buffers at the bottom of the window. Closes [#2476](https://github.com/folke/snacks.nvim/issues/2476) ([a6a9678](https://github.com/folke/snacks.nvim/commit/a6a967810e185ecc94304529dc09d57e18ba9b68))\n* **gh:** special formatting for code suggestions in review comments. See [#2463](https://github.com/folke/snacks.nvim/issues/2463) ([f1c7f62](https://github.com/folke/snacks.nvim/commit/f1c7f62f9fd6974da3dc8526f454cd364cd73eeb))\n* **gh:** when selecting lines in a diff to add a review comment, you can now suggest code changes. See [#2463](https://github.com/folke/snacks.nvim/issues/2463) ([e896fb9](https://github.com/folke/snacks.nvim/commit/e896fb93f66d6f60176d6c014ddec4352ced4c61))\n* **lua:** add any treesitter injection to a string with a comment like -- inject:graphql ([1d5b12d](https://github.com/folke/snacks.nvim/commit/1d5b12d0c67071320e5572a1f2ac1265904426b3))\n* **picker.actions:** allow specifying an additional window for `cycle_win` ([197f393](https://github.com/folke/snacks.nvim/commit/197f393bbb30684d33165106482aad0a663964c8))\n* **picker.lspconfig:** show available dynamic registered code actions ([521ef46](https://github.com/folke/snacks.nvim/commit/521ef46ae9d38f1b31ca8a05b39647fda13a56be))\n* **picker.lspconfig:** show available server commands and code actions ([7a90a08](https://github.com/folke/snacks.nvim/commit/7a90a089b781a3fc3c5cd179cdc095a0d244d5fa))\n* **win:** `opts.footer_keys` can now be an array of lhs to show instead of all. See [#2469](https://github.com/folke/snacks.nvim/issues/2469) ([6d72138](https://github.com/folke/snacks.nvim/commit/6d721388cc5760db215abd875606a273c1c685f9))\n* **win:** better zindex calculation ([08c0951](https://github.com/folke/snacks.nvim/commit/08c09515234b1ecc285d63aa56915caafa1d72d3))\n* **win:** new border `top_bottom` ([6134c98](https://github.com/folke/snacks.nvim/commit/6134c98d48657b457a7d4b1e2cd2c7ce37d98ea4))\n\n\n### Bug Fixes\n\n* **gh.item:** timestamps should be in UTC, not local time ([1ba0bf8](https://github.com/folke/snacks.nvim/commit/1ba0bf8a10b117d08c2a97347bd666f995600d8a))\n* **gh.scratch:** if scratch would hide the cursor, then scroll preview up taking scrolloff into account. See [#2480](https://github.com/folke/snacks.nvim/issues/2480) ([a271610](https://github.com/folke/snacks.nvim/commit/a2716102c8bd7d25693201af0942552f10e9a0c3))\n* **gh:** better integration with render-markdown. Closes [#2467](https://github.com/folke/snacks.nvim/issues/2467) ([717073d](https://github.com/folke/snacks.nvim/commit/717073df1a515a1564e855cc6ae8986025611e4b))\n* **gh:** diff comment action should only show when available ([fe20e95](https://github.com/folke/snacks.nvim/commit/fe20e9578033a1b726983d6f410a5cc8098fb3c2))\n* **gh:** render gh comments as markdown when displaying in a non-markdown buffer. Closes [#2481](https://github.com/folke/snacks.nvim/issues/2481) ([06e9ca9](https://github.com/folke/snacks.nvim/commit/06e9ca95f81f528c4314afb80a59ce317f12ac5d))\n* **gh:** rendering of markdown comments. Closes [#2488](https://github.com/folke/snacks.nvim/issues/2488) ([dec29f5](https://github.com/folke/snacks.nvim/commit/dec29f55666f8f4545835636077a86b150faf630))\n* **gh:** set default scratch `height=15` and fix bottom offset for custom height ([6900f3f](https://github.com/folke/snacks.nvim/commit/6900f3feaa397e8bd671be39411a370188f856c6))\n* **grep:** remove `MATCH_SEP` before sending to notify ([#2744](https://github.com/folke/snacks.nvim/issues/2744)) ([9912042](https://github.com/folke/snacks.nvim/commit/9912042fc8bca2209105526ac7534e9a0c2071b2))\n* **grep:** remove nul bytes from warning messages in grep output. Fixes [#2744](https://github.com/folke/snacks.nvim/issues/2744), Closes [#2768](https://github.com/folke/snacks.nvim/issues/2768) ([b2cb00e](https://github.com/folke/snacks.nvim/commit/b2cb00ef7d12da7f2d6e0684c43e2965896309dd))\n* **grep:** use %z to replace nul bytes ([a049339](https://github.com/folke/snacks.nvim/commit/a049339328e2599ad6e85a69fa034ac501e921b2))\n* **input:** fixed completion. Closes [#2472](https://github.com/folke/snacks.nvim/issues/2472) ([3024376](https://github.com/folke/snacks.nvim/commit/30243765808a6ea92da9886b50b4e2e01ff262e3))\n* **lspconfig:** handle complex values in lsp config preview. Closes [#2711](https://github.com/folke/snacks.nvim/issues/2711) ([25f53f7](https://github.com/folke/snacks.nvim/commit/25f53f762cc391591285adf0974a8d6e4b4b973e))\n* **markdown:** use new markview API ([9b86d57](https://github.com/folke/snacks.nvim/commit/9b86d57cc580e976ee3c89fdf20477873bd5f0c2))\n* **picker.actions:** `vim.v.count1` should be `1` in insert mode. Closes [#2492](https://github.com/folke/snacks.nvim/issues/2492) ([d902c0a](https://github.com/folke/snacks.nvim/commit/d902c0a415ffbf66321f40ecb07e73fea283d0ab))\n* **picker.confirm:** better layout for confirm ([7f62aa6](https://github.com/folke/snacks.nvim/commit/7f62aa6c6c78a1fcbe207dbb59f6b3105f756e79))\n* **picker.diff:** make diff filename extmarks play nicely with markview / markdown-renderer ([4f749ab](https://github.com/folke/snacks.nvim/commit/4f749ab355cd62bbffbb2f6cc4ddcf0fc274fece))\n* **picker.git:** fix cwd for git diff. Closes [#2483](https://github.com/folke/snacks.nvim/issues/2483) ([9076793](https://github.com/folke/snacks.nvim/commit/907679381ba5ed36a24b0176930e3ceb97ca4755))\n* **picker.lsp:** wait for pending requests. See [#2527](https://github.com/folke/snacks.nvim/issues/2527) ([fe7cfe9](https://github.com/folke/snacks.nvim/commit/fe7cfe9800a182274d0f868a74b7263b8c0c020b))\n* **picker.preview:** remove `--no-ext-diff` option for git diff preview ([836e073](https://github.com/folke/snacks.nvim/commit/836e07336ba523d4da480cd66f0241815393e98e))\n* **picker.spinner:** when parent win is not float win ([#2487](https://github.com/folke/snacks.nvim/issues/2487)) ([8ca098c](https://github.com/folke/snacks.nvim/commit/8ca098ca360b45a361c475be6ea7dd81e438cc35))\n* **picker:** fix nowait for `<c-r>` ([685c433](https://github.com/folke/snacks.nvim/commit/685c433e61812eb21a890f14dac38a8c573931df))\n* **scratch:** set filetype correctly. Closes [#2510](https://github.com/folke/snacks.nvim/issues/2510) ([3c5c23b](https://github.com/folke/snacks.nvim/commit/3c5c23ba91e608bd89bb36d76cb005aa63d20dbf))\n* **util.diff:** proper linebreak repeat for annotation boxes ([64179b9](https://github.com/folke/snacks.nvim/commit/64179b96f547bc10211de3360a1e17a237cdb434))\n* **win:** allow scrolling beyond eob ([8b5f762](https://github.com/folke/snacks.nvim/commit/8b5f76292becf9ad76ef1507cbdcec64a49ff3f4))\n* **win:** use normkey instead of keytrans for footer keys ([9bd41bb](https://github.com/folke/snacks.nvim/commit/9bd41bb2ff5acd68d81e7323a80d18aa9efb7ca9))\n* **win:** when a floating win becomes non-floating, remove its backdrop ([c1e1500](https://github.com/folke/snacks.nvim/commit/c1e15001c0da18f740bc8bbe55fc0509f41bd9c6))\n\n\n### Performance Improvements\n\n* **bigfile:** disable completion to avoid lag when entering insert mode ([#2475](https://github.com/folke/snacks.nvim/issues/2475)) ([c49c3f3](https://github.com/folke/snacks.nvim/commit/c49c3f364cde3bb1bbf79e501d1492ea8e2397bc))\n\n## [2.30.0](https://github.com/folke/snacks.nvim/compare/v2.29.0...v2.30.0) (2025-11-06)\n\n\n### Features\n\n* **diff:** prettier commit rendering (git show, diff with header) ([dc2186e](https://github.com/folke/snacks.nvim/commit/dc2186e57221cd834487e5c3fbd548180e836d1c))\n* **gh:** add inline review comment annotations to diff viewer ([c83ff8d](https://github.com/folke/snacks.nvim/commit/c83ff8d5982e6ebf92623911e232f1dbd0b0a00c))\n* **gh:** create review comments in GitHub PR diff, on diff lines. Closes [#2446](https://github.com/folke/snacks.nvim/issues/2446) ([85bf3f0](https://github.com/folke/snacks.nvim/commit/85bf3f0123cd6aaa1e06569283ffca10b59fcc3b))\n* **layout:** allow resizing split layouts. See [#2390](https://github.com/folke/snacks.nvim/issues/2390) ([913379c](https://github.com/folke/snacks.nvim/commit/913379ccd2679fc11462479205897e584496c855))\n* **picker.gh_diff:** you can now reply to review comments in diffs with `a`. See [#2446](https://github.com/folke/snacks.nvim/issues/2446) ([c3bda87](https://github.com/folke/snacks.nvim/commit/c3bda8709ab9f300e9dd15c2e0ec47f17caff1d5))\n* **picker.icons:** make it easier to add custom icon sources ([82e6966](https://github.com/folke/snacks.nvim/commit/82e69661cd0766893184dcc3ec3684b492772854))\n* **picker.marks:** added `<c-x>` to delete a mark from the list. See [#2390](https://github.com/folke/snacks.nvim/issues/2390) ([9a04605](https://github.com/folke/snacks.nvim/commit/9a04605664be0613963910d545d4164eb2f57bab))\n* **picker:** when picker was started from insert mode, return to insert after paste ([a417630](https://github.com/folke/snacks.nvim/commit/a4176301e323d9689674764da62f628742dc744a))\n* **util.async:** add proper backtrace to unhandled async errors ([01f6cac](https://github.com/folke/snacks.nvim/commit/01f6cac48fd7a3ec9bf7e5fc8a5ae22381861baf))\n\n\n### Bug Fixes\n\n* **gh:** force `fancy` diff style for gh pr diff, since that's needed to render/interact with review comments ([bd71cd4](https://github.com/folke/snacks.nvim/commit/bd71cd4b007fd414f2bd3e4ce7d292fde4b8849f))\n* **gh:** only skip empty comment reviews. Closes [#2445](https://github.com/folke/snacks.nvim/issues/2445) ([1848d74](https://github.com/folke/snacks.nvim/commit/1848d74e7c01a0c4686b6e8353158507a73dfe10))\n* **gh:** pass correct context in gh_perform_action. Closes [#2442](https://github.com/folke/snacks.nvim/issues/2442). Closes [#2443](https://github.com/folke/snacks.nvim/issues/2443) ([13edbc6](https://github.com/folke/snacks.nvim/commit/13edbc681c727adfff72c49d07fedaec566404f8))\n* **gh:** properly handly pending requests ([7a15e16](https://github.com/folke/snacks.nvim/commit/7a15e16d0165fa3b22486066795eeca788ec0c8d))\n* **gh:** use lua to parse dates so we can do this in a fast context ([cd0d6fe](https://github.com/folke/snacks.nvim/commit/cd0d6fe86465394af8ba5037bc87d7ebfecc10fb))\n* **image:** run terminal capability detection synchronous when needed. Closes [#2439](https://github.com/folke/snacks.nvim/issues/2439) ([58f1152](https://github.com/folke/snacks.nvim/commit/58f11527fe8b63ccabb1d75bb8f14f5ebbdc6a14))\n* **layout:** ignore very zindex windows for calulating layout zindex, so that it stays below things like notifications ([47340e6](https://github.com/folke/snacks.nvim/commit/47340e6b0b24773ef4789aa677394a2499762841))\n* **lsp:** properly detach buffers on LspDetach. Closes [#2457](https://github.com/folke/snacks.nvim/issues/2457) ([beb995e](https://github.com/folke/snacks.nvim/commit/beb995e1c6a554d53f6d23fc3aadea568dab2534))\n* **picker.actions:** don't open a new tab if the current tab is empty. Closes [#2461](https://github.com/folke/snacks.nvim/issues/2461) ([4e2424e](https://github.com/folke/snacks.nvim/commit/4e2424eca7ffaa6e36ebf4ac4d136d05b79edc41))\n* **picker.diff:** added `showbreak=\"\"` for fancy diff. Closes [#2441](https://github.com/folke/snacks.nvim/issues/2441) ([fb55f7b](https://github.com/folke/snacks.nvim/commit/fb55f7bf2b6521eb6d7adf6a6b4b437a80882cd0))\n* **picker.format:** use file for icon. fallback to ft when buffer is not a file. See [#2390](https://github.com/folke/snacks.nvim/issues/2390) ([20ac8bf](https://github.com/folke/snacks.nvim/commit/20ac8bfc4ac615a46f64b7806ff367b114bc942f))\n* **picker.grep_word:** pass `--word-regexp` to `ripgrep` ([6aad368](https://github.com/folke/snacks.nvim/commit/6aad36810a8b49041b8a7d3ef6b9050549be0617))\n* **picker.highlight:** resolve ([4438ee4](https://github.com/folke/snacks.nvim/commit/4438ee4770edad9fb843d841b9fdf5ef04d9f479))\n* **picker.input:** startinsert when starting the picker from terminal mode. See [#2390](https://github.com/folke/snacks.nvim/issues/2390) ([b2054a3](https://github.com/folke/snacks.nvim/commit/b2054a3a734631f33b3b08af88b171930666bffe))\n* **picker.lsp_config:** nil on lsp info ([#2459](https://github.com/folke/snacks.nvim/issues/2459)) ([c5257fa](https://github.com/folke/snacks.nvim/commit/c5257fa690fe7e7de7d0da29a6f46f2236b355f0))\n* **scope:** textobjects should use synchronous treesitter parsing. Closes [#2448](https://github.com/folke/snacks.nvim/issues/2448) ([9737c25](https://github.com/folke/snacks.nvim/commit/9737c25f2937ab4cd4751b7d041268949d9e0d97))\n* **toggle:** set/get raw values for option toggles. See [#2390](https://github.com/folke/snacks.nvim/issues/2390) ([41da728](https://github.com/folke/snacks.nvim/commit/41da728f0280033c13aaa4e2820bd3e926790a28))\n* **win:** set `foldcolumn='0'` for minimal style. See [#2390](https://github.com/folke/snacks.nvim/issues/2390) ([195faa0](https://github.com/folke/snacks.nvim/commit/195faa0646df9e9c765c3b86c58407021decdb7c))\n\n## [2.29.0](https://github.com/folke/snacks.nvim/compare/v2.28.0...v2.29.0) (2025-11-04)\n\n\n### Features\n\n* **gh.diff:** show git status in PR diff ([c671d06](https://github.com/folke/snacks.nvim/commit/c671d062d163a31894453dbca15087ea9149ac38))\n* **gh:** added reviews and nice diffs to gh buffer views. See [#2411](https://github.com/folke/snacks.nvim/issues/2411) ([1335ca1](https://github.com/folke/snacks.nvim/commit/1335ca1956fa81ddb3e249721e8f422c4cd0c329))\n* **gh:** allow to update pr branch ([#2419](https://github.com/folke/snacks.nvim/issues/2419)) ([f75f307](https://github.com/folke/snacks.nvim/commit/f75f307af3230c9872939aabd2fb484d8ad3cb5f))\n* **gh:** use new diff renderer for gh pr reviews ([714edec](https://github.com/folke/snacks.nvim/commit/714edec900334130a274ef1a21dd2b6edb7997fe))\n* **gh:** when on a review comment, the `gh_comment` action will now do a reply instead of a top-level comment. Fixes [#2410](https://github.com/folke/snacks.nvim/issues/2410) ([a4f2b9d](https://github.com/folke/snacks.nvim/commit/a4f2b9da2d0dd73e127459fb372e0eb695d70cb2))\n* **gh:** you can now use `Snacks.picker.gh_actions()` directly to see actions for the checked out PR ([d0d10f6](https://github.com/folke/snacks.nvim/commit/d0d10f6d13a4285a83ccf225d2a8938152efcef8))\n* **picker.diff:** new fancy diff renderer ([22eea90](https://github.com/folke/snacks.nvim/commit/22eea90a9548e692c80a20740934720e6d095be1))\n* **picker.git_diff:** show proper git status for git diff files ([ab48eeb](https://github.com/folke/snacks.nvim/commit/ab48eebeb37cc149907d13c904008712a858212b))\n* **picker.git_diff:** show renames ([77609a0](https://github.com/folke/snacks.nvim/commit/77609a00133cf56b69a7fee9b677a1f0c877e37b))\n* **picker.lsp_config:** added server/dynamic capabilities to preview ([da14fac](https://github.com/folke/snacks.nvim/commit/da14fac1e54dc0022b9ba724a50ae93e43f5f271))\n* **picker:** consolidate all diff options under `opts.previewers.diff`. Default style is `fancy` ([b65b06c](https://github.com/folke/snacks.nvim/commit/b65b06ca0ec7ea4730a7a06e71edbc9c1aa32980))\n* **zen:** added `center` option that defaults to `true` for zen mode and `false` for zoom mode. Closes [#2422](https://github.com/folke/snacks.nvim/issues/2422) ([3c2d791](https://github.com/folke/snacks.nvim/commit/3c2d79162f8174d5e1c33539a72025a25f4af590))\n\n\n### Bug Fixes\n\n* **dashboard:** start job after the terminal window is shown to make sure it has the correct size. Closes [#2421](https://github.com/folke/snacks.nvim/issues/2421) ([e440df3](https://github.com/folke/snacks.nvim/commit/e440df387d448a2ec332442a0eca6ece685f2b4d))\n* **diff:** fallback if `Normal` has no fg color. Closes [#2436](https://github.com/folke/snacks.nvim/issues/2436) ([7f453c4](https://github.com/folke/snacks.nvim/commit/7f453c4f322ea2664655cc78c70ff4a9b6238c75))\n* **diff:** improved diff parsing. Closes [#2424](https://github.com/folke/snacks.nvim/issues/2424). Closes [#2420](https://github.com/folke/snacks.nvim/issues/2420) ([b6e4eb7](https://github.com/folke/snacks.nvim/commit/b6e4eb7e608924a0d116bb01dabb575365bcfd75))\n* **diff:** remove diff injections. Closes [#2406](https://github.com/folke/snacks.nvim/issues/2406) ([ecc21bb](https://github.com/folke/snacks.nvim/commit/ecc21bbb9b6969b039676ad7f5d34df5974b1580))\n* **gh.api:** get repo from upstream remote if availble. fallback to origin ([5043637](https://github.com/folke/snacks.nvim/commit/50436373c277906cf40e47380f3dc1bd7769a885))\n* **gh.api:** pass repo to cmd. Closes [#2415](https://github.com/folke/snacks.nvim/issues/2415) ([78046eb](https://github.com/folke/snacks.nvim/commit/78046eb4817e30e216d2456c84005d5264aad67f))\n* **gh.diff:** fixed rendering of diff header when wrap=true ([07c569d](https://github.com/folke/snacks.nvim/commit/07c569dfd5f869dbe23d32d7ce1a7547a6abe69a))\n* **gh.item:** better method to extract repo from gh url. Closes [#2418](https://github.com/folke/snacks.nvim/issues/2418) ([52d544c](https://github.com/folke/snacks.nvim/commit/52d544cc64da4e9cae4f3df5adf7a777b2ed0217))\n* **gh.render:** added support for older `StatusContext` checks. Closes [#2407](https://github.com/folke/snacks.nvim/issues/2407) ([74864a7](https://github.com/folke/snacks.nvim/commit/74864a7bb8390684a5132e4025a77a1de92865e8))\n* **gh.render:** use check name. See [#2407](https://github.com/folke/snacks.nvim/issues/2407) ([6f60105](https://github.com/folke/snacks.nvim/commit/6f60105302fcae45524a5b6232beb52829e93e3f))\n* **gh:** better way of determining current PR ([bd3c1a0](https://github.com/folke/snacks.nvim/commit/bd3c1a071483c943ce98723ef7ac79fda0c7ee16))\n* **gh:** input for api should be a table, not a string. Closes [#2427](https://github.com/folke/snacks.nvim/issues/2427) ([1b3e409](https://github.com/folke/snacks.nvim/commit/1b3e4090a01331ef632ce904d364daf2b811bf54))\n* **image:** allow to fully disable all image rendering with `opts.image.enabled = false`. Closes [#2404](https://github.com/folke/snacks.nvim/issues/2404) ([34a6591](https://github.com/folke/snacks.nvim/commit/34a6591a616836d727140afcd9896981c64281e0))\n* **image:** disable image conversion error notifications by default ([cfcf525](https://github.com/folke/snacks.nvim/commit/cfcf52520765fb18113d89b970bd26a6aa6f543b))\n* **lsp:** check lsp handlers after LspAttach, since attached_buffers won't have been set ([1861b0a](https://github.com/folke/snacks.nvim/commit/1861b0a8eaee849fb8ed67a6764ef5021196bb58))\n* **picker.actions:** only allow stage/unstage/restore for some diffs ([9cde35b](https://github.com/folke/snacks.nvim/commit/9cde35b7b16244fee5c6f73749523e95e4a2b432))\n* **picker.diff:** move git status calc based on diff to format ([b553c18](https://github.com/folke/snacks.nvim/commit/b553c18c263b156f12bfc2a80124cf8edfa04dd3))\n* **picker.diff:** parse full diff including diff and hunk headers. Closes [#2429](https://github.com/folke/snacks.nvim/issues/2429) ([53d8012](https://github.com/folke/snacks.nvim/commit/53d8012e5e4b2115ade2c15d040223ef97ffb05c))\n* **picker.git_diff:** don't show git status, disable stage/unstage/restore when merge-base is set. Closes [#2397](https://github.com/folke/snacks.nvim/issues/2397) ([6ff82ca](https://github.com/folke/snacks.nvim/commit/6ff82cab7bd413bfe30cf1b1856729a59392c405))\n* **picker.highlight:** resolve all text chunks when needed. Not just the first. ([962aadd](https://github.com/folke/snacks.nvim/commit/962aadd3103496c7d2a02cc358a13f773f03a059))\n* **picker.undo:** cleanup tmp files in `async:on(\"done\")`. Closes [#2434](https://github.com/folke/snacks.nvim/issues/2434) ([3038dac](https://github.com/folke/snacks.nvim/commit/3038dac46009a778881a7ca98287a6c1cba1d160))\n* **picker.undo:** set initial target to the current undo entry. See [#2434](https://github.com/folke/snacks.nvim/issues/2434) ([dc245ef](https://github.com/folke/snacks.nvim/commit/dc245ef04e19b34510fa1edbe52abd61d28fea37))\n* **picker:** don't focus a picker window when toggling a window and picker wasn't current. closes [#2417](https://github.com/folke/snacks.nvim/issues/2417) ([b80b330](https://github.com/folke/snacks.nvim/commit/b80b330091b488e8f41f9e5228d9c9acbe333f7b))\n* revert rename of actions.lua to tomdar87@outlook.com ([#2423](https://github.com/folke/snacks.nvim/issues/2423)) ([8bb3ad6](https://github.com/folke/snacks.nvim/commit/8bb3ad6c53232b4c7f0cf430ef3df1bef69fd6ee))\n* **win:** fixed fixbuf. Closes [#2409](https://github.com/folke/snacks.nvim/issues/2409) ([2099572](https://github.com/folke/snacks.nvim/commit/2099572fe8b7296ecda13e20b553e9cd873cf165))\n\n## [2.28.0](https://github.com/folke/snacks.nvim/compare/v2.27.0...v2.28.0) (2025-11-01)\n\n\n### Features\n\n* **gh:** new `gh` (GitHub cli) integration ([85b8ec2](https://github.com/folke/snacks.nvim/commit/85b8ec210975aa137af4b7bef1fb7b7098be331a))\n* **image:** when opts.conceal, conceal remainig lines that are not covered by the image. See [#2391](https://github.com/folke/snacks.nvim/issues/2391) ([404027c](https://github.com/folke/snacks.nvim/commit/404027c973c0a54a369931d36263996a34162ab5))\n* **picker.buffers:** add filetype/buftype to search text ([a249c86](https://github.com/folke/snacks.nvim/commit/a249c86cf1ed3b8434bc004af3a865997706c22f))\n* **picker.buffers:** added buftype and filetype for scratch buffers ([6a13271](https://github.com/folke/snacks.nvim/commit/6a132716af145a109800a129300e43104789b5c0))\n* **picker.diff:** moved git_diff finder to separate file so it can be re-used + made it more robust. Closes [#2366](https://github.com/folke/snacks.nvim/issues/2366) ([3049ad8](https://github.com/folke/snacks.nvim/commit/3049ad8beba417924b2d4ff8f77edec53d3472f6))\n* **picker.diff:** native diff now also highlights the language of the diffed code in the diff ([7260957](https://github.com/folke/snacks.nvim/commit/726095723d0ceae2ffeedf722620238491aeaa30))\n* **picker.git_diff:** `git_diff` now also shows staged hunks and added stage/unstage/restore actions for hunks. Closes [#2382](https://github.com/folke/snacks.nvim/issues/2382) ([1fb3f4d](https://github.com/folke/snacks.nvim/commit/1fb3f4de49962a80cb88a9b143bc165042c72165))\n* **picker.git_diff:** added `staged` flag ([118648c](https://github.com/folke/snacks.nvim/commit/118648ce93b9fc3a4493783fe3efce60fcdb59a3))\n* **picker.highlights:** badges ([202e595](https://github.com/folke/snacks.nvim/commit/202e595e553b8c5865c080cc375381e6b096804c))\n* **picker.preview:** allow items to define a title used in the preview window ([4b572f4](https://github.com/folke/snacks.nvim/commit/4b572f4785df68b60234996f03b91d4581a8ad47))\n* **picker.preview:** support for images and render markdown ([9585da6](https://github.com/folke/snacks.nvim/commit/9585da6c57ed4e06c52a84d680d6b700cab42d6c))\n* **picker.util:** cmdline parser used to properly parse diff args ([5025989](https://github.com/folke/snacks.nvim/commit/502598953fa70cd4507ba39f3e9b4babd7e4df9d))\n* **picker:** better integration with markview and render-markdown when previewing ([4708e9a](https://github.com/folke/snacks.nvim/commit/4708e9a38657e71b2743a35a4530d0118c21d4fe))\n* **scratch:** store scratch info in meta files, instead of the filename + custom filekeys ([85f8e22](https://github.com/folke/snacks.nvim/commit/85f8e22281bee237d5e29746019ec21b1624925c))\n* **util.spawn:** `Proc:json()` ([5589c9d](https://github.com/folke/snacks.nvim/commit/5589c9d355606a026001fe589bc0329077951f45))\n* **util:** `Snacks.util.stop()` to safely stop/close a luv handle ([ce9e299](https://github.com/folke/snacks.nvim/commit/ce9e2993dd4d8289cfdbc129efed74a3394841b9))\n\n\n### Bug Fixes\n\n* **explorer.tree:** only strip trasiling forward slashes if not at root. Closes [#2375](https://github.com/folke/snacks.nvim/issues/2375) ([72dc621](https://github.com/folke/snacks.nvim/commit/72dc6213f758da8484094cb975bfdfc2f8f61621))\n* **explorer:** differentiate if file or folder when deleting on Windows ([#2373](https://github.com/folke/snacks.nvim/issues/2373)) ([59c5545](https://github.com/folke/snacks.nvim/commit/59c5545e91878c1f6218b032a881832bc98a46f3))\n* **explorer:** do reveal in on_show if explorer is not open yet. Closes [#2388](https://github.com/folke/snacks.nvim/issues/2388) ([ba529d4](https://github.com/folke/snacks.nvim/commit/ba529d4f5d409639e082aff916c9b8e71b480531))\n* **explorer:** schedule `on_find` (typically reveal), for both files finder and when git status updates the finder. Closes [#2388](https://github.com/folke/snacks.nvim/issues/2388) ([a9b57b2](https://github.com/folke/snacks.nvim/commit/a9b57b2a7ee0642f5c5c2c3f39d7e57fadafa1af))\n* **gh:** add action idx to `gh_actions` text ([d94184d](https://github.com/folke/snacks.nvim/commit/d94184d1d91a9b8794931538da8f9c76871b3265))\n* **image.inline:** off-by-one for finding visible images at the last line of the buffer ([04b3a54](https://github.com/folke/snacks.nvim/commit/04b3a54576757215a02d306e47b7fb9a542c37b2))\n* **image:** avoid nested math environments ([#2345](https://github.com/folke/snacks.nvim/issues/2345)) ([66e3dc4](https://github.com/folke/snacks.nvim/commit/66e3dc46190992048b571e1225b5a5c2712d2ec6))\n* **image:** check for invalid buffer ([9ad4178](https://github.com/folke/snacks.nvim/commit/9ad41782eced6a06034e568357cdad35cbf52ffa))\n* **image:** check to update on BufWinEnter and attach to buffer changes ([e18e4f6](https://github.com/folke/snacks.nvim/commit/e18e4f6452c62035289d28156dbd0966af13a046))\n* **image:** don't add placements to concealed lines. Closes [#2391](https://github.com/folke/snacks.nvim/issues/2391) ([13963b1](https://github.com/folke/snacks.nvim/commit/13963b1ec41c21aa81698ed042bb48124325f61d))\n* **image:** guard against invalid buffers. Closes [#2383](https://github.com/folke/snacks.nvim/issues/2383) ([4bb1ce1](https://github.com/folke/snacks.nvim/commit/4bb1ce16ed2882978e9524ad057cfa892a226887))\n* keymap docs ([583a0c1](https://github.com/folke/snacks.nvim/commit/583a0c1c06865b4cf64e1104c5250516f5cc6d31))\n* **keymap:** make sure opts are a table. Closes [#2392](https://github.com/folke/snacks.nvim/issues/2392) ([367d1bd](https://github.com/folke/snacks.nvim/commit/367d1bd385fd43fe40defd0a901c8597e5aee1ec))\n* **layout:** only max zindex for snacks windows/layouts ([8eddc0b](https://github.com/folke/snacks.nvim/commit/8eddc0b3809b5af68bb0fc14dcf6c7a1854133cf))\n* **picker.actions:** `drop` and `tabdrop` should never reload existing buffers ([#2368](https://github.com/folke/snacks.nvim/issues/2368)) ([6cf2fee](https://github.com/folke/snacks.nvim/commit/6cf2fee619e81e519ad900542b38ed3491dc45de))\n* **picker.actions:** use `buffer!` instead of `buffer` for edit. Closes [#2378](https://github.com/folke/snacks.nvim/issues/2378) ([2a1a001](https://github.com/folke/snacks.nvim/commit/2a1a001d3a3ed66e39da353efbe37b4ab0a7db93))\n* **picker.diff:** better filename parsing. See [#2366](https://github.com/folke/snacks.nvim/issues/2366) ([377f3bf](https://github.com/folke/snacks.nvim/commit/377f3bfeca716ada246720a7974c49aee56fd382))\n* **picker.diff:** first line of header ([fb011c2](https://github.com/folke/snacks.nvim/commit/fb011c257f29cb7a5f4098f7a7e79ac76870761d))\n* **picker.diff:** only process `---` diffs directly if it doesn't start with a diff header ([0a33aec](https://github.com/folke/snacks.nvim/commit/0a33aec0c62425031efc7867be5c466b83aa82cf))\n* **picker.filter:** get cwd from active tabpage if available ([c1b517f](https://github.com/folke/snacks.nvim/commit/c1b517f545fffcf401217bd41202833ee6465f31))\n* **picker.finder:** mutate existing opts ([c91e230](https://github.com/folke/snacks.nvim/commit/c91e23060c73432cb25f99d6ed632c22fce87d88))\n* **picker.finder:** tmp fix for [#2386](https://github.com/folke/snacks.nvim/issues/2386) ([5eea5f9](https://github.com/folke/snacks.nvim/commit/5eea5f94280ef9034c7da8bbb5ec12dc71b6916f))\n* **picker.git_branches:** git log preview. Closes [#2360](https://github.com/folke/snacks.nvim/issues/2360) ([597ebd4](https://github.com/folke/snacks.nvim/commit/597ebd411529a15b23cba1bc45b20db8f6adbbae))\n* **picker.git_diff:** remove `--default-prefix`, since that's no longer needed. See [#2382](https://github.com/folke/snacks.nvim/issues/2382) ([40774ae](https://github.com/folke/snacks.nvim/commit/40774ae6cabd4b2b76705295338c2a6a71976b98))\n* **picker.git_diff:** set `group=false` by default, since we also have `git_status` ([530e591](https://github.com/folke/snacks.nvim/commit/530e5913453d2501f79caf4d909c1932334bc9f6))\n* **picker.highlights:** modifiable for set_lines ([98345fb](https://github.com/folke/snacks.nvim/commit/98345fb66753283ee4a091bb444df537e4012233))\n* **picker.keymaps:** try to locate neovim compiled lua source files for keymaps ([76160be](https://github.com/folke/snacks.nvim/commit/76160be5d38cd67e46557cb5d0b3e36ececdfa3c))\n* **picker.lsp:** fixed `vim.str_byteindex` capability check. Closes [#2389](https://github.com/folke/snacks.nvim/issues/2389) ([46917d0](https://github.com/folke/snacks.nvim/commit/46917d0629595281e7d2de3834af8fd95584befd))\n* **picker.lsp:** some LSP servers notify completion before sending the actual result. See [#2372](https://github.com/folke/snacks.nvim/issues/2372) ([aa8a318](https://github.com/folke/snacks.nvim/commit/aa8a318779ca6b5f0bba2cf383ddf596db199c09))\n* **picker.lsp:** use `LspRequest` to track completed and cancelled requests. Fixes [#2364](https://github.com/folke/snacks.nvim/issues/2364) ([8afb609](https://github.com/folke/snacks.nvim/commit/8afb609333026e1a3a27d57ada1cf849b2adbcc9))\n* **picker.preview:** again. docgen seems broken ([758bbfa](https://github.com/folke/snacks.nvim/commit/758bbfa13a3c26d80069a9f621fcfd0f0dfc608e))\n* **picker.preview:** don't show locations for diff preview ([b064488](https://github.com/folke/snacks.nvim/commit/b0644884ef3aa589df609c95565220da7eef5cce))\n* **picker.preview:** fckup ([fd7795e](https://github.com/folke/snacks.nvim/commit/fd7795e9cd615d5262862c819b5058b42869406b))\n* **picker.preview:** fix ([e2c1c52](https://github.com/folke/snacks.nvim/commit/e2c1c527e40aecd6d1ac011aef6d3c28a208a9ec))\n* **picker.preview:** show proper preview message for deleted scratch buffers ([4ad8a41](https://github.com/folke/snacks.nvim/commit/4ad8a41eac2fb636e12a11e0129d6d2d10ffb60a))\n* **picker.util:** better relative time format ([3e30fb6](https://github.com/folke/snacks.nvim/commit/3e30fb6c705c94cf29567cc6446bd9f9284c8c4d))\n* **picker.util:** ignore errors from corrupted kv stores. Closes [#2394](https://github.com/folke/snacks.nvim/issues/2394) ([b3d01c5](https://github.com/folke/snacks.nvim/commit/b3d01c59ba4ab4183b12e9e10bc1fcbbef1b02be))\n* **picker.watch:** check again for closed picker after schedule. See [#2365](https://github.com/folke/snacks.nvim/issues/2365) ([8ad80de](https://github.com/folke/snacks.nvim/commit/8ad80de67b68db773cd50599f93dfada85e00eae))\n* **picker:** close picker when layout closes. Closes [#2365](https://github.com/folke/snacks.nvim/issues/2365) ([779746f](https://github.com/folke/snacks.nvim/commit/779746f9a82e3a925393557ead441a3d22606534))\n* **picker:** dont watch files for closed pickers. Fixes [#2365](https://github.com/folke/snacks.nvim/issues/2365) ([c4ec8b6](https://github.com/folke/snacks.nvim/commit/c4ec8b6d12a9b671e65709ec64b9beb6969815c5))\n* **picker:** increase default show_delay to 5s. Closes [#2364](https://github.com/folke/snacks.nvim/issues/2364) ([b3197e3](https://github.com/folke/snacks.nvim/commit/b3197e3a2a2cec8e090a74308b630a1f451a35f0))\n* **picker:** only trim space in the title if it's preceded by a word like character (skips icons) ([2439c49](https://github.com/folke/snacks.nvim/commit/2439c493a5dbd9fd880bf70c7986ebafd6f0c9f6))\n* **picker:** pause input progress info for 60ms to prevent flickering when finder is too fast ([ecde81f](https://github.com/folke/snacks.nvim/commit/ecde81fc0ce7c4834def0ce710dd5dc62b0822fc))\n* **scratch:** make sure zindex of scratch window is higher than existing floating windows ([c8422da](https://github.com/folke/snacks.nvim/commit/c8422da50dee7d725e1a66a5dc6a930e6ac57625))\n* **scroll:** only reset count when needed ([551d79f](https://github.com/folke/snacks.nvim/commit/551d79f1c0bd5400bcf00d2133832c20b1fb29f2))\n* **util.job:** scroll to top when process exits ([b544157](https://github.com/folke/snacks.nvim/commit/b5441575e07af9f179cbeb8ea6d0b9951b28481a))\n* **util.job:** stop on BufWipeout and BufDelete ([c956b37](https://github.com/folke/snacks.nvim/commit/c956b372467467dafb32713a95d3dbc22ae5c3bc))\n* **util.job:** stop when attached buffer is no longer valid ([221d4b1](https://github.com/folke/snacks.nvim/commit/221d4b17475c36fd92b2e26baac5515f8260ef88))\n* **util.job:** use nvim_win_set_cursor instead of `gg` ([5faed2f](https://github.com/folke/snacks.nvim/commit/5faed2f7abed7fb97aed0425b2b1b03fb6048fa9))\n* **util.lsp:** `Snacks.util.lsp.on()` should trigger for each lsp client per buffer ([52f30a1](https://github.com/folke/snacks.nvim/commit/52f30a198a19bf5da6aa95cc642bfbb99b9bbfbf))\n* **util:** color() should not create hl groups ([17033e6](https://github.com/folke/snacks.nvim/commit/17033e67ef1c42a2295e2921c201f1b404d625d8))\n* **win:** ignore errors on destroy. Closes [#2381](https://github.com/folke/snacks.nvim/issues/2381) ([a8930bd](https://github.com/folke/snacks.nvim/commit/a8930bdb619024e8ba3e1fc7efc1bd8ea9a27a5a))\n* **win:** scratch buffers were sometimes not deleted ([0387297](https://github.com/folke/snacks.nvim/commit/03872973b3326ab2caff2b10d983b4cb775944f0))\n* **win:** when fixbuf triggers in a floating window, just close it. Closes [#2380](https://github.com/folke/snacks.nvim/issues/2380) ([de35242](https://github.com/folke/snacks.nvim/commit/de352425f7acd4dd1ed3ee06f9479129017da087))\n\n\n### Performance Improvements\n\n* **animate:** smoother animations ([b7a3fed](https://github.com/folke/snacks.nvim/commit/b7a3fed8d9822f448122af441018f66febbc50f4))\n* **notifier:** stop trying to fit more notifs in the layout after skipping max 10 ([3a8ecf5](https://github.com/folke/snacks.nvim/commit/3a8ecf591263e4706d9b3a45da590df914ea5505))\n* **picker.util:** cache badge hl groups ([cb85844](https://github.com/folke/snacks.nvim/commit/cb85844e8404a95c3ac0d509ec7cedd0f3d5375c))\n* **scroll:** combine all scrolling commands in one command + restore vim.v.count ([0fbea13](https://github.com/folke/snacks.nvim/commit/0fbea13c9d5ba2887ad8c1ffb20d77e11174b390))\n* **scroll:** smoother scrolling using new animations ([2221fe6](https://github.com/folke/snacks.nvim/commit/2221fe616657b9ed82bdea8566813939a7b25918))\n* **statuscolumn:** only calculate components that are actually needed ([bb80317](https://github.com/folke/snacks.nvim/commit/bb803176478dc603c1a2d09ca717964c6a27bfae))\n\n\n### Reverts\n\n* jump `buffer` -&gt; `buffer!`. See [#2378](https://github.com/folke/snacks.nvim/issues/2378) ([143e9b5](https://github.com/folke/snacks.nvim/commit/143e9b58c7b8301bdc36b1b8a03449078beb49d1))\n\n## [2.27.0](https://github.com/folke/snacks.nvim/compare/v2.26.0...v2.27.0) (2025-10-26)\n\n\n### Features\n\n* **keymap:** added new `enabled` option ([b0f21fa](https://github.com/folke/snacks.nvim/commit/b0f21fa745953ac6bb096a4811cb32e42d7ca714))\n* **picker.proc:** finder to process json ([5294c4f](https://github.com/folke/snacks.nvim/commit/5294c4f39ed9bdc0f2c483885d9a1834a4df4d21))\n* **util.job:** simple wrapper around jobstart to work with terminals (used in dashboards and pickers) ([de05631](https://github.com/folke/snacks.nvim/commit/de05631e6a656a88d1eebf078c44e5e4b9747742))\n* **util.lsp:** added overload for `Snacks.util.lsp.on(cb)` ([f33aa20](https://github.com/folke/snacks.nvim/commit/f33aa2017a2671fb4a0e71316f385c8010c8b81b))\n\n\n### Bug Fixes\n\n* **dashboard:** don't add sleep in nushell. Closes [#1706](https://github.com/folke/snacks.nvim/issues/1706) ([44f71d2](https://github.com/folke/snacks.nvim/commit/44f71d2113866c0a6f16a8fa70af8933c1d87919))\n* **explorer:** refresh git status on all tabs when needed. Closes [#2348](https://github.com/folke/snacks.nvim/issues/2348) ([1472211](https://github.com/folke/snacks.nvim/commit/1472211f9ccd171f69ec7f33764620dd935b5ccf))\n* **explorer:** windows path fixes ([e1dc6b3](https://github.com/folke/snacks.nvim/commit/e1dc6b3bddd0d16d0faa5d6802a975f7a7165b2a))\n\n## [2.26.0](https://github.com/folke/snacks.nvim/compare/v2.25.0...v2.26.0) (2025-10-25)\n\n\n### Features\n\n* **explorer:** add cross-platform trash support ([ed08ef1](https://github.com/folke/snacks.nvim/commit/ed08ef1a630508ebab098aa6e8814b89084f8c03))\n* **keymap:** add filetype and LSP-aware keymap management ([0bf34af](https://github.com/folke/snacks.nvim/commit/0bf34afe34ee297430f23d2aba0b104c5379dc15))\n* **util:** add LSP utility module with dynamic capability handlers ([7a63ba5](https://github.com/folke/snacks.nvim/commit/7a63ba5d374acaa7317833b6e03d2603e90e0983))\n* **win:** add `SnacksWinSeparator` to default `win.wo.winhighlight` ([#2340](https://github.com/folke/snacks.nvim/issues/2340)) ([869709d](https://github.com/folke/snacks.nvim/commit/869709dd658b53ea5706f086f93a50e89a429a5d)), closes [#2336](https://github.com/folke/snacks.nvim/issues/2336)\n* **win:** add default *Snacks* prefixed `WinSeparator` ([#2338](https://github.com/folke/snacks.nvim/issues/2338)) ([381265b](https://github.com/folke/snacks.nvim/commit/381265b5430c991f4343a2f4530bc2de37abac18)), closes [#2336](https://github.com/folke/snacks.nvim/issues/2336)\n\n\n### Bug Fixes\n\n* **dahboard:** do full terminal reset when receiving first output and displayed cached contents ([c952834](https://github.com/folke/snacks.nvim/commit/c9528341a6ef9dc9cb404b1c901b1276af331ccf))\n* **dashboard:** don't write to closed terminal buffer ([f75eaf1](https://github.com/folke/snacks.nvim/commit/f75eaf1e18cea03605e626eca2a1b9c4345071d4))\n* **dashboard:** work-around for jobstart+pty issue where not all output is processed before exit. Closes [#1706](https://github.com/folke/snacks.nvim/issues/1706) ([4d776bd](https://github.com/folke/snacks.nvim/commit/4d776bdd1d6d7998f2c7c7f08c2e964419eb74be))\n* **explorer:** macos has `trash` pre-installed, so no need to try `osascript` and move to first. Closes [#2349](https://github.com/folke/snacks.nvim/issues/2349) ([d569072](https://github.com/folke/snacks.nvim/commit/d569072b2e39e0078b55ea56b133fb9a30d78bad))\n* **image:** detach image when reloading image file. Closes [#2343](https://github.com/folke/snacks.nvim/issues/2343) ([7bf4175](https://github.com/folke/snacks.nvim/commit/7bf4175588a784bbf7463b68351833ed64f5c6cc))\n* **image:** increase timeout for querying the terminal. Closes [#2344](https://github.com/folke/snacks.nvim/issues/2344) ([4122143](https://github.com/folke/snacks.nvim/commit/4122143240fb7f43b27dddec670b844404cb08db))\n* **image:** let healthcheck wait till terminal detection is done ([b029511](https://github.com/folke/snacks.nvim/commit/b029511abb1359da28de45faeeec400f419d7ee7))\n* **image:** only attach to a buffer once. Closes [#2343](https://github.com/folke/snacks.nvim/issues/2343) ([6f72643](https://github.com/folke/snacks.nvim/commit/6f726433232422d26157adfd0df3dd464341222b))\n* **image:** work around tmux extended-keys breaking TermResponse. Closes [#2332](https://github.com/folke/snacks.nvim/issues/2332) ([e93dcfd](https://github.com/folke/snacks.nvim/commit/e93dcfdf394ef16732f06021d941146be912043c))\n* **layout:** provide parent win width/height when relative to win ([#2346](https://github.com/folke/snacks.nvim/issues/2346)) ([602393a](https://github.com/folke/snacks.nvim/commit/602393aed2dd8059e74afff6712a423b6f048cfe))\n* **picker:** fix race condition causing \"Finder yielded after done\" error. Closes [#2327](https://github.com/folke/snacks.nvim/issues/2327) ([c9ccbe5](https://github.com/folke/snacks.nvim/commit/c9ccbe56179f1d4adb06fea47f4eea0c57736c2d))\n* **picker:** set min file width to 40 ([69417ac](https://github.com/folke/snacks.nvim/commit/69417ac68152bc08d0ea0640e211f2a3eb48bac6))\n* **win:** use `sbuffer` instead of `split` for split windows ([bbd6d42](https://github.com/folke/snacks.nvim/commit/bbd6d42a9738c3a4c7c35f5ebde91a5ede8bec3a))\n\n\n### Performance Improvements\n\n* **picker:** don't use treesitter string parser, since a change in nightly creates thousands of unlisted buffers in that case ([ad6c0a5](https://github.com/folke/snacks.nvim/commit/ad6c0a5e542b6b47b4ac5e2ebcbdd663b8a7e908))\n* **picker:** re-use existing string parsers per language to prevent needing to create new parsers ([efa304a](https://github.com/folke/snacks.nvim/commit/efa304a078993198e6fa088845fe8925708abb4e))\n\n## [2.25.0](https://github.com/folke/snacks.nvim/compare/v2.24.0...v2.25.0) (2025-10-23)\n\n\n### Features\n\n* **notifier:** added `gap` option. Closes [#2331](https://github.com/folke/snacks.nvim/issues/2331) ([b1acbb0](https://github.com/folke/snacks.nvim/commit/b1acbb0fcce9ed1ead3fd511eb934eeefe238b69))\n* **select:** allow configuring options for specific vim.ui.select kinds ([bca5b05](https://github.com/folke/snacks.nvim/commit/bca5b058388fb381f6d04c3624a541f7c0637382))\n* **snacks:** added `Snacks.version`. auto updated by the release workflow ([a283beb](https://github.com/folke/snacks.nvim/commit/a283beb6dc94f7a17c48dcb6878e0dd3493bf370))\n\n\n### Bug Fixes\n\n* **dashboard:** fix issue with opening file at location due to splitkeep and restoring laststatus/showtabline ([1a2b34d](https://github.com/folke/snacks.nvim/commit/1a2b34dffd524b0f7373c5868dbb7597360e1a8c))\n* **scroll:** stop animations when buf/changedtick changes ([a42b376](https://github.com/folke/snacks.nvim/commit/a42b3761f702e770d745709682dfe3d7e3ef1bb6))\n\n## [2.24.0](https://github.com/folke/snacks.nvim/compare/v2.23.0...v2.24.0) (2025-10-23)\n\n\n### Features\n\n* **bigfile:** disable mini-hipatterns ([#2170](https://github.com/folke/snacks.nvim/issues/2170)) ([3d4dd13](https://github.com/folke/snacks.nvim/commit/3d4dd13d2e7e33b81ffda9baa58f8852e4ca84f6))\n* **dashboard:** optional `filter` for projects. Closes [#798](https://github.com/folke/snacks.nvim/issues/798) ([fe88a07](https://github.com/folke/snacks.nvim/commit/fe88a07d5337e21317ab1a7613add6c364bb9eae))\n* **debug:** allow debug evaluation of block selections ([#1331](https://github.com/folke/snacks.nvim/issues/1331)) ([231ffae](https://github.com/folke/snacks.nvim/commit/231ffae08d956039899fb56000a6e03a99819905))\n* **git:** allow configuring extra git args and git cmd args for all git sources. See [#2178](https://github.com/folke/snacks.nvim/issues/2178) ([5782b5c](https://github.com/folke/snacks.nvim/commit/5782b5cda0a2dcc032ec16cbdd6e39fb2baedaa6))\n* **image:** add icns support ([#2120](https://github.com/folke/snacks.nvim/issues/2120)) ([9df47bc](https://github.com/folke/snacks.nvim/commit/9df47bce6a3b752831b4970c26a8886b2843e9bb))\n* **image:** added clear fun. Closes [#1394](https://github.com/folke/snacks.nvim/issues/1394) ([30687d1](https://github.com/folke/snacks.nvim/commit/30687d195b060e1857cbf905b672af6e48dacc2a))\n* **image:** added support for base64 encoded images in url. Closes [#2304](https://github.com/folke/snacks.nvim/issues/2304) ([2c56e10](https://github.com/folke/snacks.nvim/commit/2c56e10b1dd69dcebf4d0915af396dd0bd7123a6))\n* **image:** allow specifying a page number for inlined pdfs ([#1806](https://github.com/folke/snacks.nvim/issues/1806)) ([3f0fe34](https://github.com/folke/snacks.nvim/commit/3f0fe34308b06e66c8ce1ce07f0b18a4d9525bdc))\n* **indent:** pass win to filter func. Closes [#2307](https://github.com/folke/snacks.nvim/issues/2307) ([8116e0b](https://github.com/folke/snacks.nvim/commit/8116e0b380701a3b1deafe8d14416be6ee403f6f))\n* **input:** added support for a custom highlight functions. Closes [#2216](https://github.com/folke/snacks.nvim/issues/2216) ([9b80137](https://github.com/folke/snacks.nvim/commit/9b80137aced62886de5e1399eb6e4000e57683de))\n* **layout:** height=0.7 for preview in vscode layout ([c3d6c01](https://github.com/folke/snacks.nvim/commit/c3d6c019165e55d704f2596562dd310c7b0a8a10))\n* **layout:** static (non-flex) layouts now shrink the root box to fit the contents. See [#2035](https://github.com/folke/snacks.nvim/issues/2035) ([ba7845b](https://github.com/folke/snacks.nvim/commit/ba7845bb85a0cfad3c7c5f75b3bc05c68679a090))\n* **picker.finder:** added assertions that finder is still running when receiving results ([a45503b](https://github.com/folke/snacks.nvim/commit/a45503b95752055e19186b75a4f9874cd39aa834))\n* **picker.git_diff:** add `base` option to show diff against a merge base. Useful to see changes on a branch/PR ([7964f04](https://github.com/folke/snacks.nvim/commit/7964f040bf605b2a3e8d66d02c453469352e005e))\n* **picker.git:** allow passing extra args to git log command for file renames ([#1964](https://github.com/folke/snacks.nvim/issues/1964)) ([2aee35d](https://github.com/folke/snacks.nvim/commit/2aee35d0591f80b4a186e0ad3c600cd05c3f2a4d))\n* **picker.git:** use default previewer args in git_show ([#1736](https://github.com/folke/snacks.nvim/issues/1736)) ([f324f96](https://github.com/folke/snacks.nvim/commit/f324f96bea48727d4a5967d443c3c2314fe4af60))\n* **picker.layout:** added `config` hook for resolved layouts. See [#2035](https://github.com/folke/snacks.nvim/issues/2035) ([722f9ea](https://github.com/folke/snacks.nvim/commit/722f9eac7c459364a37d81e6a8df92fe0ee9d6da))\n* **picker.lsp_config:** added more info to lsp picker ([636be5c](https://github.com/folke/snacks.nvim/commit/636be5c3d1b35b2041123efcc5b2a86df0dc9f93))\n* **picker.lsp:** added lsp_incoming_calls and lsp_outgoing_calls. Closes [#1843](https://github.com/folke/snacks.nvim/issues/1843) ([55d6670](https://github.com/folke/snacks.nvim/commit/55d6670a7eb2667d467489b5c6c6a2ed428cead2))\n* **picker.lsp:** added option `keep_parents` to `lsp_symbols` (default `false`). See [#2083](https://github.com/folke/snacks.nvim/issues/2083). closes [#2266](https://github.com/folke/snacks.nvim/issues/2266) ([2b9d522](https://github.com/folke/snacks.nvim/commit/2b9d52258d83361479b5a4a6fca6845c58e08050))\n* **picker.projects:** make max_depth customizable ([#2253](https://github.com/folke/snacks.nvim/issues/2253)) ([3e9e2e2](https://github.com/folke/snacks.nvim/commit/3e9e2e2d71cb869467072bfd6864aa5179f8749c))\n* **picker.scratch:** add scratch picker with grep, new and delete keybinds ([#1019](https://github.com/folke/snacks.nvim/issues/1019)) ([ca0f8b2](https://github.com/folke/snacks.nvim/commit/ca0f8b2c09a6b437479e7d12bdb209731d9eb621))\n* **picker.select:** select now fits the list to the items independent of the layout. Closes [#2035](https://github.com/folke/snacks.nvim/issues/2035) ([5c63614](https://github.com/folke/snacks.nvim/commit/5c63614880f6a1c911cd2e0cf9291bcea7612950))\n* **picker:** add author field to git log ([#2295](https://github.com/folke/snacks.nvim/issues/2295)) ([2cf864a](https://github.com/folke/snacks.nvim/commit/2cf864aaa1bc31a4d030a52fe03ebac3e65be65d))\n* **picker:** add exact match position highlighting for grep results ([3b54c8d](https://github.com/folke/snacks.nvim/commit/3b54c8d3d1f0cd5b2698e343b218a01a42f4388f))\n* **picker:** add git_restore action for git_status picker ([2b22fe7](https://github.com/folke/snacks.nvim/commit/2b22fe78614a001c51c0b4025236770817ac999e))\n* **picker:** add toggle_regex for grep ([#1594](https://github.com/folke/snacks.nvim/issues/1594)) ([bd6ee23](https://github.com/folke/snacks.nvim/commit/bd6ee235463dc55c650396fae2ea02e32d4c1496))\n* **picker:** added `Snacks.picker.tags()` a picker for ctags. Closes [#1728](https://github.com/folke/snacks.nvim/issues/1728) ([4290287](https://github.com/folke/snacks.nvim/commit/42902871f5ff95bd0b87790abdadb1eb10a42fd1))\n* **picker:** added custom options to `vim.ui.select` that snacks can use for a better select ([264cab1](https://github.com/folke/snacks.nvim/commit/264cab138039412a151b21fdc30d4928f50d79b4))\n* **picker:** added live support to `git_log`, which uses `-S` (pickaxe)  to search. Closes [#1544](https://github.com/folke/snacks.nvim/issues/1544) ([c9fa6f7](https://github.com/folke/snacks.nvim/commit/c9fa6f7b0724587d4c4974817aad96d93f469437))\n* **picker:** allow configuring pathspec for git grep ([#2311](https://github.com/folke/snacks.nvim/issues/2311)) ([57fbda7](https://github.com/folke/snacks.nvim/commit/57fbda70d66d808c10974448d4003f567b78e784))\n* **picker:** also ignore dot bare git files ([#2058](https://github.com/folke/snacks.nvim/issues/2058)) ([4bb0dae](https://github.com/folke/snacks.nvim/commit/4bb0dae95d1ecc10daf50fd490deaf9a28b26f1c))\n* **picker:** enhanced resume with multi-state support and flexible API ([bc6c446](https://github.com/folke/snacks.nvim/commit/bc6c446c11a92bc5b6d5a960bcf3488c519c647a))\n* **picker:** flexible filename format ([#2294](https://github.com/folke/snacks.nvim/issues/2294)) ([9ad5d53](https://github.com/folke/snacks.nvim/commit/9ad5d5374ac7cd24c79e99a4645add1960eb93fa))\n* **picker:** mapped `<c-g>` to `print_cwd` in list. See [#2244](https://github.com/folke/snacks.nvim/issues/2244) ([faa6aba](https://github.com/folke/snacks.nvim/commit/faa6abacb40f2e02203f2baabc988e3564d63952))\n* **picker:** Support rmagatti/autosession session manager ([#1825](https://github.com/folke/snacks.nvim/issues/1825)) ([fc06234](https://github.com/folke/snacks.nvim/commit/fc06234ce13b7e653e0a5947a266abf016dc163f))\n* **picker:** updated Snacks.picker.lsp_config to work with `vim.lsp.config` ([292d46f](https://github.com/folke/snacks.nvim/commit/292d46f773af05aaea6a21f13fcc179adea95494))\n* **picker:** when resuming a source that has nothing to resume, start a picker with the source instead ([db3c13c](https://github.com/folke/snacks.nvim/commit/db3c13c28e0e7231bc3a45cd0db0e6683fc6c2c5))\n* **terminal:** minor improvements for user experience ([#2276](https://github.com/folke/snacks.nvim/issues/2276)) ([39b14c4](https://github.com/folke/snacks.nvim/commit/39b14c400653f320133b3f8c65cdb612e42f9ca1))\n* **toggle:** allow notification customization via function ([#2247](https://github.com/folke/snacks.nvim/issues/2247)) ([3ccab97](https://github.com/folke/snacks.nvim/commit/3ccab9736b298c8a8ef13aca5e3e9e7dc64c73bd))\n* **win:** added support for `vim.o.winborder`. Set win.border = true to use it ([b30523c](https://github.com/folke/snacks.nvim/commit/b30523c89fda32efe43e99fe71235d63c9a44a3b))\n* **win:** all existing snacks windows for all plugins now honor `vim.o.winborder`. Defaults to `rounded` if not set. ([c1737d8](https://github.com/folke/snacks.nvim/commit/c1737d866ebddb8270e3bdce9ec6f425ff24fb48))\n* **win:** generalize footer options for keys ([#363](https://github.com/folke/snacks.nvim/issues/363)) ([b8d1719](https://github.com/folke/snacks.nvim/commit/b8d17192b663305398df98930ac79b3c7612b809))\n* **win:** make split window \"stacking\" configurable ([e46a094](https://github.com/folke/snacks.nvim/commit/e46a09427cfed62ea7f37039b76b2b2a13fddec8))\n\n\n### Bug Fixes\n\n* **bigfile:** bigfile doesn't work on windows. ([#1969](https://github.com/folke/snacks.nvim/issues/1969)) ([b4944ff](https://github.com/folke/snacks.nvim/commit/b4944ff320ca23ba10a7498da4dfe13e8065395e)), closes [#1722](https://github.com/folke/snacks.nvim/issues/1722)\n* **bufdelete:** try alternate buffer first and otherwise last used buffer ([914c900](https://github.com/folke/snacks.nvim/commit/914c9004be843c96b43fd86a1010c00dc147e5b4))\n* **dashboard:** fix path filtering for `recent_files` with `cwd` option ([#2201](https://github.com/folke/snacks.nvim/issues/2201)) ([057d4ab](https://github.com/folke/snacks.nvim/commit/057d4ab80e42d76ae0d24d0582d27cf3630c0ec1))\n* **dashboard:** oldfiles filter should return a boolean instead of the result of find. Fixes [#2283](https://github.com/folke/snacks.nvim/issues/2283) ([fcd309f](https://github.com/folke/snacks.nvim/commit/fcd309f9ea8529d5128474720ecf275bd8ee4ce5))\n* **dashboard:** pcall chansend for dashoard terminal widgets ([dc65ffd](https://github.com/folke/snacks.nvim/commit/dc65ffd4f591fd68f1433e4bd815af832ed737b8))\n* **dashboard:** recent cwd filter matching ([5c4365e](https://github.com/folke/snacks.nvim/commit/5c4365e99398fc67f0b4379d6e4a4b581bc3f485))\n* **dashboard:** recent_files section not displaying files without cwd parameter ([#2284](https://github.com/folke/snacks.nvim/issues/2284)) ([1ed737e](https://github.com/folke/snacks.nvim/commit/1ed737e46502ed2e47dd9cc9825d881ae9844b7f))\n* **dashboard:** replace deprecated AutoSession command ([#2288](https://github.com/folke/snacks.nvim/issues/2288)) ([e9228d6](https://github.com/folke/snacks.nvim/commit/e9228d6b2f64631b49619466ebdd75c0da37e1f8))\n* **dashboard:** restore showtabline/laststatus when entering another non-float window. Closes [#1774](https://github.com/folke/snacks.nvim/issues/1774) ([cc69a93](https://github.com/folke/snacks.nvim/commit/cc69a9304bfdd047b7ea9ffa879985c351c6327b))\n* **dashboard:** set `border = \"none\"` on `terminal` sections ([#1643](https://github.com/folke/snacks.nvim/issues/1643)) ([83f364f](https://github.com/folke/snacks.nvim/commit/83f364f8334801d163ecc02c717dd8ee1e07ad53))\n* **dashboard:** update cursor on loading the dashboard. Closes [#2004](https://github.com/folke/snacks.nvim/issues/2004) ([29682a0](https://github.com/folke/snacks.nvim/commit/29682a0a724cb81a109f70bf0cbadb0cf6bcc630))\n* **dashboard:** use fqn for icon. Closes [#1496](https://github.com/folke/snacks.nvim/issues/1496) ([24e92e0](https://github.com/folke/snacks.nvim/commit/24e92e0c947f6a22e6b131d405549c607dc9f5f0))\n* **dim:** fixed the issue of dim's scope variable being nil and outputting… ([#1938](https://github.com/folke/snacks.nvim/issues/1938)) ([943a3c7](https://github.com/folke/snacks.nvim/commit/943a3c7d4a97950900fdc16784f3d11f3ca574ea))\n* **explorer.git:** don't propagate deletes to parent dirs that don't exist ([835c4cb](https://github.com/folke/snacks.nvim/commit/835c4cbfc6043a3abab8c8f01cd67e368a90cd93))\n* **explorer.watch:** handle systems where fs_event doesn't return file names. Closes [#2190](https://github.com/folke/snacks.nvim/issues/2190). Closes [#2032](https://github.com/folke/snacks.nvim/issues/2032) ([d6e34b1](https://github.com/folke/snacks.nvim/commit/d6e34b158d892cd774b36503cbd3a9e62c7951e3))\n* **explorer:** mounted directories being detected as non-directories in Tree:expand ([#2053](https://github.com/folke/snacks.nvim/issues/2053)) ([7a5eb10](https://github.com/folke/snacks.nvim/commit/7a5eb1036a313db1880e2e11f42aa6a1c20f1a23))\n* **explorer:** reset main when entering another window. Closes [#1587](https://github.com/folke/snacks.nvim/issues/1587) ([a5d45d5](https://github.com/folke/snacks.nvim/commit/a5d45d543e1c506fb0d49ac569c2a0ce5403ac37))\n* **git:** always check parents for git root to fix an issue with git submodules. Closes [#2143](https://github.com/folke/snacks.nvim/issues/2143) ([14dd362](https://github.com/folke/snacks.nvim/commit/14dd362d5dd500806c2f5357f4afd8145a268a01))\n* **gitbrowse:** fixed urls for gitlab ([#2073](https://github.com/folke/snacks.nvim/issues/2073)) ([9ebf052](https://github.com/folke/snacks.nvim/commit/9ebf052feff78411c2f68bfa94d0a17cbf1e6d85))\n* **gitbrowse:** send commit as a opt when calling gitbrowse ([#2289](https://github.com/folke/snacks.nvim/issues/2289)) ([a466429](https://github.com/folke/snacks.nvim/commit/a4664298ba6669ec14f704b9602339f448bd45c9))\n* **git:** set `diff.noprefix=false` for `git diff` to ensure correct format ([#2174](https://github.com/folke/snacks.nvim/issues/2174)) ([93f43ca](https://github.com/folke/snacks.nvim/commit/93f43ca10f77e7b22f1e58cbeba6d7e3e8b04d9f))\n* **image.terminal:** do only terminal detection for now. Closes [#2323](https://github.com/folke/snacks.nvim/issues/2323) ([6c7ddae](https://github.com/folke/snacks.nvim/commit/6c7ddae887ca6f0776eb46b57c6f78f25ddf0238))\n* **image:** correct off by one issue in render fallback ([#1560](https://github.com/folke/snacks.nvim/issues/1560)) ([441bdcd](https://github.com/folke/snacks.nvim/commit/441bdcd2103bb43f3275f4aca6d76d93fd1aaa92))\n* **image:** correct render fallback to handle \"editor\" relative position ([#2296](https://github.com/folke/snacks.nvim/issues/2296)) ([c552cea](https://github.com/folke/snacks.nvim/commit/c552cea13199e518c167c8815156ae9c01577b27))\n* **image:** correct render fallback to handle \"editor\" relative position ([#2297](https://github.com/folke/snacks.nvim/issues/2297)) ([1c3f15c](https://github.com/folke/snacks.nvim/commit/1c3f15cb54c0ee12b8cd4fd59a8ddc5ebe1fdd3c))\n* **image:** detect kitty image protocol through terminal capability request. Closes [#1695](https://github.com/folke/snacks.nvim/issues/1695) ([43261ba](https://github.com/folke/snacks.nvim/commit/43261baf87fdc5f970c12e9a6c795ba8a4e7595c))\n* **image:** do not save remote image if fetch fails ([#1915](https://github.com/folke/snacks.nvim/issues/1915)) ([cb6bf05](https://github.com/folke/snacks.nvim/commit/cb6bf052daa11c287d0d8fa8f168190eb40c0c8d))\n* **image:** ENOENT on preview ([#2301](https://github.com/folke/snacks.nvim/issues/2301)) ([5173e96](https://github.com/folke/snacks.nvim/commit/5173e96f3359121233e817c12307d531a8622e4f))\n* **image:** hover close in insert mode ([#2215](https://github.com/folke/snacks.nvim/issues/2215)) ([ef59af0](https://github.com/folke/snacks.nvim/commit/ef59af0ffc1289602a0792ee03724d4e36a0a229))\n* **image:** markdown inline link query for shortened urls ([#1481](https://github.com/folke/snacks.nvim/issues/1481)) ([2daa1b2](https://github.com/folke/snacks.nvim/commit/2daa1b28b2151f3add39863de4df245a5140badf))\n* **image:** set winblend=0 for floatwin when use unicode placeholders ([#1615](https://github.com/folke/snacks.nvim/issues/1615)) ([758e64c](https://github.com/folke/snacks.nvim/commit/758e64c18fc5934244ab48be8d17b90fa36ad16e))\n* **image:** skip `\\usepackage` in comments and body ([#2325](https://github.com/folke/snacks.nvim/issues/2325)) ([90227af](https://github.com/folke/snacks.nvim/commit/90227af4977504ae2d4294fe76a63117dfd18498))\n* **image:** work-around for sha256 not allowed to be a Blob ([92a08ce](https://github.com/folke/snacks.nvim/commit/92a08cece72aeb67cf2a527991cbffdab093db5e))\n* **indent:** check that win is valid in step. Closes [#1943](https://github.com/folke/snacks.nvim/issues/1943) ([e409f31](https://github.com/folke/snacks.nvim/commit/e409f31cc968f90139ad66941827b42ef95de7fd))\n* **indent:** nil check before setting extmark ([#1635](https://github.com/folke/snacks.nvim/issues/1635)) ([02bf7d2](https://github.com/folke/snacks.nvim/commit/02bf7d2205ea7a4b903fa5266668f9fc7768f6c9))\n* **input:** schedule stopinsert. Fixes [#1841](https://github.com/folke/snacks.nvim/issues/1841) ([ad6cbc8](https://github.com/folke/snacks.nvim/commit/ad6cbc8d5d4b49c8030083c1f55fc7c3679f3ac4))\n* **input:** zindex ([67d690d](https://github.com/folke/snacks.nvim/commit/67d690d3625ff2899a7505a418bda91cc59042f7))\n* **input:** zindex. Closes [#2302](https://github.com/folke/snacks.nvim/issues/2302) ([d491236](https://github.com/folke/snacks.nvim/commit/d49123694157597e64c284e5bd541cdd31538ba8))\n* **layout:** allocate at least 1 cell for a widget and enlarge/shrink the root box when needed. Closes [#2261](https://github.com/folke/snacks.nvim/issues/2261) ([71d6d3c](https://github.com/folke/snacks.nvim/commit/71d6d3cad4cb842e4053f7bb39894144ee0dc81b))\n* **layout:** allow width/height to be a function. Closes [#2184](https://github.com/folke/snacks.nvim/issues/2184) ([c757d4d](https://github.com/folke/snacks.nvim/commit/c757d4dc28c5baedaab44a59545760d905d87b20))\n* **lazygit:** allow extensible user args ([#789](https://github.com/folke/snacks.nvim/issues/789)) ([da655a3](https://github.com/folke/snacks.nvim/commit/da655a353849bccb73d66dbb3caa9c238e7b0cae))\n* **lazygit:** check if default config file exists before adding to LG_CONFIG_FILE ([#2256](https://github.com/folke/snacks.nvim/issues/2256)) ([3731644](https://github.com/folke/snacks.nvim/commit/3731644e38fe494399e75dc893215d1a801654ff))\n* **main:** get correct winid for prev window ([db399b1](https://github.com/folke/snacks.nvim/commit/db399b1332848477b0cd881faabe95a0efddf1c6))\n* **notifier:** include icon in padding in minimal style ([#2239](https://github.com/folke/snacks.nvim/issues/2239)) ([6daef52](https://github.com/folke/snacks.nvim/commit/6daef528c1422b33b2f2f713822602f9d66a5d51))\n* **notifier:** keep filtered notifications in history ([#2209](https://github.com/folke/snacks.nvim/issues/2209)) ([ac61546](https://github.com/folke/snacks.nvim/commit/ac6154688baa79ec099fd662365fccf1a2feefd1))\n* **picker.actions:** `<c-g>` in list view now prints file path instead of cwd. Fallback to cwd ([0b0a58a](https://github.com/folke/snacks.nvim/commit/0b0a58ae4aa643e66ff2b87ce5087857bcab1756))\n* **picker.actions:** ensure the current window is updated after tabdrop ([#2326](https://github.com/folke/snacks.nvim/issues/2326)) ([b30121b](https://github.com/folke/snacks.nvim/commit/b30121bfce84fdcbe53cb724c97388cbe4e18980))\n* **picker.actions:** multi-action descriptions. Fixes [#1501](https://github.com/folke/snacks.nvim/issues/1501) ([4edf207](https://github.com/folke/snacks.nvim/commit/4edf207bfeef70e3062a604825766c81d8809359))\n* **picker.actions:** take into account if source is `recent` explicitly ([#1920](https://github.com/folke/snacks.nvim/issues/1920)) ([b9bd8ae](https://github.com/folke/snacks.nvim/commit/b9bd8ae98213973131d21abf806e56c3f1c8f0a9))\n* **picker.core:** respect camelCase for scoring when ignorecase is true ([#1601](https://github.com/folke/snacks.nvim/issues/1601)) ([a32735b](https://github.com/folke/snacks.nvim/commit/a32735b9e8513bd71d82744597490ac10e343c6e))\n* **picker.format:** added min_width for truncated paths ([b7f8116](https://github.com/folke/snacks.nvim/commit/b7f811613a0a999f6a275260ef2963ecff3a16e8))\n* **picker.format:** apply hidden file hl group last. Fixes [#2127](https://github.com/folke/snacks.nvim/issues/2127) ([0bf8fe4](https://github.com/folke/snacks.nvim/commit/0bf8fe4ece5d8a2a4b87f098b014fd9901475a1b))\n* **picker.format:** correcter max_width for truncpath ([a5d2964](https://github.com/folke/snacks.nvim/commit/a5d29646e593d52e952183021d5902e2a1ebc583))\n* **picker.format:** simplified resolvable formatters and more correct ([d5b6d30](https://github.com/folke/snacks.nvim/commit/d5b6d30b5e9acd3279406e0a3d382d37d657a28f))\n* **picker.git_diff:** use absolute path when adding buffer to avoid duplicates ([#1819](https://github.com/folke/snacks.nvim/issues/1819)) ([a012f39](https://github.com/folke/snacks.nvim/commit/a012f394c9988ec30ec03474d5b971e1796ee3dd))\n* **picker.git:** add `ignorecase` for `git_grep` ([#1629](https://github.com/folke/snacks.nvim/issues/1629)) ([7502e77](https://github.com/folke/snacks.nvim/commit/7502e7780363217ffcf3a998fd7d661ce37c1f01))\n* **picker.git:** use unmerged icon for unmerged. Fixes [#1531](https://github.com/folke/snacks.nvim/issues/1531) ([abee3c9](https://github.com/folke/snacks.nvim/commit/abee3c9eff1c77eb66151800b3683d4043527f88))\n* **picker.grep:** better line/col parsing. Closes [#2126](https://github.com/folke/snacks.nvim/issues/2126). Fixes [#2123](https://github.com/folke/snacks.nvim/issues/2123) ([1fee799](https://github.com/folke/snacks.nvim/commit/1fee799ad67d004c0cd8412a496bb20cb2f03e9b))\n* **picker.grep:** faulty rg cmd. Closes [#2280](https://github.com/folke/snacks.nvim/issues/2280) ([65a5c8b](https://github.com/folke/snacks.nvim/commit/65a5c8b3d05b0c08838aab9db8427b7f62342ef8))\n* **picker.list:** resize when needed. Closes [#2290](https://github.com/folke/snacks.nvim/issues/2290) ([df018ed](https://github.com/folke/snacks.nvim/commit/df018edfdbc5df832b46b9bdc9eafb1d69ea460b))\n* **picker.lsp_config:** cmd can be a function ([ba745ba](https://github.com/folke/snacks.nvim/commit/ba745ba281c02b12dc898de9e652a408c48b2bbe))\n* **picker.lsp:** don't process lsp request results when aborted. Closes [#2327](https://github.com/folke/snacks.nvim/issues/2327) ([4e10708](https://github.com/folke/snacks.nvim/commit/4e1070867a701b863d4bce4fd54a6c11314de506))\n* **picker.lsp:** move get_clients inside vim.schedule to prevent issues on Neovim 0.11. Closes [#2320](https://github.com/folke/snacks.nvim/issues/2320) ([79f3a8d](https://github.com/folke/snacks.nvim/commit/79f3a8d8b3c19bb27d1c34381d27d55a87a374dc))\n* **picker.lsp:** trigger docs workflow ([6f1158f](https://github.com/folke/snacks.nvim/commit/6f1158fe9bada1cb467defcdfb55f5217a90d709))\n* **picker.man:** make tab/split/vsplit work. Closes [#2171](https://github.com/folke/snacks.nvim/issues/2171) ([f39d114](https://github.com/folke/snacks.nvim/commit/f39d1144e707d59857dfa5d78162748d1e8cef4a))\n* **picker.marks:** fix buffer checking ([#2287](https://github.com/folke/snacks.nvim/issues/2287)) ([ca0858a](https://github.com/folke/snacks.nvim/commit/ca0858a30a88e8a28325c0c1edc0cd24b905c4e4))\n* **picker.preview:** better hack to deal with buffer local option weirdness ([c968d4d](https://github.com/folke/snacks.nvim/commit/c968d4def4ee3769e6523cd4d8599695b7183a3f))\n* **picker.preview:** directory preview should use cwd. Closes [#2212](https://github.com/folke/snacks.nvim/issues/2212). Fixes [#2093](https://github.com/folke/snacks.nvim/issues/2093) ([d050712](https://github.com/folke/snacks.nvim/commit/d05071255c865eb2cb3cd493235bf25ad6947513))\n* **picker.preview:** don't record previeww searches in history and prevent scrolling from the top. Closes [#2305](https://github.com/folke/snacks.nvim/issues/2305) ([080320b](https://github.com/folke/snacks.nvim/commit/080320bb820ffdb6103f993da076b100ea68333c))\n* **picker.preview:** dont do win-local hack for floating windows ([12b2f0d](https://github.com/folke/snacks.nvim/commit/12b2f0d2bdf18e50e8caa4e1ad3c6f6cc9365833))\n* **picker.qflist:** error with qflist picker when the list contains invalid items ([#2293](https://github.com/folke/snacks.nvim/issues/2293)) ([6af1e76](https://github.com/folke/snacks.nvim/commit/6af1e76758c6e9ad8792202de2ba069da2a93a68))\n* **picker.recent:** include closed / unlisted buffers in recent. Closes [#1745](https://github.com/folke/snacks.nvim/issues/1745) ([5959631](https://github.com/folke/snacks.nvim/commit/595963140e464e9bd8244b758a590a7c0b5d0798))\n* **picker:** add type field to qflist item ([#1538](https://github.com/folke/snacks.nvim/issues/1538)) ([#1539](https://github.com/folke/snacks.nvim/issues/1539)) ([125978b](https://github.com/folke/snacks.nvim/commit/125978b57ac484d7c3487bd97b672c9157ba1ff0))\n* **picker:** added show_delay to config max ms to wait to show if no results found yet. Closes [#2206](https://github.com/folke/snacks.nvim/issues/2206) ([64583a0](https://github.com/folke/snacks.nvim/commit/64583a0386e79b226bde4ce04432220836343e57))\n* **picker:** allow some sources to use the current window as main. Closes [#2012](https://github.com/folke/snacks.nvim/issues/2012). See [#1941](https://github.com/folke/snacks.nvim/issues/1941) ([5cda953](https://github.com/folke/snacks.nvim/commit/5cda9532ca215f7065dc12be1cc23458e3137c2a))\n* **picker:** correct z-index for preview=\"main\" layout ([e796aef](https://github.com/folke/snacks.nvim/commit/e796aef0fabc791cdb4a7ec6ecfc91b0eccce1d7))\n* **picker:** do not record consecutive duplicate history ([#2040](https://github.com/folke/snacks.nvim/issues/2040)) ([d0a5310](https://github.com/folke/snacks.nvim/commit/d0a53104172d48951e47e686c9e913ae5a6efb6f))\n* **picker:** fixup for pickers that dont display files ([1b4205e](https://github.com/folke/snacks.nvim/commit/1b4205eb1a224f668e85abeda2c0b1f0f73f477d))\n* **picker:** load correct actions in list of action names. Closes [#1501](https://github.com/folke/snacks.nvim/issues/1501) ([b064be2](https://github.com/folke/snacks.nvim/commit/b064be2882a0a081caa2464a57379d0ac58f4f0f))\n* **picker:** lsp_config now includes any configfured LSP and excludes deprecated servers ([a0d6eba](https://github.com/folke/snacks.nvim/commit/a0d6eba1a22719ffaed9b1ac2cf79e33b1c64e4c))\n* **picker:** prevent WinEnter handling during startup ([756a791](https://github.com/folke/snacks.nvim/commit/756a791131304a9063ff8e3af52811efbcaef688))\n* **picker:** show_delay config value ([67bb3a7](https://github.com/folke/snacks.nvim/commit/67bb3a7ba0478c892a4f06ac0446ca101af787c9))\n* **picker:** show_delay is in ms. Also increase it to allow auto_confirm to work properly ([924a930](https://github.com/folke/snacks.nvim/commit/924a9304e92bd29da4bd63a87cb0b05524254a87))\n* **picker:** use nvim_paste instead of nvim_put. Closes [#1941](https://github.com/folke/snacks.nvim/issues/1941) ([021e04f](https://github.com/folke/snacks.nvim/commit/021e04fa6fef78f52ac189592d176e7febdd09d4))\n* **projects:** normalize item.text for correct Windows support ([#2275](https://github.com/folke/snacks.nvim/issues/2275)) ([457596b](https://github.com/folke/snacks.nvim/commit/457596be6d53d4cd4b6bad8614dfde0c11549c41))\n* **rename:** made rename more robust and make sure target directory exists. Closes [#2252](https://github.com/folke/snacks.nvim/issues/2252) ([c494447](https://github.com/folke/snacks.nvim/commit/c494447737900d3ed56a51aa4e6f2aa118c2c518))\n* **scope:** allow user to disable keys ([#1918](https://github.com/folke/snacks.nvim/issues/1918)) ([bebf0bd](https://github.com/folke/snacks.nvim/commit/bebf0bd38e3e7071abc4085ad46f1ebc32cdfe17))\n* **scratch:** branch fallback for detached head ([#1519](https://github.com/folke/snacks.nvim/issues/1519)) ([98345c7](https://github.com/folke/snacks.nvim/commit/98345c70126147f871d90ab23787b0dc00937b84))\n* **scratch:** hide buffer after formatting when close ([#1523](https://github.com/folke/snacks.nvim/issues/1523)) ([4379085](https://github.com/folke/snacks.nvim/commit/43790856166685c4541397bd59b5927146be8f82))\n* **scratch:** use icon[1] when icon is a table to avoid table.concat error ([#2242](https://github.com/folke/snacks.nvim/issues/2242)) ([ba90011](https://github.com/folke/snacks.nvim/commit/ba90011a1481b9b9314a7cebbb319f7472ea25a8))\n* **scroll:** don't animate 1 line scrolls (jk). Closes [#1620](https://github.com/folke/snacks.nvim/issues/1620) ([d293b21](https://github.com/folke/snacks.nvim/commit/d293b21fe1a603dfb4757feb82ab3e67b78589f2))\n* **scroll:** stop anim and reset state when win has new buf, or buf was changed. Closes [#1820](https://github.com/folke/snacks.nvim/issues/1820). Closes [#2221](https://github.com/folke/snacks.nvim/issues/2221) ([766f7b8](https://github.com/folke/snacks.nvim/commit/766f7b87aa1d94f356a77755886150dc7c4c756e))\n* **statuscolumn:** show open folds in consecutive levels ([#1534](https://github.com/folke/snacks.nvim/issues/1534)) ([7bcd3ba](https://github.com/folke/snacks.nvim/commit/7bcd3baaf8e9fbea1c51e0690e67e7be69441311))\n* **terminal:** check win valid before creating a new terminal ([#1927](https://github.com/folke/snacks.nvim/issues/1927)) ([ba7bbcd](https://github.com/folke/snacks.nvim/commit/ba7bbcd0df45f1f4e390fc79dcaa974b4c4ea3c6))\n* **terminal:** make sure terminals opend with `open()` can be found with `list()`. Closes [#2172](https://github.com/folke/snacks.nvim/issues/2172). Closes [#2173](https://github.com/folke/snacks.nvim/issues/2173) ([13f3006](https://github.com/folke/snacks.nvim/commit/13f3006dbf69e0a1a3189775d9c70ee3456ed7e0))\n* **terminal:** set buffer when opening terminal with position='current' ([#2162](https://github.com/folke/snacks.nvim/issues/2162)) ([2aacf55](https://github.com/folke/snacks.nvim/commit/2aacf550820430594a74ccf995a11b0be9184eea)), closes [#2148](https://github.com/folke/snacks.nvim/issues/2148)\n* **terminal:** stack only terminal splits by default. Closes [#2137](https://github.com/folke/snacks.nvim/issues/2137) ([8c50196](https://github.com/folke/snacks.nvim/commit/8c501965beff9a741b29eea53c7f876b039bddea))\n* **util:** fix invalid window error ([#1996](https://github.com/folke/snacks.nvim/issues/1996)) ([32e5bf1](https://github.com/folke/snacks.nvim/commit/32e5bf17309ca26e6075a14c3907b0959188d781))\n* **util:** only use mini.icons if it has been setup. Closes [#2199](https://github.com/folke/snacks.nvim/issues/2199) ([774bf9d](https://github.com/folke/snacks.nvim/commit/774bf9d8c8d9bc401355f53605f91d469236f6c6))\n* **win:** check parent win is valid before getting size ([#2315](https://github.com/folke/snacks.nvim/issues/2315)) ([471eb03](https://github.com/folke/snacks.nvim/commit/471eb036c47abf9e71c33b9e01ebb1b2d464b791))\n* **zen:** make zoom and minimizing work in terminal mode ([#1912](https://github.com/folke/snacks.nvim/issues/1912)) ([fb54927](https://github.com/folke/snacks.nvim/commit/fb54927ab0d5b4ce3a377d9bcd7b172d0692725d))\n\n\n### Performance Improvements\n\n* **dashboard:** add basic OSC11 and CSI6n support to terminal sections (gh 10 seconds faster) ([fb016d2](https://github.com/folke/snacks.nvim/commit/fb016d20c2a415450708e3eb837462f6dcea46ba))\n* **git:** invoke `git status` with `--no-optional-locks` ([#2175](https://github.com/folke/snacks.nvim/issues/2175)) ([e441c64](https://github.com/folke/snacks.nvim/commit/e441c641eb3ff4ffce7535578399b7f6a7a0b2e1))\n* **grep:** move match parsing to resolve and fix an issue with `.*` results. Closes [#2308](https://github.com/folke/snacks.nvim/issues/2308) ([1417701](https://github.com/folke/snacks.nvim/commit/1417701af6e98ece624db386361b006d1e13d1fa))\n* **picker:** set `limit_live=10000` by default. Makes no sense to load millions of matches when doing live searches. ([04990d0](https://github.com/folke/snacks.nvim/commit/04990d042c7e95d1f2cea5bf26892cb4a5f024fd))\n\n## [2.23.0](https://github.com/folke/snacks.nvim/compare/v2.22.0...v2.23.0) (2025-09-15)\n\n\n### Features\n\n* **image.doc:** enable inline math rendering for neorg files. Closes [#1438](https://github.com/folke/snacks.nvim/issues/1438) ([ec487f5](https://github.com/folke/snacks.nvim/commit/ec487f5d85c12e4cee27d4e1235a1f82f99bfe03))\n* **image.inline:** honor `concealcursor` and hide conceal when selecting lines. Closes [#1478](https://github.com/folke/snacks.nvim/issues/1478) ([bc0630e](https://github.com/folke/snacks.nvim/commit/bc0630e43be5699bb94dadc302c0d21615421d93))\n* **image:** proper inline rendering of math expressions. Closes [#1318](https://github.com/folke/snacks.nvim/issues/1318). Closes [#1454](https://github.com/folke/snacks.nvim/issues/1454) ([6ea4fa7](https://github.com/folke/snacks.nvim/commit/6ea4fa72dccd6db0713c13e6672f9e8fb4007417))\n* **picker.git:** added `all` option to also list remote branched for `git_branches`. Closes [#1465](https://github.com/folke/snacks.nvim/issues/1465) ([3d695ab](https://github.com/folke/snacks.nvim/commit/3d695ab7d062d40c980ca5fd9fe6e593c8f35b12))\n* **scope:** allow disabling scopes with `vim.g.snacks_scope = false` or `vim.b.snacks_scope = false`. Closes [#1463](https://github.com/folke/snacks.nvim/issues/1463) ([5315e26](https://github.com/folke/snacks.nvim/commit/5315e267fffad9a257ab6909e87eb43e2f636049))\n* **util:** small ts parse wrapper that parses async when available ([9f0aa20](https://github.com/folke/snacks.nvim/commit/9f0aa2048945604d7f87bbc2594efa42c0f78c23))\n\n\n### Bug Fixes\n\n* **dashboard:** escape filenames for edit. Closes [#1453](https://github.com/folke/snacks.nvim/issues/1453) ([8b0e79a](https://github.com/folke/snacks.nvim/commit/8b0e79ab4cbb0bc5b22dd00a471d0b2bafb1c6f0))\n* **explorer:** confirm prompt now defaults to `No` ([f970cbb](https://github.com/folke/snacks.nvim/commit/f970cbb258d23942906be83b808d2ca2fcc24ab2))\n* **image.inline:** remove debug ([d9bb639](https://github.com/folke/snacks.nvim/commit/d9bb639feda0daf4e4df6eaa47989099b74dde46))\n* **image.latex:** don't nest image nodes ([714d761](https://github.com/folke/snacks.nvim/commit/714d7616f0b76ff7c099b5604b19a1a6ab909511))\n* **image.queries:** add image type ([1bbd479](https://github.com/folke/snacks.nvim/commit/1bbd47973df1ae2127576de8fcea720499c159ad))\n* **image:** only show anchor icon for inline images shown on the lines below. Closes [#1479](https://github.com/folke/snacks.nvim/issues/1479) ([bc2ed15](https://github.com/folke/snacks.nvim/commit/bc2ed15c41cb147f957db02959c9422b6a9b84ba))\n* **indent:** zero indent for blank lines. Closes [#1477](https://github.com/folke/snacks.nvim/issues/1477) ([cddf714](https://github.com/folke/snacks.nvim/commit/cddf714dd66a14b0cf556f9be82165b22517de1a))\n* **input:** add cr keybind for normal mode. Closes [#1468](https://github.com/folke/snacks.nvim/issues/1468). Closes [#1466](https://github.com/folke/snacks.nvim/issues/1466) ([78f0ad6](https://github.com/folke/snacks.nvim/commit/78f0ad6ce7283b0e2d6ac2b9b82ac731c7c30b93))\n* **lsp:** fix deprecated warnings related to lsp client ([07fefd2](https://github.com/folke/snacks.nvim/commit/07fefd2a99b2ae376f9704a8a3885c838cfc31c8))\n* **picker.preview:** always use builtin for git log preview ([f0d3433](https://github.com/folke/snacks.nvim/commit/f0d34336dbac2909654ca05aabc472edc73c7c8a))\n* **statuscolumn:** better way of determining open folds. Closes [#1445](https://github.com/folke/snacks.nvim/issues/1445) ([1239fb8](https://github.com/folke/snacks.nvim/commit/1239fb84bc426d4fcd1c8dc9dde8503c17501842))\n* **util.spawn:** correctly mark as faild on abort ([6917597](https://github.com/folke/snacks.nvim/commit/6917597f6d22d79fcd0bf9b0eb7845f7ffdc80a0))\n* **win:** make sure the border is set when setting the title ([76311ab](https://github.com/folke/snacks.nvim/commit/76311aba31182adcd85cc3381abca76b917668b7))\n\n\n### Performance Improvements\n\n* **image:** async treesitter parsing for images ([e55ae37](https://github.com/folke/snacks.nvim/commit/e55ae37bebd53ab0e24a47d88ef50267207ffd91))\n\n\n### Reverts\n\n* dont always set border when setting title. Closes [#1436](https://github.com/folke/snacks.nvim/issues/1436) ([fa29c6c](https://github.com/folke/snacks.nvim/commit/fa29c6c92631026a7ee41249c78bd91562e67a09))\n\n## [2.22.0](https://github.com/folke/snacks.nvim/compare/v2.21.0...v2.22.0) (2025-02-25)\n\n\n### Features\n\n* **image:** allow disabling math rendering. Closes [#1247](https://github.com/folke/snacks.nvim/issues/1247) ([1543a06](https://github.com/folke/snacks.nvim/commit/1543a063fbd3a462879d696b2885f4aa90c55896))\n* **image:** configurable templates for math expressions. Closes [#1338](https://github.com/folke/snacks.nvim/issues/1338) ([e039139](https://github.com/folke/snacks.nvim/commit/e039139291f85eebf3eeb41cc5ad9dc4265cafa4))\n* **image:** removed `org` integration, since that is now handled by the org mode plugin directly. ([956fe69](https://github.com/folke/snacks.nvim/commit/956fe69df328d2da924a04061802fb7d2ec5fef6))\n* **picker.input:** added some ctrl+r keymaps similar to cmdline. Closes [#1420](https://github.com/folke/snacks.nvim/issues/1420) ([c864a7d](https://github.com/folke/snacks.nvim/commit/c864a7d378da2a11afb09302b3220264e2aa3409))\n* **util:** util method to check if ts lang is available on any Neovim version. See [#1422](https://github.com/folke/snacks.nvim/issues/1422) ([e2cb9df](https://github.com/folke/snacks.nvim/commit/e2cb9df7d0695911f60a4510191aaf4a3d0d81ad))\n\n\n### Bug Fixes\n\n* **compat:** fixup ([ceabfc1](https://github.com/folke/snacks.nvim/commit/ceabfc1b89fe8e46b5138ae2417890121f5dfa02))\n* **compat:** properly detect async treesitter parsing ([842605f](https://github.com/folke/snacks.nvim/commit/842605f072e5d124a47eeb212bc2f78345bec4c4))\n* **compat:** vim.fs.normalize. Closes [#1321](https://github.com/folke/snacks.nvim/issues/1321) ([2295cfc](https://github.com/folke/snacks.nvim/commit/2295cfcca5bc749f169fb83ca4bdea9a85ad79a3))\n* **dim:** check that win is valid when animating dim. Closes [#1342](https://github.com/folke/snacks.nvim/issues/1342) ([47e1440](https://github.com/folke/snacks.nvim/commit/47e1440d547233772a3958580d429b38b5959edd))\n* **image.placement:** max width/height in cells is 297. Closes [#1345](https://github.com/folke/snacks.nvim/issues/1345) ([5fa93cb](https://github.com/folke/snacks.nvim/commit/5fa93cb6846b5998bc0b4b4ac9de47108fe39ce6))\n* **image.terminal:** reset queue when timer runs ([2b34c4d](https://github.com/folke/snacks.nvim/commit/2b34c4dc05aa4cbccc6171fa530e95c218e9bc9c))\n* **image.terminal:** write queued terminal output on main ([1b63b18](https://github.com/folke/snacks.nvim/commit/1b63b1811c58f661ad22f390a52aa6723703dc3d))\n* **picker.buffers:** add `a` flag when buffer is visible in a window. See [#1417](https://github.com/folke/snacks.nvim/issues/1417) ([91c3da0](https://github.com/folke/snacks.nvim/commit/91c3da0b4b286967d6d0166c0fc5769795a78918))\n* **picker.recent:** expand to full path before normalizing. Closes [#1406](https://github.com/folke/snacks.nvim/issues/1406) ([cf47fa7](https://github.com/folke/snacks.nvim/commit/cf47fa7cee80b0952706aacd4068310fe041761e))\n* **picker:** allow overriding winhl of layout box wins. Closes [#1424](https://github.com/folke/snacks.nvim/issues/1424) ([b0f983e](https://github.com/folke/snacks.nvim/commit/b0f983ef9aa9b9855ff0b72350cd3dc80de70675))\n* **picker:** disable regex for grep_word ([#1363](https://github.com/folke/snacks.nvim/issues/1363)) ([54298eb](https://github.com/folke/snacks.nvim/commit/54298eb624bd89f10f288b92560861277a34116d))\n* **picker:** remove unused keymaps for mouse scrolling ([33df54d](https://github.com/folke/snacks.nvim/commit/33df54dae71df7f7ec17551c23ad0ffc677e6ad1))\n* **picker:** update titles before showing. Closes [#1337](https://github.com/folke/snacks.nvim/issues/1337) ([3ae9863](https://github.com/folke/snacks.nvim/commit/3ae98636aaaf8f1b2f55b264f5745ae268de532f))\n* **scope:** use `rawequal` to check if scope impl is treesitter. Closes [#1413](https://github.com/folke/snacks.nvim/issues/1413) ([4ce197b](https://github.com/folke/snacks.nvim/commit/4ce197bff9cb9b78a0bdcebb6f7ebbf22cd48c6a))\n* **scroll:** compat with Neovim 0.9.4 ([4c52b7f](https://github.com/folke/snacks.nvim/commit/4c52b7f25da0ce6b2b830ce060dbd162706acf33))\n* **statuscolumn:** right-align the current line number when relativenumber=true. Closes [#1376](https://github.com/folke/snacks.nvim/issues/1376) ([dd15e3a](https://github.com/folke/snacks.nvim/commit/dd15e3a05a2111231c53726f18e39a147162c20f))\n* **win:** don't update title is relative win is invalid. Closes [#1348](https://github.com/folke/snacks.nvim/issues/1348) ([a00c323](https://github.com/folke/snacks.nvim/commit/a00c323d4b244f781df6df8b11bbfa47f63202d4))\n* **win:** use correct keys for displaying help. Closes [#1364](https://github.com/folke/snacks.nvim/issues/1364) ([b100c93](https://github.com/folke/snacks.nvim/commit/b100c937177536cf2aa634ddd2aa5b8a1dd23ace))\n* **zen:** always count cmdheight towards Zen bottom offset ([#1402](https://github.com/folke/snacks.nvim/issues/1402)) ([041bf1d](https://github.com/folke/snacks.nvim/commit/041bf1da9ed12498cbe3273dd90ef83e0a4913fa))\n\n\n### Performance Improvements\n\n* **scope:** use async treesitter parsing when available ([e0f882e](https://github.com/folke/snacks.nvim/commit/e0f882e6d6464666319502151cc244a090d4377f))\n\n## 2.21.0 (2025-02-20)\n\n\n### Features\n\n* added new `image` snacks plugin for the kitty graphics protocol ([4e4e630](https://github.com/folke/snacks.nvim/commit/4e4e63048e5ddae6f921f1a1b4bd11a53016c7aa))\n* **bigfile:** configurable average line length (default = 1000). Useful for minified files. Closes [#576](https://github.com/folke/snacks.nvim/issues/576). Closes [#372](https://github.com/folke/snacks.nvim/issues/372) ([7fa92a2](https://github.com/folke/snacks.nvim/commit/7fa92a24501fa85b567c130b4e026f9ca1efed17))\n* **compat:** added `svim`, a compatibility layer for Neovim. Closes [#1321](https://github.com/folke/snacks.nvim/issues/1321) ([bc902f7](https://github.com/folke/snacks.nvim/commit/bc902f7032df305df7dc48104cfa4e37967b3bdf))\n* **debug:** graduate proc debug to Snacks.debug.cmd ([eced303](https://github.com/folke/snacks.nvim/commit/eced3033ea29bf9154a5f2c5207bf9fc97368599))\n* **explorer:** `opts.include` and `opts.exclude`. Closes [#1068](https://github.com/folke/snacks.nvim/issues/1068) ([ab1889c](https://github.com/folke/snacks.nvim/commit/ab1889c35b1845f487f31f0399ec0c8bd2c6e521))\n* **explorer:** added `Snacks.explorer.reveal()` to reveal the current file in the tree. ([b4cf6bb](https://github.com/folke/snacks.nvim/commit/b4cf6bb48d882a873a6954bff2802d88e8e19e0d))\n* **explorer:** added copy/paste (yank/paste) for files. Closes [#1195](https://github.com/folke/snacks.nvim/issues/1195) ([938aee4](https://github.com/folke/snacks.nvim/commit/938aee4a02119ad693a67c38b64a9b3232a72565))\n* **explorer:** added ctrl+f to grep in the item's directory ([0454b21](https://github.com/folke/snacks.nvim/commit/0454b21165cb84d2f59a1daf6226de065c90d4f7))\n* **explorer:** added ctrl+t to open a terminal in the item's directory ([81f9006](https://github.com/folke/snacks.nvim/commit/81f90062c50430c1bad9546fcb65c3e43a76be9b))\n* **explorer:** added diagnostics file/directory status ([7f1b60d](https://github.com/folke/snacks.nvim/commit/7f1b60d5576345af5e7b990f3a9e4bca49cd3686))\n* **explorer:** added quick nav with `[`, `]` with `d/w/e` for diagnostics ([d1d5585](https://github.com/folke/snacks.nvim/commit/d1d55850ecb4aac1396c314a159db1e90a34bd79))\n* **explorer:** added support for live search ([82c4a50](https://github.com/folke/snacks.nvim/commit/82c4a50985c9bb9f4b1d598f10a30e1122a35212))\n* **explorer:** allow disabling untracked git status. Closes [#983](https://github.com/folke/snacks.nvim/issues/983) ([a3b083b](https://github.com/folke/snacks.nvim/commit/a3b083b8443b1ae1299747fdac8da51c3160835b))\n* **explorer:** deal with existing buffers when renaming / deleting files. Closes [#1315](https://github.com/folke/snacks.nvim/issues/1315) ([6614a2c](https://github.com/folke/snacks.nvim/commit/6614a2c84f1ad8528aa03caeb2574b274ee0c20b))\n* **explorer:** different hl group for broken links ([1989921](https://github.com/folke/snacks.nvim/commit/1989921466e6b5234ae8f71add41b8defd55f732))\n* **explorer:** disable fuzzy searches by default for explorer since it's too noisy and we can't sort on score due to tree view ([b07788f](https://github.com/folke/snacks.nvim/commit/b07788f14a28daa8d0b387c1258f8f348f47420f))\n* **explorer:** file watching that works on all platforms ([8399465](https://github.com/folke/snacks.nvim/commit/8399465872c51fab54ad5d02eb315e258ec96ed1))\n* **explorer:** focus on first file when searching in the explorer ([1d4bea4](https://github.com/folke/snacks.nvim/commit/1d4bea4a9ee8a5258c6ae085ac66dd5cc05a9749))\n* **explorer:** git index watcher ([4c12475](https://github.com/folke/snacks.nvim/commit/4c12475e80528d8d48b9584d78d645e4a51c3298))\n* **explorer:** show symlink target ([dfa79e0](https://github.com/folke/snacks.nvim/commit/dfa79e04436ebfdc83ba71c0048fc1636b4de5aa))\n* **git_log:** add author filter ([#1091](https://github.com/folke/snacks.nvim/issues/1091)) ([8c11661](https://github.com/folke/snacks.nvim/commit/8c1166165b17376ed87f0dedfc480c7cb8e42b7c))\n* **gitbrowse:** add support for git.sr.ht ([#1297](https://github.com/folke/snacks.nvim/issues/1297)) ([a3b47e5](https://github.com/folke/snacks.nvim/commit/a3b47e5202d924e6a6d4386bb5f94cc5857c4f8c))\n* **gitbrowse:** open permalinks to files. Fixes [#320](https://github.com/folke/snacks.nvim/issues/320) ([#438](https://github.com/folke/snacks.nvim/issues/438)) ([2a06e4c](https://github.com/folke/snacks.nvim/commit/2a06e4ce9957dea555d38b4f52024ea9e2902d8e))\n* **image.doc:** allow configuring the header for latex / typst inline in the document. Closes [#1303](https://github.com/folke/snacks.nvim/issues/1303) ([bde3add](https://github.com/folke/snacks.nvim/commit/bde3adddc7d787c5e93eb13af55b6e702d86418b))\n* **image.doc:** allow setting `image.src` with `#set!`. Closes [#1276](https://github.com/folke/snacks.nvim/issues/1276) ([65f89e2](https://github.com/folke/snacks.nvim/commit/65f89e2d6f3790b0687f09ebe2811d953bd09e0c))\n* **image.doc:** check for `image.ignore` in ts meta. See [#1276](https://github.com/folke/snacks.nvim/issues/1276) ([29c777a](https://github.com/folke/snacks.nvim/commit/29c777a0a0291a0caba17f2e9aeb86b6097fc83c))\n* **image:** `conceal` option for inline rendering (disabled by default) ([684666f](https://github.com/folke/snacks.nvim/commit/684666f6432eae139b8ca6813b1a88679f8febc1))\n* **image:** `Snacks.image.hover()` ([5f466be](https://github.com/folke/snacks.nvim/commit/5f466becd96ebcd0a52352f2d53206e0e86de35a))\n* **image:** add support for `svelte` ([#1277](https://github.com/folke/snacks.nvim/issues/1277)) ([54ab77c](https://github.com/folke/snacks.nvim/commit/54ab77c5d2b2edefa29fc63de73c7b2b60d2651b))\n* **image:** adde support for `Image` in jsx ([95878ad](https://github.com/folke/snacks.nvim/commit/95878ad32aaf310f465a004ef12e9edddf939287))\n* **image:** added `opts.img_dirs` to configure the search path for resolving images. Closes [#1222](https://github.com/folke/snacks.nvim/issues/1222) ([ad0b88d](https://github.com/folke/snacks.nvim/commit/ad0b88dc0814dc760c7b6ed4efc7c2fa8d27ba76))\n* **image:** added `Snacks.image.doc.at_cursor()`. See [#1108](https://github.com/folke/snacks.nvim/issues/1108) ([6348ccf](https://github.com/folke/snacks.nvim/commit/6348ccf1209739552f70800910c206637f3b2d2c))\n* **image:** added fallback image rendering for wezterm. Closes [#1063](https://github.com/folke/snacks.nvim/issues/1063) ([9e6b1a6](https://github.com/folke/snacks.nvim/commit/9e6b1a62a87aa201dea13a755f6ac1ed680a20d1))\n* **image:** added math rendering for typst. Closes [#1260](https://github.com/folke/snacks.nvim/issues/1260) ([e225823](https://github.com/folke/snacks.nvim/commit/e2258236a2fc770a87a81125232ca786ec7a1cf1))\n* **image:** added proper support for tmux ([b1a3b66](https://github.com/folke/snacks.nvim/commit/b1a3b66fade926e9d211453275ddf1be19a847a5))\n* **image:** added support for `.image` tags in neorg ([59bbe8d](https://github.com/folke/snacks.nvim/commit/59bbe8d90e91d4b4f63cc5fcb36c81bd8eeee850))\n* **image:** added support for `typst`. Closes [#1235](https://github.com/folke/snacks.nvim/issues/1235) ([507c183](https://github.com/folke/snacks.nvim/commit/507c1836e3c5cfc5194bb6350ece1a1e0a1edf14))\n* **image:** added support for a bunch of aditional languages ([a596f8a](https://github.com/folke/snacks.nvim/commit/a596f8a9ea0a058490bca8aca70f935cded18d22))\n* **image:** added support for angle bracket urls. Closes [#1209](https://github.com/folke/snacks.nvim/issues/1209) ([14a1f32](https://github.com/folke/snacks.nvim/commit/14a1f32eafd50b5b6ae742052e6da04b1e0167b2))\n* **image:** added support for math expressions in latex and markdown doc + images in latex. Closes [#1223](https://github.com/folke/snacks.nvim/issues/1223) ([1bca71a](https://github.com/folke/snacks.nvim/commit/1bca71a1332e3119ece0e62d668b75e6c98d948c))\n* **image:** added support for mermaid diagrams in markdown ([f8e7942](https://github.com/folke/snacks.nvim/commit/f8e7942d6c83a1b1953320054102eb32bf536d98))\n* **image:** added support for remote image viewing. Closes [#1156](https://github.com/folke/snacks.nvim/issues/1156) ([#1165](https://github.com/folke/snacks.nvim/issues/1165)) ([a5748ea](https://github.com/folke/snacks.nvim/commit/a5748ea8db2ac14fbc9c05376cc6d154d749f881))\n* **image:** added support for tsx, jsx, vue and angular ([ab0ba5c](https://github.com/folke/snacks.nvim/commit/ab0ba5cb22d7bf62fa204f08426e601a20750f29))\n* **image:** added support for wikilink style images. Closes [#1210](https://github.com/folke/snacks.nvim/issues/1210) ([3fda272](https://github.com/folke/snacks.nvim/commit/3fda27200d9af7ed181e9ee0a841c50137e9a5be))\n* **image:** allow customizing font size for math expressions ([b052eb9](https://github.com/folke/snacks.nvim/commit/b052eb93728df6cc0c09b7ee42fec6d93477fc3e))\n* **image:** allow customizing the default magick args for vector images ([2096fcd](https://github.com/folke/snacks.nvim/commit/2096fcdd739500ba8275d791b20d60f306c61b33))\n* **image:** allow forcing image rendering even when the terminal support detection fails ([d17a6e4](https://github.com/folke/snacks.nvim/commit/d17a6e4af888c43ba3faddc30231aa2aebc699d4))\n* **image:** apply image window options ([73366fa](https://github.com/folke/snacks.nvim/commit/73366fa17018d7fd4d115cec2466b2d8e7233341))\n* **image:** better detection of image capabilities of the terminal/mux environment ([1795d4b](https://github.com/folke/snacks.nvim/commit/1795d4b1ec767886300faa4965539fe67318a06a))\n* **image:** better error handling + option to disable error notifications ([1adfd29](https://github.com/folke/snacks.nvim/commit/1adfd29af3d1b4db2ba46f7a292410a2f9105fd6))\n* **image:** better health checks ([d389c5d](https://github.com/folke/snacks.nvim/commit/d389c5df14d83b6aff9eb6734906888780e8ca71))\n* **image:** check for `magick` in health check ([1284835](https://github.com/folke/snacks.nvim/commit/12848356c4fd672476f47d9dea9999784c140c05))\n* **image:** custom `src` resolve function ([af21ea3](https://github.com/folke/snacks.nvim/commit/af21ea3ccf6c11246cfbb1bef061caa4f387f1f0))\n* **image:** enabled pdf previews ([39bf513](https://github.com/folke/snacks.nvim/commit/39bf5131c4f8cd79c1779a5cb80e526cf9e4fffe))\n* **image:** floats in markdown. Closes [#1151](https://github.com/folke/snacks.nvim/issues/1151) ([4e10e31](https://github.com/folke/snacks.nvim/commit/4e10e31398e6921ac19371099f06640b8753bc8a))\n* **image:** health checks ([0d5b106](https://github.com/folke/snacks.nvim/commit/0d5b106d4eae756cd612fdabde36aa795a444546))\n* **image:** images are now properly scaled based on device DPI and image DPI. Closing [#1257](https://github.com/folke/snacks.nvim/issues/1257) ([004050c](https://github.com/folke/snacks.nvim/commit/004050c43533ac38a224649268e913c6fb0c4caa))\n* **image:** make manual hover work correctly ([942cb92](https://github.com/folke/snacks.nvim/commit/942cb9291e096d8604d515499e295ec67578b71a))\n* **image:** make math packages configurable. Closes [#1295](https://github.com/folke/snacks.nvim/issues/1295) ([e27ba72](https://github.com/folke/snacks.nvim/commit/e27ba726b15e71eca700141c2030ac858bc8025c))\n* **image:** markdown inline image preview. `opts.image` must be enabled and terminal needs support ([001f300](https://github.com/folke/snacks.nvim/commit/001f3002cabb9e23d8f1b23e0567db2d41c098a6))\n* **image:** refactor + css/html + beter image fitting ([e35d6cd](https://github.com/folke/snacks.nvim/commit/e35d6cd4ba87e8ff71d6ebe52b7be53408e13538))\n* **image:** refactor of treesitter queries to support inline image data ([0bf0c62](https://github.com/folke/snacks.nvim/commit/0bf0c6223d71ced4e5dc7ab7357b0a36a91a0a67))\n* **images:** added support for org-mode. Closes [#1276](https://github.com/folke/snacks.nvim/issues/1276) ([10387af](https://github.com/folke/snacks.nvim/commit/10387af009e51678788506527b46240b2139fd7f))\n* **image:** show progress indicator when converting image files ([b65178b](https://github.com/folke/snacks.nvim/commit/b65178b470385f0a81256d54c9d80f153cd14efd))\n* **image:** try resolving paths relative to the document and to the cwd. See [#1203](https://github.com/folke/snacks.nvim/issues/1203) ([668cbbb](https://github.com/folke/snacks.nvim/commit/668cbbba473d757144e24188b458e57dc0e98943))\n* **image:** url_decode strings ([d41704f](https://github.com/folke/snacks.nvim/commit/d41704f3daae823513c90adb913976bfabc36387))\n* **image:** use `tectonic` when available ([8d073cc](https://github.com/folke/snacks.nvim/commit/8d073ccc0ca984f844cc2a8f8506f23f3fcea56a))\n* **image:** use kitty's unicode placeholder images ([7d655fe](https://github.com/folke/snacks.nvim/commit/7d655fe09d2c705ff5707902f4ed925a62a61d3b))\n* **image:** use search dirs to resolve file from both cwd and dirname of file. Closes [#1305](https://github.com/folke/snacks.nvim/issues/1305) ([bf01460](https://github.com/folke/snacks.nvim/commit/bf01460e6d82b720fb50664d372c1daf5ade249d))\n* **image:** utility function to get a png dimensions from the file header ([a6d866a](https://github.com/folke/snacks.nvim/commit/a6d866ab72e5cad7840d69a7354cc67e2699f46e))\n* **matcher:** call on_match after setting score ([23ce529](https://github.com/folke/snacks.nvim/commit/23ce529fb663337f9dc17ca08aa601b172469031))\n* **picker.actions:** `cmd` action now always allows to edit the command. Closes [#1033](https://github.com/folke/snacks.nvim/issues/1033) ([a177885](https://github.com/folke/snacks.nvim/commit/a17788539a5e66784535d0c973bdc08728f16c46))\n* **picker.actions:** option to disable notify for yank action. Closes [#1117](https://github.com/folke/snacks.nvim/issues/1117) ([f6a807d](https://github.com/folke/snacks.nvim/commit/f6a807da6d4e6ab591f85592a472bbb5bc6583f7))\n* **picker.config:** better source field spec ([6c58b67](https://github.com/folke/snacks.nvim/commit/6c58b67890bbd2076a7f5b69f57ab666cb9b7410))\n* **picker.db:** allow configuring the sqlite3 lib path. Closes [#1025](https://github.com/folke/snacks.nvim/issues/1025) ([b990044](https://github.com/folke/snacks.nvim/commit/b9900444d2ea494bba8857e5224059002ee8c465))\n* **picker.files:** added `ft` option to filter by extension(s) ([12a7ea2](https://github.com/folke/snacks.nvim/commit/12a7ea28b97827575a1768d6013dd3c7bedd5ebb))\n* **picker.format:** `opts.formatters.file.use_git_status_hl` defaults to `true` and adds git status hl to filename ([243eeca](https://github.com/folke/snacks.nvim/commit/243eecaca5f465602a9ba68e5c0fa375b90a13fb))\n* **picker.git_diff:** use the `diff` previewer for `git_diff` so that `delta` can be used. See [#1302](https://github.com/folke/snacks.nvim/issues/1302) ([92786c5](https://github.com/folke/snacks.nvim/commit/92786c5b03ae4772521050acabcb619283eeb94a))\n* **picker.git:** add confirmation before deleting a git branch ([#951](https://github.com/folke/snacks.nvim/issues/951)) ([337a3ae](https://github.com/folke/snacks.nvim/commit/337a3ae7eebb95020596f15a349a85d2f6be31a4))\n* **picker.git:** add create and delete branch to git_branches ([#909](https://github.com/folke/snacks.nvim/issues/909)) ([8676c40](https://github.com/folke/snacks.nvim/commit/8676c409e148e28eff93c114aca0c1bf3d42281a))\n* **picker.git:** allow passing extra args to git grep. Closes [#1184](https://github.com/folke/snacks.nvim/issues/1184) ([7122a03](https://github.com/folke/snacks.nvim/commit/7122a03fdf0b7bb9a5c6645b0e86f9e3a9f9290b))\n* **picker.git:** allow passing extra args to other git pickers ([#1205](https://github.com/folke/snacks.nvim/issues/1205)) ([4d46574](https://github.com/folke/snacks.nvim/commit/4d46574b247d72bf1a602cdda2ddd8da39854234))\n* **picker.lazy:** don't use `grep`. Parse spec files manually. Closes [#972](https://github.com/folke/snacks.nvim/issues/972) ([0928007](https://github.com/folke/snacks.nvim/commit/09280078e8339f018be5249fe0e1d7b9d32db7f7))\n* **picker.lsp:** added original symbol to item.item. Closes [#1171](https://github.com/folke/snacks.nvim/issues/1171) ([45a6f8d](https://github.com/folke/snacks.nvim/commit/45a6f8d1ee0c323246413d1e1d43b0f0c9da18a2))\n* **picker.lsp:** use existing buffers for preview when opened ([d4e6353](https://github.com/folke/snacks.nvim/commit/d4e63531c9fba63ded6fb470a5d53c98af110478))\n* **picker.preview:** allow confguring `preview = {main = true, enabled = false}` ([1839c65](https://github.com/folke/snacks.nvim/commit/1839c65f6784bedb7ae96a84ee741fa5c0023226))\n* **picker.preview:** allow passing additional args to the git preview command ([910437f](https://github.com/folke/snacks.nvim/commit/910437f1451ccaaa495aa1eca99e0a73fc798d40))\n* **picker.proc:** added proc debug mode ([d870f16](https://github.com/folke/snacks.nvim/commit/d870f164534d1853fd8c599d7933cc5324272a09))\n* **picker.undo:** `ctrl+y` to yank added lines, `ctrl+shift+y` to yank deleted lines ([3baf95d](https://github.com/folke/snacks.nvim/commit/3baf95d3a1005105b57ce53644ff6224ee3afa1c))\n* **picker.undo:** added ctrl+y to yank added lines from undo ([811a24c](https://github.com/folke/snacks.nvim/commit/811a24cc16a8e9b7ec947c95b73e1fe05e4692d1))\n* **picker.util:** lua globber ([97dcd9c](https://github.com/folke/snacks.nvim/commit/97dcd9c168c667538a4c6cc1384c4981a37afcad))\n* **picker.util:** utility function to get all bins on the PATH ([5d42c7e](https://github.com/folke/snacks.nvim/commit/5d42c7e5e480bde04fd9506b3df64b579446c4f9))\n* **picker:** `opts.focus` can be used to set default focus window. `opts.enter` if picker should be focused on enter.  Closes [#1162](https://github.com/folke/snacks.nvim/issues/1162) ([e8de28b](https://github.com/folke/snacks.nvim/commit/e8de28b56ec85ad45cdb3c303c5ee5da0e070baf))\n* **picker:** add LSP symbol range to result item ([#1123](https://github.com/folke/snacks.nvim/issues/1123)) ([c0481ab](https://github.com/folke/snacks.nvim/commit/c0481ab0b69c6111bfc5077bd1550acbb480f05d))\n* **picker:** added `c-q` to list ([6d0d2dc](https://github.com/folke/snacks.nvim/commit/6d0d2dc2a7e07de9704a172bd5295f4920eb965f))\n* **picker:** added `git_grep` picker. Closes [#986](https://github.com/folke/snacks.nvim/issues/986) ([2dc9016](https://github.com/folke/snacks.nvim/commit/2dc901634b250059cc9b7129bdeeedd24520b86c))\n* **picker:** added `lsp_config` source ([0d4aa98](https://github.com/folke/snacks.nvim/commit/0d4aa98cea0de6144853d820e52e6e35d0f0c609))\n* **picker:** added treesitter symbols picker ([a6beb0f](https://github.com/folke/snacks.nvim/commit/a6beb0f280d3f43513998882faf199acf3818ddf))\n* **picker:** allow complex titles ([#1112](https://github.com/folke/snacks.nvim/issues/1112)) ([f200b3f](https://github.com/folke/snacks.nvim/commit/f200b3f6c8f84147e1a80f70b8f1714645c59af6))\n* **picker:** allow configuring file icon width. Closes [#981](https://github.com/folke/snacks.nvim/issues/981) ([52c1086](https://github.com/folke/snacks.nvim/commit/52c1086ecdf410dfec3317144d46de7c6f86c1ad))\n* **picker:** allow overriding default file/dir/dir_open icons. Closes [#1199](https://github.com/folke/snacks.nvim/issues/1199) ([41c4391](https://github.com/folke/snacks.nvim/commit/41c4391b72ff5ef3dfa8216fc608bbe02bbd4d1c))\n* **picker:** default `c-t` keymap to open in tab ([ffc6fe3](https://github.com/folke/snacks.nvim/commit/ffc6fe3965cb176c2b3e2bdb0aee4478e4dc2b94))\n* **picker:** each window can now be `toggled` (also input), `hidden` and have `auto_hide` ([01efab2](https://github.com/folke/snacks.nvim/commit/01efab2ddb75d2077229231201c5a69ab2df3ad8))\n* **picker:** get filetype from modeline when needed. Closes [#987](https://github.com/folke/snacks.nvim/issues/987) ([5af04ab](https://github.com/folke/snacks.nvim/commit/5af04ab6672ae38bf7d72427e75f925615f93904))\n* **picker:** image previewer using kitty graphics protocol ([2b0aa93](https://github.com/folke/snacks.nvim/commit/2b0aa93efc9aa662e0cb9446cc4639f3be1a9d1e))\n* **picker:** new native diff mode (disabled by default). Can be used to show delta diffs for undo. Closes [#1288](https://github.com/folke/snacks.nvim/issues/1288) ([d6a38ac](https://github.com/folke/snacks.nvim/commit/d6a38acbf5765eeb5ca2558bcb0d1ae1428dd2ca))\n* **picker:** pin picker as a split to left/bottom/top/right with `ctrl+z+(hjkl)` ([27cba53](https://github.com/folke/snacks.nvim/commit/27cba535a6763cbca3f3162c5c4bb48c6f382005))\n* **picker:** renamed `native` -&gt; `builtin` + fixed diff mode used for undo. Closes [#1302](https://github.com/folke/snacks.nvim/issues/1302) ([bd6a62a](https://github.com/folke/snacks.nvim/commit/bd6a62af12ca5e8cab88b94912e65bff26c9feba))\n* **scope:** allow injected languages to be parsed by treesitter ([#823](https://github.com/folke/snacks.nvim/issues/823)) ([aba21dd](https://github.com/folke/snacks.nvim/commit/aba21ddc712b12db8469680dd7f2080063cb6d5c))\n* **scroll:** big rework to make scroll play nice with virtual lines ([e71955a](https://github.com/folke/snacks.nvim/commit/e71955a941300cd81bf6d7ab36d1352b62d6f568))\n* **scroll:** scroll improvements. Closes [#1024](https://github.com/folke/snacks.nvim/issues/1024) ([73d2f0f](https://github.com/folke/snacks.nvim/commit/73d2f0f40c702acaf7a1a3e833fc5460cb552578))\n* **statuscolumn:** added mouse click handler to open/close folds. Closes [#968](https://github.com/folke/snacks.nvim/issues/968) ([98a7b64](https://github.com/folke/snacks.nvim/commit/98a7b647c9e245ef02d57d566bf8461c2f7beb56))\n* **terminal:** added `Snacks.terminal.list()`. Closes [#421](https://github.com/folke/snacks.nvim/issues/421). Closes [#759](https://github.com/folke/snacks.nvim/issues/759) ([73c4b62](https://github.com/folke/snacks.nvim/commit/73c4b628963004760ccced0192d1c2633c9e3657))\n* **terminal:** added `start_insert` ([64129e4](https://github.com/folke/snacks.nvim/commit/64129e4c3c5b247c61b1f46bc0faaa1e69e7eef8))\n* **terminal:** auto_close and auto_insert. Closes [#965](https://github.com/folke/snacks.nvim/issues/965) ([bb76cae](https://github.com/folke/snacks.nvim/commit/bb76cae87e81a871435570b91c8c6f6e27eb9955))\n* **terminal:** don't use deprecated `vim.fn.termopen` on Neovim &gt;= 0.10 ([37f6665](https://github.com/folke/snacks.nvim/commit/37f6665c488d90bf50b99cfe0b0fab40f990c497))\n* test ([520ed85](https://github.com/folke/snacks.nvim/commit/520ed85169c873a8492077520ff37a5f0233c67d))\n* **toggle:** allow user to add custom which-key description ([#1121](https://github.com/folke/snacks.nvim/issues/1121)) ([369732e](https://github.com/folke/snacks.nvim/commit/369732e65e0077e51487547131045526ccbdad1b))\n* **treesitter:** add `tree` boolean to toggle on/off tree symbols ([#1105](https://github.com/folke/snacks.nvim/issues/1105)) ([c61f9eb](https://github.com/folke/snacks.nvim/commit/c61f9eb28695b9f96682d6dbf67072947dfa2737))\n* **util:** `Snacks.util.winhl` helper to deal with `vim.wo.winhighlight` ([4c1d7b4](https://github.com/folke/snacks.nvim/commit/4c1d7b4720218122885877877e7883cc491133ed))\n* **util:** base64 shim for Neovim &lt; 0.10 ([96f1227](https://github.com/folke/snacks.nvim/commit/96f12274a49bb2e6d0d558e652c728d27d4c3ff8))\n* **util:** Snacks.util.color can now get the color from a list of hl groups ([a33f65d](https://github.com/folke/snacks.nvim/commit/a33f65d936a85efa9aaee9e44bcd70069134a816))\n* **util:** util.spawn ([a76fe13](https://github.com/folke/snacks.nvim/commit/a76fe13148a899274484972a8705052bef4baa93))\n* **words:** add `filter` function for user to disable specific filetypes ([#1296](https://github.com/folke/snacks.nvim/issues/1296)) ([d62e752](https://github.com/folke/snacks.nvim/commit/d62e7527a5e9608ab0033bc63a329baf8757ea6d))\n\n\n### Bug Fixes\n\n* **all:** better support for opening windows / pickers / ... on multiple tab pages. Closes [#1043](https://github.com/folke/snacks.nvim/issues/1043) ([8272c1c](https://github.com/folke/snacks.nvim/commit/8272c1c66f43390294debb24759c32627653aedb))\n* **bigfile:** check that passed path is the one from the buffer ([8deea64](https://github.com/folke/snacks.nvim/commit/8deea64dba3b9b8f57e52bb6b0133263f6ff171f))\n* **buffers:** use `\"` mark for full buffer position when set. Closes [#1160](https://github.com/folke/snacks.nvim/issues/1160) ([7d350bc](https://github.com/folke/snacks.nvim/commit/7d350bc0c7e897ad0d7bd7fc9a470dabecab32ca))\n* **compat:** correct Neovim 0.11 check ([448a55a](https://github.com/folke/snacks.nvim/commit/448a55a0e3c437bacc945c4ea98a6342ccb2b769))\n* **dashboard:** allow dashboard to be the main editor window ([e3ead3c](https://github.com/folke/snacks.nvim/commit/e3ead3c648b3b6c8af0557c6412ae0307cc92018))\n* **dashboard:** dashboard can be a main editor window ([f36c70a](https://github.com/folke/snacks.nvim/commit/f36c70a912c2893b10336b4645d3447264813a34))\n* **dashboard:** use `Snacks.util.icon` for icons. Closes [#1192](https://github.com/folke/snacks.nvim/issues/1192) ([c2f06da](https://github.com/folke/snacks.nvim/commit/c2f06daeca6c3304d1e94225323dbe8c2f7f797d))\n* **debug:** better args handling for debugging cmds ([48a3fed](https://github.com/folke/snacks.nvim/commit/48a3fed3c51390650d134bc5d76d15ace8d614ea))\n* **explorer.git:** always at `.git` directory to ignored ([f7a35b8](https://github.com/folke/snacks.nvim/commit/f7a35b8214f393e2412adc0c8f2fe85d956c4b02))\n* **explorer.git:** better git status watching ([09349ec](https://github.com/folke/snacks.nvim/commit/09349ecd44040666db9d4835994a378a9ff53e8c))\n* **explorer.git:** dont reset cursor when git status is done updating ([bc87992](https://github.com/folke/snacks.nvim/commit/bc87992e712c29ef8e826f3550f9b8e3f1a9308d))\n* **explorer.git:** vim.schedule git updates ([3aad761](https://github.com/folke/snacks.nvim/commit/3aad7616209951320d54f83dd7df35d5578ea61f))\n* **explorer.tree:** fix linux ([6f5399b](https://github.com/folke/snacks.nvim/commit/6f5399b47c55f916fcc3a82dcc71cce0eb5d7c92))\n* **explorer.tree:** symlink directories ([e5f1e91](https://github.com/folke/snacks.nvim/commit/e5f1e91249b468ff3a7d14a8650074c27f1fdb30))\n* **explorer.watch:** pcall watcher, since it can give errors on windows ([af96818](https://github.com/folke/snacks.nvim/commit/af968181af6ce6a988765fe51558b2caefdcf863))\n* **explorer:** always refresh state when opening the picker since changes might have happened that were not monitored ([c61114f](https://github.com/folke/snacks.nvim/commit/c61114fb32910863a543a4a7a1f63e9915983d26))\n* **explorer:** call original `on_close`. Closes [#971](https://github.com/folke/snacks.nvim/issues/971) ([a0bee9f](https://github.com/folke/snacks.nvim/commit/a0bee9f662d4e22c6533e6544b4daedecd2aacc0))\n* **explorer:** change grep in item dir keymap to leader-/. Closes [#1000](https://github.com/folke/snacks.nvim/issues/1000) ([9dfa276](https://github.com/folke/snacks.nvim/commit/9dfa276ea424a091f5d5bdc008aff127850441b2))\n* **explorer:** check that picker is still open ([50fa1be](https://github.com/folke/snacks.nvim/commit/50fa1be38ee8366d79e1fa58b38abf31d3955033))\n* **explorer:** disable follow for explorer search by default. No longer needed. Link directories may show as files then though, but that's not an issue. See [#960](https://github.com/folke/snacks.nvim/issues/960) ([b9a17d8](https://github.com/folke/snacks.nvim/commit/b9a17d82a726dc6cfd9a0b4f8566178708073808))\n* **explorer:** dont focus first file when not searching ([3fd437c](https://github.com/folke/snacks.nvim/commit/3fd437ccd38d79b876154097149d130cdb01e653))\n* **explorer:** dont process git when picker closed ([c255d9c](https://github.com/folke/snacks.nvim/commit/c255d9c6a02f070f0048c5eaa40921f71e9f2acb))\n* **explorer:** last status for indent guides taking hidden / ignored files into account ([94bd2ef](https://github.com/folke/snacks.nvim/commit/94bd2eff74acd7faa78760bf8a55d9c269e99190))\n* **explorer:** strip cwd from search text for explorer items ([38f392a](https://github.com/folke/snacks.nvim/commit/38f392a8ad75ced790f89c8ef43a91f98a2bb6e3))\n* **explorer:** windows ([b560054](https://github.com/folke/snacks.nvim/commit/b56005466952b759a2f610e8b3c8263444402d76))\n* **exporer.tree:** and now hopefully on windows ([ef9b12d](https://github.com/folke/snacks.nvim/commit/ef9b12d68010a931c76533925a8c730123241635))\n* **gitbrowse:** add support for GitHub Enterprise Cloud repo url ([#1089](https://github.com/folke/snacks.nvim/issues/1089)) ([97fd57e](https://github.com/folke/snacks.nvim/commit/97fd57e8a0555023d2968354ca5f2b62de988103))\n* **gitbrowse:** cwd for permalinks ([#1038](https://github.com/folke/snacks.nvim/issues/1038)) ([0bf47dc](https://github.com/folke/snacks.nvim/commit/0bf47dc319e4d6848366aff5c1a42cd08672d3e3))\n* **gitbrowse:** previous logic always overwrote 'commit' ([#1127](https://github.com/folke/snacks.nvim/issues/1127)) ([2f3f080](https://github.com/folke/snacks.nvim/commit/2f3f080ede4d5f75c0b02d1698156648832cb974))\n* **git:** use nul char as separator for git status ([8e0dfd2](https://github.com/folke/snacks.nvim/commit/8e0dfd285665bedf67441efe11c9c1318781826f))\n* **health:** skip dot dirs... Closes [#1293](https://github.com/folke/snacks.nvim/issues/1293) ([aaed4a9](https://github.com/folke/snacks.nvim/commit/aaed4a94111ddfd9d23cdecb01e4ae53030c2c3e))\n* **image.doc:** crop inline typst equations properly ([#1320](https://github.com/folke/snacks.nvim/issues/1320)) ([4f8b9eb](https://github.com/folke/snacks.nvim/commit/4f8b9ebf717b8acf41be02b0bd5a6d75f6038ea7))\n* **image.doc:** fixed at_cursor. Closes [#1258](https://github.com/folke/snacks.nvim/issues/1258) ([76f5ee4](https://github.com/folke/snacks.nvim/commit/76f5ee4a1bd2566fc1460a1b11aa6a0bc36d2f5d))\n* **image.health:** add check for ghost-script to render pdfs. Closes [#1248](https://github.com/folke/snacks.nvim/issues/1248) ([2b52d89](https://github.com/folke/snacks.nvim/commit/2b52d89508a448a1ca0c500afbd325e77023afc1))\n* **image.health:** allow `convert` if `magick` not available ([4589e25](https://github.com/folke/snacks.nvim/commit/4589e2575894090a1e62aae11cf17856f5b84ea5))\n* **image.hover:** close when needed. Closes [#1229](https://github.com/folke/snacks.nvim/issues/1229) ([1f9ba12](https://github.com/folke/snacks.nvim/commit/1f9ba127554bd3bd9780bfb925adfdf1e0ee73f9))\n* **image.latex:** include doc packages for math rendering. Closes [#1262](https://github.com/folke/snacks.nvim/issues/1262) ([2ee6488](https://github.com/folke/snacks.nvim/commit/2ee64887c2be80c6b7b8fac4bb0617c827fde0d0))\n* **image.latex:** inline math formulas. Closes [#1246](https://github.com/folke/snacks.nvim/issues/1246) ([9e422e1](https://github.com/folke/snacks.nvim/commit/9e422e12876002cba59ac4825bbeea89996e0196))\n* **image.markdown:** fix image treesitter query. Closes [#1300](https://github.com/folke/snacks.nvim/issues/1300) ([830ac62](https://github.com/folke/snacks.nvim/commit/830ac62815e70f3db35ed7a295710c836578c6e3))\n* **image.terminal:** set passthrough=all instead of on for tmux. Closes [#1249](https://github.com/folke/snacks.nvim/issues/1249) ([efcc25d](https://github.com/folke/snacks.nvim/commit/efcc25dcfa3ccaa7aa0a11b6cf065f8b8d32e485))\n* **image:** added support for relative paths. Closes [#1143](https://github.com/folke/snacks.nvim/issues/1143) ([2ef6375](https://github.com/folke/snacks.nvim/commit/2ef63754b9c2a835a1d464766a22ba1bd4b16ea3))\n* **image:** better cell size calculation for non-HDPI displays ([e146a66](https://github.com/folke/snacks.nvim/commit/e146a66cb767c60c6e84b2ab9a4522abdb6a5cc0))\n* **image:** better image position caluclation. Closes [#1268](https://github.com/folke/snacks.nvim/issues/1268) ([5c0607e](https://github.com/folke/snacks.nvim/commit/5c0607e31a76317bc34f840fe8cc283b6a8d00c5))\n* **image:** create cache dir ([f8c4e03](https://github.com/folke/snacks.nvim/commit/f8c4e03d025de17fb2302b3253bc72b8c0693c24))\n* **image:** delay sending first image, to make ghostty happy. Closes [#1333](https://github.com/folke/snacks.nvim/issues/1333) ([9aa8cbb](https://github.com/folke/snacks.nvim/commit/9aa8cbb8031750fce640d476df67d88f60fd7c4e))\n* **image:** delete terminal image on exit, just to be sure ([317bfac](https://github.com/folke/snacks.nvim/commit/317bfaca65dc53aa0a74885cf0c48c64fdfc30a9))\n* **image:** do not attach to invalid buffers ([#1238](https://github.com/folke/snacks.nvim/issues/1238)) ([9a5e4de](https://github.com/folke/snacks.nvim/commit/9a5e4deaec451e77464759b4d78e7207caee14a7))\n* **image:** don't fallback to `convert` on windows, since that is a system tool ([c1a1984](https://github.com/folke/snacks.nvim/commit/c1a1984fdb537017b6239d5592d1f7d25a77caa9))\n* **image:** failed state ([5a37d83](https://github.com/folke/snacks.nvim/commit/5a37d838973f216822448a9dae935724754acbf0))\n* **image:** fix disappearing images when changing colorscheme ([44e2f8e](https://github.com/folke/snacks.nvim/commit/44e2f8e573a8e4971badb8c7d3c1181fed7d5de3))\n* **image:** fixed gsub for angle brackets. Closes [#1301](https://github.com/folke/snacks.nvim/issues/1301) ([beaa1c2](https://github.com/folke/snacks.nvim/commit/beaa1c2efcc598a9752b4536ef95606a10835aaa))\n* **image:** fixup ([de3cba5](https://github.com/folke/snacks.nvim/commit/de3cba5158509b82e2f0ff9fc9101effccc1a863))\n* **image:** handle file uppercase file extensions. Closes [#1202](https://github.com/folke/snacks.nvim/issues/1202) ([356f621](https://github.com/folke/snacks.nvim/commit/356f6216b90b85878af2c0134c8f3955349cae18))\n* **image:** handle inline images at the same TS node, but that changed url. See [#1203](https://github.com/folke/snacks.nvim/issues/1203) ([86e3ddf](https://github.com/folke/snacks.nvim/commit/86e3ddf2e4f7a08d8172d2b2383eb51b4c0bbb5f))\n* **image:** hide progress when finished loading in for wezterm ([526896a](https://github.com/folke/snacks.nvim/commit/526896ad3e736786c4520efce6f97c831677ca69))\n* **image:** let text conversion continue on errors. See [#1303](https://github.com/folke/snacks.nvim/issues/1303) ([6d1cda4](https://github.com/folke/snacks.nvim/commit/6d1cda4a6df71f146daf171fbd6d95e53123a61e))\n* **image:** mermaid theme. Closes [#1282](https://github.com/folke/snacks.nvim/issues/1282) ([8117fb4](https://github.com/folke/snacks.nvim/commit/8117fb4cbbaec9fbcfe7fe0b6c3a9c933d6c27ee))\n* **image:** move assertion for src/content. See [#1276](https://github.com/folke/snacks.nvim/issues/1276) ([31e21cc](https://github.com/folke/snacks.nvim/commit/31e21ccef857e600a72fc059ee660fd134595f9d))\n* **image:** only load image when the file exists. Closes [#1143](https://github.com/folke/snacks.nvim/issues/1143) ([298499d](https://github.com/folke/snacks.nvim/commit/298499dcb943ab49946e648ab79bf14868480560))\n* **image:** only setup tmux pass-through on supported terminals. Fixes [#1054](https://github.com/folke/snacks.nvim/issues/1054) ([78e692c](https://github.com/folke/snacks.nvim/commit/78e692cd07b752e29e021635a70f353024d9c9b4))\n* **image:** prevent image id collisions by interleaving the nvim pid hash in the image id ([31788ba](https://github.com/folke/snacks.nvim/commit/31788ba74e12081e79165f4447f6ff0f7e33b696))\n* **image:** relax check for wezterm. Closes [#1076](https://github.com/folke/snacks.nvim/issues/1076) ([8d5ae25](https://github.com/folke/snacks.nvim/commit/8d5ae25806f88ec6c79f094eb7f3cc3413584309))\n* **image:** remove `wezterm` from supported terminals, since they don't support unicode placeholders. Closes [#1063](https://github.com/folke/snacks.nvim/issues/1063) ([345260f](https://github.com/folke/snacks.nvim/commit/345260f39f70d63625e63d3c6771b2a8224f45c9))\n* **image:** remove debug ([13863ea](https://github.com/folke/snacks.nvim/commit/13863ea25d169ef35f939b836c5edf8116042b89))\n* **image:** remove ft check, since we use lang already. Closes [#1177](https://github.com/folke/snacks.nvim/issues/1177) ([4bcd26a](https://github.com/folke/snacks.nvim/commit/4bcd26aca8150a70b40b62673731046f85a205ff))\n* **image:** remove some default latex packages ([f45dd6c](https://github.com/folke/snacks.nvim/commit/f45dd6c44c1319a2660b3b390d8d39ec5f2d73dc))\n* **image:** remove test ([462578e](https://github.com/folke/snacks.nvim/commit/462578edb8fb13f0c158d2c9ac9479109dfdab31))\n* **image:** return converted filename instead of original src. Closes [#1213](https://github.com/folke/snacks.nvim/issues/1213) ([118eab0](https://github.com/folke/snacks.nvim/commit/118eab0cfd4093bcd2b120378e5ea0685a333950))\n* **image:** show full size when not showing image inline ([d7c8fd9](https://github.com/folke/snacks.nvim/commit/d7c8fd9a482a98e44442071d1d02342ebb256be4))\n* **image:** support Neovim &lt; 0.10 ([c067ffe](https://github.com/folke/snacks.nvim/commit/c067ffe86ce931702f82d2a1bd4c0ea98c3bfdd0))\n* **image:** wrong return when trying second command ([74c4298](https://github.com/folke/snacks.nvim/commit/74c42985be207f6c9ed164bd1fae6be81fecd5bb))\n* **input:** add missing hl group for input title ([#1164](https://github.com/folke/snacks.nvim/issues/1164)) ([7014b91](https://github.com/folke/snacks.nvim/commit/7014b91b927a384a7219629cf53e19573c832c23))\n* **layout:** deep merge instead of shallow merge for window options. Closes [#1166](https://github.com/folke/snacks.nvim/issues/1166) ([27256cf](https://github.com/folke/snacks.nvim/commit/27256cf989475e3305713341930a7709e3670eac))\n* **layout:** just hide any layouts below a backdrop. easier and looks better. ([0dab071](https://github.com/folke/snacks.nvim/commit/0dab071dbabaea642f42b2a13d5fc8f00a391963))\n* **layout:** make sure width/height are at least `1`. Closes [#1090](https://github.com/folke/snacks.nvim/issues/1090) ([c554097](https://github.com/folke/snacks.nvim/commit/c5540974fa7a55720f0a1e55d0afe948c8f8fe0a))\n* **layout:** take winbar into account for split layouts. Closes [#996](https://github.com/folke/snacks.nvim/issues/996) ([e4e5040](https://github.com/folke/snacks.nvim/commit/e4e5040d9b9b58ac3bc44a6709bbb5e55e58adea))\n* **layout:** zindex weirdness on stable. Closes [#1180](https://github.com/folke/snacks.nvim/issues/1180) ([72ffb3d](https://github.com/folke/snacks.nvim/commit/72ffb3d1a2812671bb3487e490a3b1dd380bc234))\n* **notifier:** keep notif when current buf is notif buf ([a13c891](https://github.com/folke/snacks.nvim/commit/a13c891a59ec0e67a75824fe1505a9e57fbfca0f))\n* **picker.actions:** better set cmdline. Closes [#1291](https://github.com/folke/snacks.nvim/issues/1291) ([570c035](https://github.com/folke/snacks.nvim/commit/570c035b9417aaa2f02cadf00c83f5b968a70b6c))\n* **picker.actions:** check that plugin exists before loading it in help. Closes [#1134](https://github.com/folke/snacks.nvim/issues/1134) ([e326de9](https://github.com/folke/snacks.nvim/commit/e326de9e0ce4f97c974359568617dc69a0cd6d67))\n* **picker.actions:** don't delete empty buffer when its in another tabpage. Closes [#1005](https://github.com/folke/snacks.nvim/issues/1005) ([1491b54](https://github.com/folke/snacks.nvim/commit/1491b543ef1d8a0eb29a6ebc35db4fb808dcb47f))\n* **picker.actions:** don't reuse_win in floating windows (like the picker preview) ([4b9ea98](https://github.com/folke/snacks.nvim/commit/4b9ea98007cddc0af80fa0479a86a1bf2e880b66))\n* **picker.actions:** fix qflist position ([#911](https://github.com/folke/snacks.nvim/issues/911)) ([6d3c135](https://github.com/folke/snacks.nvim/commit/6d3c1352358e0e2980f9f323b6ca8a62415963bc))\n* **picker.actions:** keymap confirm. Closes [#1252](https://github.com/folke/snacks.nvim/issues/1252) ([a9a84dd](https://github.com/folke/snacks.nvim/commit/a9a84dde2e474eb9ee57630ab2f6418bfe1b380f))\n* **picker.actions:** reverse prev/next on select with a reversed list layout. Closes [#1124](https://github.com/folke/snacks.nvim/issues/1124) ([eae55e7](https://github.com/folke/snacks.nvim/commit/eae55e7ca3b7c33882884a439e12d26200403a66))\n* **picker.actions:** use `vim.v.register` instead of `+` as default. ([9ab6637](https://github.com/folke/snacks.nvim/commit/9ab6637df061fb03c6c5ba937dee5bfef92a6633))\n* **picker.buffers:** remove `dd` to delete buffer from input keymaps. Closes [#1193](https://github.com/folke/snacks.nvim/issues/1193) ([f311d1c](https://github.com/folke/snacks.nvim/commit/f311d1c83a25fbce63e322c72ad6c99a02f84a2f))\n* **picker.colorscheme:** use wildignore. Closes [#969](https://github.com/folke/snacks.nvim/issues/969) ([ba8badf](https://github.com/folke/snacks.nvim/commit/ba8badfe74783e97934c21a69e0c44883092587f))\n* **picker.config:** use `&lt;c-w&gt;HJKL` to move float to far left/bottom/top/right. Only in normal mode. ([34dd83c](https://github.com/folke/snacks.nvim/commit/34dd83c2572658c3f6140e8a8acc1bcfbf7cf32b))\n* **picker.explorer:** ensure diagnostics can be disabled ([#1145](https://github.com/folke/snacks.nvim/issues/1145)) ([885c140](https://github.com/folke/snacks.nvim/commit/885c1409e898b2f6f806cdb31f6ca9d7d84b4ff3))\n* **picker.git:** account for deleted files in git diff. Closes [#1001](https://github.com/folke/snacks.nvim/issues/1001) ([e9e2e69](https://github.com/folke/snacks.nvim/commit/e9e2e6976e3cc7c1110892c9c4a6882dd88ca6fd))\n* **picker.git:** apply args to `git`, and not `git grep`. ([2e284e2](https://github.com/folke/snacks.nvim/commit/2e284e23d956767a50321de9c9bb0c005ea7c51f))\n* **picker.git:** better handling of multi file staging ([b39a3ba](https://github.com/folke/snacks.nvim/commit/b39a3ba40af7c63e0cf0f5e6a2c242c6d3f22591))\n* **picker.git:** correct root dir for git log ([c114a0d](https://github.com/folke/snacks.nvim/commit/c114a0da1a3984345c3035474b8a688592288c9d))\n* **picker.git:** formatting of git log ([f320026](https://github.com/folke/snacks.nvim/commit/f32002607a5a81a1d25eda27b954fc6ba8e9fd1b))\n* **picker.git:** handle git status renames. Closes [#1003](https://github.com/folke/snacks.nvim/issues/1003) ([93ad23a](https://github.com/folke/snacks.nvim/commit/93ad23a0abb4c712722b74e3c066e6b42881fc81))\n* **picker.git:** preserve chronological order when matching ([#1216](https://github.com/folke/snacks.nvim/issues/1216)) ([8b19fd0](https://github.com/folke/snacks.nvim/commit/8b19fd0332835d48f7fe9fe203fa1c2b27976cd2))\n* **picker.git:** properly handle file renames for git log. Closes [#1154](https://github.com/folke/snacks.nvim/issues/1154) ([9c436cb](https://github.com/folke/snacks.nvim/commit/9c436cb273c9b6984da275ba449fda2780d4fa2e))\n* **picker.help:** make sure plugin is loaded for which we want to view the help ([3841a87](https://github.com/folke/snacks.nvim/commit/3841a8705a5e433d88539176d7c67a0ee6a9a92c))\n* **picker.highlight:** lower case treesitter parser name ([3367983](https://github.com/folke/snacks.nvim/commit/336798345c1503689917a4a4a03a03a3da33119a))\n* **picker.highlights:** close on confirm. Closes [#1096](https://github.com/folke/snacks.nvim/issues/1096) ([76f6e4f](https://github.com/folke/snacks.nvim/commit/76f6e4f81cff6f00c8ff027af9351f38ffa6d9f0))\n* **picker.input:** prevent save dialog ([fcb2f50](https://github.com/folke/snacks.nvim/commit/fcb2f508dd6b58c98b781229db895d22c69e6f21))\n* **picker.lines:** use original buf instead of current (which can be the picker on refresh) ([7ccf9c9](https://github.com/folke/snacks.nvim/commit/7ccf9c9d6934a76d5bd835bbd6cf1e764960f14e))\n* **picker.list:** `list:view` should never transform reverse. Closes [#1016](https://github.com/folke/snacks.nvim/issues/1016) ([be781f9](https://github.com/folke/snacks.nvim/commit/be781f9fcb3d99db03c9c6979386565b65f8801b))\n* **picker.list:** allow horizontal scrolling in the list ([572436b](https://github.com/folke/snacks.nvim/commit/572436bc3f16691172a6a0e94c8ffaf16b4170f0))\n* **picker.list:** better wrap settings for when wrapping is enabled ([a542ea4](https://github.com/folke/snacks.nvim/commit/a542ea4d3487bd1aa449350c320bfdbe0c23083b))\n* **picker.list:** correct offset calculation for large scrolloff. Closes [#1208](https://github.com/folke/snacks.nvim/issues/1208) ([f4ca368](https://github.com/folke/snacks.nvim/commit/f4ca368672e2231cc34abbd96208812cc6bb1aa1))\n* **picker.list:** don't return non-matching items. Closes [#1133](https://github.com/folke/snacks.nvim/issues/1133) ([d07e7ac](https://github.com/folke/snacks.nvim/commit/d07e7ac6209356f74405bdd9d881fcacdf80f5ad))\n* **picker.list:** don't show preview when target cursor/top not yet reached. Closes [#1204](https://github.com/folke/snacks.nvim/issues/1204) ([b02cb5e](https://github.com/folke/snacks.nvim/commit/b02cb5e8826179b385b870edbda1631213391cf1))\n* **picker.list:** dont transform with reverse for resolving target. Closes [#1142](https://github.com/folke/snacks.nvim/issues/1142) ([0e36317](https://github.com/folke/snacks.nvim/commit/0e363177bd4a8037a127bc3fab6bf9d442da1123))\n* **picker.list:** keep existing target if it exists unless `force = true`. Closes [#1152](https://github.com/folke/snacks.nvim/issues/1152) ([121e74e](https://github.com/folke/snacks.nvim/commit/121e74e4a5b7962ee370a8d8ae75d1c7b4c2e11c))\n* **picker.list:** let user override wrap ([22da4bd](https://github.com/folke/snacks.nvim/commit/22da4bd5118a63268e6516ac74a8c3dc514218d3))\n* **picker.list:** reset preview when no results. Closes [#1133](https://github.com/folke/snacks.nvim/issues/1133) ([f8bc119](https://github.com/folke/snacks.nvim/commit/f8bc1192cb3f740913f9198fabaf87b46434a926))\n* **picker.lsp:** fix indent guides for sorted document symbols ([17360e4](https://github.com/folke/snacks.nvim/commit/17360e400905f50c5cc513b072c207233f825a73))\n* **picker.lsp:** handle invalid lsp kinds. Closes [#1182](https://github.com/folke/snacks.nvim/issues/1182) ([f3cdd02](https://github.com/folke/snacks.nvim/commit/f3cdd02620bd5075e453be7451a260dbbee68cab))\n* **picker.lsp:** only sort when not getting workspace symbols. Closes [#1071](https://github.com/folke/snacks.nvim/issues/1071) ([d607d2e](https://github.com/folke/snacks.nvim/commit/d607d2e050d9ab19ebe51db38aab807958f05bad))\n* **picker.lsp:** sort document symbols by position ([cc22177](https://github.com/folke/snacks.nvim/commit/cc22177dcf288195022b0f739da3d00fcf56e3d7))\n* **picker.matcher:** don't optimize pattern subsets when pattern has a negation ([a6b3d78](https://github.com/folke/snacks.nvim/commit/a6b3d7840baef2cc9207353a7c1a782fc8508af9))\n* **picker.matcher:** only consider subset patterns that contain only whitespace and alpha numeric chars. Closes [#1013](https://github.com/folke/snacks.nvim/issues/1013) ([fcf2311](https://github.com/folke/snacks.nvim/commit/fcf2311c0e68d91b71bc1be114ad13e84cd7771d))\n* **picker.notifications:** close on confirm. Closes [#1092](https://github.com/folke/snacks.nvim/issues/1092) ([a8dda99](https://github.com/folke/snacks.nvim/commit/a8dda993e5f2a0262a2be1585511a6df7e5dcb8c))\n* **picker.preview:** clear namespace on reset ([a6d418e](https://github.com/folke/snacks.nvim/commit/a6d418e877033de9a12288cdbf7e78d2f0f5d661))\n* **picker.preview:** don't clear preview state on close so that colorscheme can be restored. Closes [#932](https://github.com/folke/snacks.nvim/issues/932) ([9688bd9](https://github.com/folke/snacks.nvim/commit/9688bd92cda4fbe57210bbdfbb9c940516382f9a))\n* **picker.preview:** don't reset preview when filtering and the same item is previewed ([c8285c2](https://github.com/folke/snacks.nvim/commit/c8285c2ca2c4805019e105967f17e60f82faf106))\n* **picker.preview:** fix newlines before setting lines of a buffer ([62c2c62](https://github.com/folke/snacks.nvim/commit/62c2c62671cf88ace1bd9fdd26411158d7072e0b))\n* **picker.preview:** hide line numbers / status column for directory preview. Closes [#1029](https://github.com/folke/snacks.nvim/issues/1029) ([f9aca86](https://github.com/folke/snacks.nvim/commit/f9aca86bf3ddbbd56cb53f71250c301f90af35a2))\n* **picker.preview:** preview for uris. Closes [#1075](https://github.com/folke/snacks.nvim/issues/1075) ([c1f93e2](https://github.com/folke/snacks.nvim/commit/c1f93e25bb927dce2e1eb46610b6347460f0c69b))\n* **picker.preview:** update titles on layout update. Closes [#1113](https://github.com/folke/snacks.nvim/issues/1113) ([89b3ce1](https://github.com/folke/snacks.nvim/commit/89b3ce11ca700badc92af0e1a37be2e19b79cd55))\n* **picker.preview:** work-around for Neovim's messy window-local options (that are never truly local). Closes [#1100](https://github.com/folke/snacks.nvim/issues/1100) ([e5960d8](https://github.com/folke/snacks.nvim/commit/e5960d8e32ed2771a1d84ce4532bf0e2dc4dc8ca))\n* **picker.proc:** don't close stdout on proc exit, since it might still contain buffered output. Closes [#966](https://github.com/folke/snacks.nvim/issues/966) ([3b7021e](https://github.com/folke/snacks.nvim/commit/3b7021e7fdf88e13fdf06643ae9a7224e1291495))\n* **picker.proc:** make sure to emit the last line when done. Closes [#1095](https://github.com/folke/snacks.nvim/issues/1095) ([b94926e](https://github.com/folke/snacks.nvim/commit/b94926e5cc697c54c73e7c2b3759c8432afca91d))\n* **picker.projects:** add custom project dirs ([c7293bd](https://github.com/folke/snacks.nvim/commit/c7293bdfe7664eca6f49816795ffb7f2af5b8302))\n* **picker.projects:** use fd or fdfind ([270250c](https://github.com/folke/snacks.nvim/commit/270250cf4646dbb16c3d1a453257a3f024b8f362))\n* **picker.watch:** schedule_wrap. Closes [#1049](https://github.com/folke/snacks.nvim/issues/1049) ([f489d61](https://github.com/folke/snacks.nvim/commit/f489d61f54c3a32c35c439a16ff0f097dbe93028))\n* **picker.zoxide:** directory icon ([#1031](https://github.com/folke/snacks.nvim/issues/1031)) ([33dbebb](https://github.com/folke/snacks.nvim/commit/33dbebb75395b5e80e441214985c0d9143d323d6))\n* **picker:** `nil` on `:quit`. Closes [#1107](https://github.com/folke/snacks.nvim/issues/1107) ([1219f5e](https://github.com/folke/snacks.nvim/commit/1219f5e43baf1c17e305d605d3db8972aae19bf5))\n* **picker:** `opts.focus = false` now works again ([031f9e9](https://github.com/folke/snacks.nvim/commit/031f9e96fb85cd417868ab2ba03946cb98fd06c8))\n* **picker:** closed check for show preview. Closes [#1181](https://github.com/folke/snacks.nvim/issues/1181) ([c1f4d30](https://github.com/folke/snacks.nvim/commit/c1f4d3032529a7af3d0863c775841f6dc13e03d6))\n* **picker:** consider zen windows as main. Closes [#973](https://github.com/folke/snacks.nvim/issues/973) ([b1db65a](https://github.com/folke/snacks.nvim/commit/b1db65ac61127581cbe3bca8e54a8faf8ce16e5f))\n* **picker:** disabled preview main ([9fe43bd](https://github.com/folke/snacks.nvim/commit/9fe43bdf9b6c04b129e84bd7c2cb7ebd8e04bfae))\n* **picker:** don't render list when closed. See [#1308](https://github.com/folke/snacks.nvim/issues/1308) ([681ae6e](https://github.com/folke/snacks.nvim/commit/681ae6e3078503d2c4cc137a492782a0ee3977b3))\n* **picker:** exit insert mode before closing with `&lt;c-c&gt;` to prevent cursor shifting left. Close [#956](https://github.com/folke/snacks.nvim/issues/956) ([71eae96](https://github.com/folke/snacks.nvim/commit/71eae96bfa5ccafad9966a7bc40982ebe05d8f5d))\n* **picker:** go back to last window on cancel instead of main ([4551f49](https://github.com/folke/snacks.nvim/commit/4551f499c7945036761fd48927cc07b9720fce56))\n* **picker:** initial preview state when main ([cd6e336](https://github.com/folke/snacks.nvim/commit/cd6e336ec0dc8b95e7a75c86cba297a16929370e))\n* **picker:** only show extmark errors when debug is enabled. Closes [#988](https://github.com/folke/snacks.nvim/issues/988) ([f6d9af7](https://github.com/folke/snacks.nvim/commit/f6d9af7410963780c48772f7bd9ee3f5e7be8599))\n* **picker:** remove debug ([a23b10e](https://github.com/folke/snacks.nvim/commit/a23b10e6cafeae7b9e06be47ba49295d0c921a97))\n* **picker:** remove debug :) ([3d53a73](https://github.com/folke/snacks.nvim/commit/3d53a7364e438a7652bb6b90b95c334c32cab938))\n* **picker:** save toggles for resume. Closes [#1085](https://github.com/folke/snacks.nvim/issues/1085) ([e390713](https://github.com/folke/snacks.nvim/commit/e390713ac6f92d0076f38b518645b55222ecf4d1))\n* **picker:** sometimes main layout win gets selected. Closes [#1015](https://github.com/folke/snacks.nvim/issues/1015) ([4799f82](https://github.com/folke/snacks.nvim/commit/4799f829683272b06ad9bf8b8e9816f28b3a46ef))\n* **picker:** update titles last on show. Closes [#1113](https://github.com/folke/snacks.nvim/issues/1113) ([96796db](https://github.com/folke/snacks.nvim/commit/96796db21e474eff0d0ddeee2afa6c2c346756c7))\n* **picker:** vim.ui.select callback is called when canceling selection ([#1115](https://github.com/folke/snacks.nvim/issues/1115)) ([4c3bfa2](https://github.com/folke/snacks.nvim/commit/4c3bfa29f3122c4fb855c1adaef01cf22612624a))\n* **scroll:** added `keepjumps` ([7161dc1](https://github.com/folke/snacks.nvim/commit/7161dc1b570849324bb2b0b808c6f2cc46ef6f84))\n* **statuscolumn:** only execute `za` when fold exists ([#1093](https://github.com/folke/snacks.nvim/issues/1093)) ([345eeb6](https://github.com/folke/snacks.nvim/commit/345eeb69417f5568930f283e3be01f0ef55bee63))\n* **terminal:** check for 0.11 ([6e45829](https://github.com/folke/snacks.nvim/commit/6e45829879da987cb4ed01d3098eb2507da72343))\n* **terminal:** softer check for using jobstart with `term=true` instead of deprecated termopen ([544a2ae](https://github.com/folke/snacks.nvim/commit/544a2ae01c28056629a0c90f8d0ff40995c84e42))\n* **toggle:** hide toggle when real keymap does not exist. Closes [#378](https://github.com/folke/snacks.nvim/issues/378) ([ee9e617](https://github.com/folke/snacks.nvim/commit/ee9e6179fe18a2bf36ebb5e81ddf1052e04577dc))\n* **win:** apply win-local window options for new buffers displayed in a window. Fixes [#925](https://github.com/folke/snacks.nvim/issues/925) ([cb99c46](https://github.com/folke/snacks.nvim/commit/cb99c46fa171134f582f6b13bef32f6d25ebda59))\n* **win:** better handling when the command window is open. Closes [#1245](https://github.com/folke/snacks.nvim/issues/1245) ([7720410](https://github.com/folke/snacks.nvim/commit/77204102a1f5869bf37d8ccbc5e8e0769cfe8db4))\n* **win:** call `on_close` before actually closing so that prev win can be set. Closes [#962](https://github.com/folke/snacks.nvim/issues/962) ([a1cb54c](https://github.com/folke/snacks.nvim/commit/a1cb54cc9e579c53bbd4b96949acf2341b31a3ee))\n* **words:** default count to 1. Closes [#1307](https://github.com/folke/snacks.nvim/issues/1307) ([45ec90b](https://github.com/folke/snacks.nvim/commit/45ec90bdd91d7730b81662ee3bfcdd4a88ed908f))\n* **zen:** properly get zoom options. Closes [#1207](https://github.com/folke/snacks.nvim/issues/1207) ([3100333](https://github.com/folke/snacks.nvim/commit/3100333fdb777853c77aeac46b92fcdaba8e3e57))\n\n\n### Performance Improvements\n\n* **dashboard:** speed up filtering for recent_files ([#1250](https://github.com/folke/snacks.nvim/issues/1250)) ([b91f417](https://github.com/folke/snacks.nvim/commit/b91f417670e8f35ac96a2ebdecceeafdcc43ba4a))\n* **explorer:** disable watchdirs fallback watcher ([5d34380](https://github.com/folke/snacks.nvim/commit/5d34380310861cd42e32ce0865bd8cded9027b41))\n* **explorer:** early exit for tree calculation ([1a30610](https://github.com/folke/snacks.nvim/commit/1a30610ab78cce8bb184166de2ef35ee2ca1987a))\n* **explorer:** no need to get git status with `-uall`. Closes [#983](https://github.com/folke/snacks.nvim/issues/983) ([bc087d3](https://github.com/folke/snacks.nvim/commit/bc087d36d6126ccf25f8bb3ead405ec32547d85d))\n* **explorer:** only update tree if git status actually changed ([5a2acf8](https://github.com/folke/snacks.nvim/commit/5a2acf82b2aff0b6f7121ce953c5754de6fd1e01))\n* **explorer:** only update tree when diagnostics actually changed ([1142f46](https://github.com/folke/snacks.nvim/commit/1142f46a27358c8f48023382389a8b31c9628b6b))\n* **image.convert:** identify during convert instead of calling identify afterwards ([7b7f42f](https://github.com/folke/snacks.nvim/commit/7b7f42fb3bee6083677d66b301424c26b4ff41c2))\n* **image:** no need to run identify before convert for local files ([e2d9941](https://github.com/folke/snacks.nvim/commit/e2d99418968b0dc690ca6b56dac688d70e9b5e40))\n* **picker.list:** only re-render when visible items changed ([c72e62e](https://github.com/folke/snacks.nvim/commit/c72e62ef9012161ec6cd86aa749d780f77d1cc87))\n* **picker:** cache treesitter line highlights ([af31c31](https://github.com/folke/snacks.nvim/commit/af31c312872cab2a47e17ed2ee67bf5940a522d4))\n* **picker:** cache wether ts lang exists and disable smooth scrolling on big files ([719b36f](https://github.com/folke/snacks.nvim/commit/719b36fa70c35a7015537aa0bfd2956f6128c87d))\n* **scroll:** much better/easier/faster method for vertical cursor positioning ([a3194d9](https://github.com/folke/snacks.nvim/commit/a3194d95199c4699a4da0d4c425a19544ed8d670))\n\n\n### Documentation\n\n* docgen ([b503e3e](https://github.com/folke/snacks.nvim/commit/b503e3ee9fdd57202e5815747e67d1f6259468a4))\n\n## [2.20.0](https://github.com/folke/snacks.nvim/compare/v2.19.0...v2.20.0) (2025-02-08)\n\n\n### Features\n\n* **picker.undo:** `ctrl+y` to yank added lines, `ctrl+shift+y` to yank deleted lines ([3baf95d](https://github.com/folke/snacks.nvim/commit/3baf95d3a1005105b57ce53644ff6224ee3afa1c))\n* **picker:** added treesitter symbols picker ([a6beb0f](https://github.com/folke/snacks.nvim/commit/a6beb0f280d3f43513998882faf199acf3818ddf))\n* **terminal:** added `Snacks.terminal.list()`. Closes [#421](https://github.com/folke/snacks.nvim/issues/421). Closes [#759](https://github.com/folke/snacks.nvim/issues/759) ([73c4b62](https://github.com/folke/snacks.nvim/commit/73c4b628963004760ccced0192d1c2633c9e3657))\n\n\n### Bug Fixes\n\n* **explorer:** change grep in item dir keymap to leader-/. Closes [#1000](https://github.com/folke/snacks.nvim/issues/1000) ([9dfa276](https://github.com/folke/snacks.nvim/commit/9dfa276ea424a091f5d5bdc008aff127850441b2))\n* **layout:** take winbar into account for split layouts. Closes [#996](https://github.com/folke/snacks.nvim/issues/996) ([e4e5040](https://github.com/folke/snacks.nvim/commit/e4e5040d9b9b58ac3bc44a6709bbb5e55e58adea))\n* **picker.git:** account for deleted files in git diff. Closes [#1001](https://github.com/folke/snacks.nvim/issues/1001) ([e9e2e69](https://github.com/folke/snacks.nvim/commit/e9e2e6976e3cc7c1110892c9c4a6882dd88ca6fd))\n* **picker.git:** handle git status renames. Closes [#1003](https://github.com/folke/snacks.nvim/issues/1003) ([93ad23a](https://github.com/folke/snacks.nvim/commit/93ad23a0abb4c712722b74e3c066e6b42881fc81))\n* **picker.lines:** use original buf instead of current (which can be the picker on refresh) ([7ccf9c9](https://github.com/folke/snacks.nvim/commit/7ccf9c9d6934a76d5bd835bbd6cf1e764960f14e))\n* **picker.proc:** don't close stdout on proc exit, since it might still contain buffered output. Closes [#966](https://github.com/folke/snacks.nvim/issues/966) ([3b7021e](https://github.com/folke/snacks.nvim/commit/3b7021e7fdf88e13fdf06643ae9a7224e1291495))\n\n## [2.19.0](https://github.com/folke/snacks.nvim/compare/v2.18.0...v2.19.0) (2025-02-07)\n\n\n### Features\n\n* **bigfile:** configurable average line length (default = 1000). Useful for minified files. Closes [#576](https://github.com/folke/snacks.nvim/issues/576). Closes [#372](https://github.com/folke/snacks.nvim/issues/372) ([7fa92a2](https://github.com/folke/snacks.nvim/commit/7fa92a24501fa85b567c130b4e026f9ca1efed17))\n* **explorer:** add hl groups for ignored / hidden files. Closes [#887](https://github.com/folke/snacks.nvim/issues/887) ([85e1b34](https://github.com/folke/snacks.nvim/commit/85e1b343b0cc6d2facc0763d9e1c1de4b63b99ac))\n* **explorer:** added ctrl+f to grep in the item's directory ([0454b21](https://github.com/folke/snacks.nvim/commit/0454b21165cb84d2f59a1daf6226de065c90d4f7))\n* **explorer:** added ctrl+t to open a terminal in the item's directory ([81f9006](https://github.com/folke/snacks.nvim/commit/81f90062c50430c1bad9546fcb65c3e43a76be9b))\n* **explorer:** added diagnostics file/directory status ([7f1b60d](https://github.com/folke/snacks.nvim/commit/7f1b60d5576345af5e7b990f3a9e4bca49cd3686))\n* **explorer:** added quick nav with `[`, `]` with `d/w/e` for diagnostics ([d1d5585](https://github.com/folke/snacks.nvim/commit/d1d55850ecb4aac1396c314a159db1e90a34bd79))\n* **explorer:** added support for live search ([82c4a50](https://github.com/folke/snacks.nvim/commit/82c4a50985c9bb9f4b1d598f10a30e1122a35212))\n* **explorer:** allow disabling untracked git status. Closes [#983](https://github.com/folke/snacks.nvim/issues/983) ([a3b083b](https://github.com/folke/snacks.nvim/commit/a3b083b8443b1ae1299747fdac8da51c3160835b))\n* **explorer:** different hl group for broken links ([1989921](https://github.com/folke/snacks.nvim/commit/1989921466e6b5234ae8f71add41b8defd55f732))\n* **explorer:** disable fuzzy searches by default for explorer since it's too noisy and we can't sort on score due to tree view ([b07788f](https://github.com/folke/snacks.nvim/commit/b07788f14a28daa8d0b387c1258f8f348f47420f))\n* **explorer:** file watcher when explorer is open ([6936c14](https://github.com/folke/snacks.nvim/commit/6936c1491d4aa8ffb4448acca677589a1472bb3a))\n* **explorer:** file watching that works on all platforms ([8399465](https://github.com/folke/snacks.nvim/commit/8399465872c51fab54ad5d02eb315e258ec96ed1))\n* **explorer:** focus on first file when searching in the explorer ([1d4bea4](https://github.com/folke/snacks.nvim/commit/1d4bea4a9ee8a5258c6ae085ac66dd5cc05a9749))\n* **explorer:** git index watcher ([4c12475](https://github.com/folke/snacks.nvim/commit/4c12475e80528d8d48b9584d78d645e4a51c3298))\n* **explorer:** rewrite that no longer depends on `fd` for exploring ([6149a7b](https://github.com/folke/snacks.nvim/commit/6149a7babbd2c6d9cd924bb70102d80a7f045287))\n* **explorer:** show symlink target ([dfa79e0](https://github.com/folke/snacks.nvim/commit/dfa79e04436ebfdc83ba71c0048fc1636b4de5aa))\n* **matcher:** call on_match after setting score ([23ce529](https://github.com/folke/snacks.nvim/commit/23ce529fb663337f9dc17ca08aa601b172469031))\n* **picker.git:** add confirmation before deleting a git branch ([#951](https://github.com/folke/snacks.nvim/issues/951)) ([337a3ae](https://github.com/folke/snacks.nvim/commit/337a3ae7eebb95020596f15a349a85d2f6be31a4))\n* **picker.git:** add create and delete branch to git_branches ([#909](https://github.com/folke/snacks.nvim/issues/909)) ([8676c40](https://github.com/folke/snacks.nvim/commit/8676c409e148e28eff93c114aca0c1bf3d42281a))\n* **picker.lazy:** don't use `grep`. Parse spec files manually. Closes [#972](https://github.com/folke/snacks.nvim/issues/972) ([0928007](https://github.com/folke/snacks.nvim/commit/09280078e8339f018be5249fe0e1d7b9d32db7f7))\n* **picker.lsp:** use existing buffers for preview when opened ([d4e6353](https://github.com/folke/snacks.nvim/commit/d4e63531c9fba63ded6fb470a5d53c98af110478))\n* **picker.matcher:** internal `on_match` ([47b3b69](https://github.com/folke/snacks.nvim/commit/47b3b69570271b12bbd72b9dbcfbd445b915beca))\n* **picker.preview:** allow confguring `preview = {main = true, enabled = false}` ([1839c65](https://github.com/folke/snacks.nvim/commit/1839c65f6784bedb7ae96a84ee741fa5c0023226))\n* **picker.undo:** added ctrl+y to yank added lines from undo ([811a24c](https://github.com/folke/snacks.nvim/commit/811a24cc16a8e9b7ec947c95b73e1fe05e4692d1))\n* **picker:** `picker:dir()` to get the dir of the item (when a directory) or it's parent (when a file) ([969608a](https://github.com/folke/snacks.nvim/commit/969608ab795718cc23299247bf4ea0a199fcca3f))\n* **picker:** added `git_grep` picker. Closes [#986](https://github.com/folke/snacks.nvim/issues/986) ([2dc9016](https://github.com/folke/snacks.nvim/commit/2dc901634b250059cc9b7129bdeeedd24520b86c))\n* **picker:** allow configuring file icon width. Closes [#981](https://github.com/folke/snacks.nvim/issues/981) ([52c1086](https://github.com/folke/snacks.nvim/commit/52c1086ecdf410dfec3317144d46de7c6f86c1ad))\n* **picker:** better checkhealth ([b773368](https://github.com/folke/snacks.nvim/commit/b773368f8aa6e84a68e979f0e335d23de71f405a))\n* **picker:** get filetype from modeline when needed. Closes [#987](https://github.com/folke/snacks.nvim/issues/987) ([5af04ab](https://github.com/folke/snacks.nvim/commit/5af04ab6672ae38bf7d72427e75f925615f93904))\n* **picker:** opts.on_close ([6235f44](https://github.com/folke/snacks.nvim/commit/6235f44b115c45dd009c45b81a52f8d99863efaa))\n* **picker:** pin picker as a split to left/bottom/top/right with `ctrl+z+(hjkl)` ([27cba53](https://github.com/folke/snacks.nvim/commit/27cba535a6763cbca3f3162c5c4bb48c6f382005))\n* **rename:** add `old` to `on_rename` callback ([455228e](https://github.com/folke/snacks.nvim/commit/455228ed3a07bf3aee34a75910785b9978f53da6))\n* **scope:** allow injected languages to be parsed by treesitter ([#823](https://github.com/folke/snacks.nvim/issues/823)) ([aba21dd](https://github.com/folke/snacks.nvim/commit/aba21ddc712b12db8469680dd7f2080063cb6d5c))\n* **statuscolumn:** added mouse click handler to open/close folds. Closes [#968](https://github.com/folke/snacks.nvim/issues/968) ([98a7b64](https://github.com/folke/snacks.nvim/commit/98a7b647c9e245ef02d57d566bf8461c2f7beb56))\n* **terminal:** added `start_insert` ([64129e4](https://github.com/folke/snacks.nvim/commit/64129e4c3c5b247c61b1f46bc0faaa1e69e7eef8))\n* **terminal:** auto_close and auto_insert. Closes [#965](https://github.com/folke/snacks.nvim/issues/965) ([bb76cae](https://github.com/folke/snacks.nvim/commit/bb76cae87e81a871435570b91c8c6f6e27eb9955))\n\n\n### Bug Fixes\n\n* **bigfile:** check that passed path is the one from the buffer ([8deea64](https://github.com/folke/snacks.nvim/commit/8deea64dba3b9b8f57e52bb6b0133263f6ff171f))\n* **explorer.git:** better git status watching ([09349ec](https://github.com/folke/snacks.nvim/commit/09349ecd44040666db9d4835994a378a9ff53e8c))\n* **explorer.git:** dont reset cursor when git status is done updating ([bc87992](https://github.com/folke/snacks.nvim/commit/bc87992e712c29ef8e826f3550f9b8e3f1a9308d))\n* **explorer.git:** vim.schedule git updates ([3aad761](https://github.com/folke/snacks.nvim/commit/3aad7616209951320d54f83dd7df35d5578ea61f))\n* **explorer.tree:** fix linux ([6f5399b](https://github.com/folke/snacks.nvim/commit/6f5399b47c55f916fcc3a82dcc71cce0eb5d7c92))\n* **explorer.tree:** symlink directories ([e5f1e91](https://github.com/folke/snacks.nvim/commit/e5f1e91249b468ff3a7d14a8650074c27f1fdb30))\n* **explorer.watch:** pcall watcher, since it can give errors on windows ([af96818](https://github.com/folke/snacks.nvim/commit/af968181af6ce6a988765fe51558b2caefdcf863))\n* **explorer:** always refresh state when opening the picker since changes might have happened that were not monitored ([c61114f](https://github.com/folke/snacks.nvim/commit/c61114fb32910863a543a4a7a1f63e9915983d26))\n* **explorer:** call original `on_close`. Closes [#971](https://github.com/folke/snacks.nvim/issues/971) ([a0bee9f](https://github.com/folke/snacks.nvim/commit/a0bee9f662d4e22c6533e6544b4daedecd2aacc0))\n* **explorer:** check that picker is still open ([50fa1be](https://github.com/folke/snacks.nvim/commit/50fa1be38ee8366d79e1fa58b38abf31d3955033))\n* **explorer:** clear cache after action. Fixes [#890](https://github.com/folke/snacks.nvim/issues/890) ([34097ff](https://github.com/folke/snacks.nvim/commit/34097ff37e0fb53771bbe3bf927048d06b4576f6))\n* **explorer:** clear selection after delete. Closes [#898](https://github.com/folke/snacks.nvim/issues/898) ([44733ea](https://github.com/folke/snacks.nvim/commit/44733eaa78fb899dc3b3d72d7cac6f447356a287))\n* **explorer:** disable follow for explorer search by default. No longer needed. Link directories may show as files then though, but that's not an issue. See [#960](https://github.com/folke/snacks.nvim/issues/960) ([b9a17d8](https://github.com/folke/snacks.nvim/commit/b9a17d82a726dc6cfd9a0b4f8566178708073808))\n* **explorer:** don't use --absolute-path option, since that resolves paths to realpath. See [#901](https://github.com/folke/snacks.nvim/issues/901). See [#905](https://github.com/folke/snacks.nvim/issues/905). See [#904](https://github.com/folke/snacks.nvim/issues/904) ([97570d2](https://github.com/folke/snacks.nvim/commit/97570d23ac42dac813c5eb49637381fa3b728246))\n* **explorer:** dont focus first file when not searching ([3fd437c](https://github.com/folke/snacks.nvim/commit/3fd437ccd38d79b876154097149d130cdb01e653))\n* **explorer:** dont process git when picker closed ([c255d9c](https://github.com/folke/snacks.nvim/commit/c255d9c6a02f070f0048c5eaa40921f71e9f2acb))\n* **explorer:** last status for indent guides taking hidden / ignored files into account ([94bd2ef](https://github.com/folke/snacks.nvim/commit/94bd2eff74acd7faa78760bf8a55d9c269e99190))\n* **explorer:** strip cwd from search text for explorer items ([38f392a](https://github.com/folke/snacks.nvim/commit/38f392a8ad75ced790f89c8ef43a91f98a2bb6e3))\n* **explorer:** windows ([b560054](https://github.com/folke/snacks.nvim/commit/b56005466952b759a2f610e8b3c8263444402d76))\n* **exporer.tree:** and now hopefully on windows ([ef9b12d](https://github.com/folke/snacks.nvim/commit/ef9b12d68010a931c76533925a8c730123241635))\n* **git:** use nul char as separator for git status ([8e0dfd2](https://github.com/folke/snacks.nvim/commit/8e0dfd285665bedf67441efe11c9c1318781826f))\n* **health:** better health checks for picker. Closes [#917](https://github.com/folke/snacks.nvim/issues/917) ([427f036](https://github.com/folke/snacks.nvim/commit/427f036c6532097859d9177f32ccb037803abaa4))\n* **picker.actions:** close preview before buffer delete ([762821e](https://github.com/folke/snacks.nvim/commit/762821e420cef56b03b6897b008454eefe68fd1d))\n* **picker.actions:** don't reuse_win in floating windows (like the picker preview) ([4b9ea98](https://github.com/folke/snacks.nvim/commit/4b9ea98007cddc0af80fa0479a86a1bf2e880b66))\n* **picker.actions:** fix qflist position ([#911](https://github.com/folke/snacks.nvim/issues/911)) ([6d3c135](https://github.com/folke/snacks.nvim/commit/6d3c1352358e0e2980f9f323b6ca8a62415963bc))\n* **picker.actions:** get win after splitting or tabnew. Fixes [#896](https://github.com/folke/snacks.nvim/issues/896) ([95d3e7f](https://github.com/folke/snacks.nvim/commit/95d3e7f96182e4cc8aa0c05a616a61eba9a77366))\n* **picker.colorscheme:** use wildignore. Closes [#969](https://github.com/folke/snacks.nvim/issues/969) ([ba8badf](https://github.com/folke/snacks.nvim/commit/ba8badfe74783e97934c21a69e0c44883092587f))\n* **picker.db:** better script to download sqlite3 on windows. See [#918](https://github.com/folke/snacks.nvim/issues/918) ([84d1a92](https://github.com/folke/snacks.nvim/commit/84d1a92faba55a470a6c52a1aca86988b0c57869))\n* **picker.finder:** correct check if filter changed ([52bc24c](https://github.com/folke/snacks.nvim/commit/52bc24c23256246e863992dfcc3172c527254f55))\n* **picker.input:** fixed startinsert weirdness with prompt buffers (again) ([c030827](https://github.com/folke/snacks.nvim/commit/c030827d7ad3fe7117bf81c0db1613c958015211))\n* **picker.input:** set as not modified when setting input through API ([54a041f](https://github.com/folke/snacks.nvim/commit/54a041f7fca05234379d7bceff6b036acc679cdc))\n* **picker.list:** better wrap settings for when wrapping is enabled ([a542ea4](https://github.com/folke/snacks.nvim/commit/a542ea4d3487bd1aa449350c320bfdbe0c23083b))\n* **picker.list:** let user override wrap ([22da4bd](https://github.com/folke/snacks.nvim/commit/22da4bd5118a63268e6516ac74a8c3dc514218d3))\n* **picker.list:** nil check ([c22e46a](https://github.com/folke/snacks.nvim/commit/c22e46ab9a1f1416368759e0979bc5c0c64c0084))\n* **picker.lsp:** fix indent guides for sorted document symbols ([17360e4](https://github.com/folke/snacks.nvim/commit/17360e400905f50c5cc513b072c207233f825a73))\n* **picker.lsp:** sort document symbols by position ([cc22177](https://github.com/folke/snacks.nvim/commit/cc22177dcf288195022b0f739da3d00fcf56e3d7))\n* **picker.matcher:** don't optimize pattern subsets when pattern has a negation ([a6b3d78](https://github.com/folke/snacks.nvim/commit/a6b3d7840baef2cc9207353a7c1a782fc8508af9))\n* **picker.preview:** don't clear preview state on close so that colorscheme can be restored. Closes [#932](https://github.com/folke/snacks.nvim/issues/932) ([9688bd9](https://github.com/folke/snacks.nvim/commit/9688bd92cda4fbe57210bbdfbb9c940516382f9a))\n* **picker.preview:** update main preview when changing the layout ([604c603](https://github.com/folke/snacks.nvim/commit/604c603dfafdac0c2edc725ff8bcdcc395100028))\n* **picker.projects:** add custom project dirs ([c7293bd](https://github.com/folke/snacks.nvim/commit/c7293bdfe7664eca6f49816795ffb7f2af5b8302))\n* **picker.projects:** use fd or fdfind ([270250c](https://github.com/folke/snacks.nvim/commit/270250cf4646dbb16c3d1a453257a3f024b8f362))\n* **picker.select:** default height shows just the items. See [#902](https://github.com/folke/snacks.nvim/issues/902) ([c667622](https://github.com/folke/snacks.nvim/commit/c667622fb7569a020195e3e35c1405e4a1fa0e7e))\n* **picker:** better handling when entering the root layout window. Closes [#894](https://github.com/folke/snacks.nvim/issues/894) ([e294fd8](https://github.com/folke/snacks.nvim/commit/e294fd8a273b1f5622c8a3259802d91a1ed01110))\n* **picker:** consider zen windows as main. Closes [#973](https://github.com/folke/snacks.nvim/issues/973) ([b1db65a](https://github.com/folke/snacks.nvim/commit/b1db65ac61127581cbe3bca8e54a8faf8ce16e5f))\n* **picker:** disabled preview main ([9fe43bd](https://github.com/folke/snacks.nvim/commit/9fe43bdf9b6c04b129e84bd7c2cb7ebd8e04bfae))\n* **picker:** exit insert mode before closing with `&lt;c-c&gt;` to prevent cursor shifting left. Close [#956](https://github.com/folke/snacks.nvim/issues/956) ([71eae96](https://github.com/folke/snacks.nvim/commit/71eae96bfa5ccafad9966a7bc40982ebe05d8f5d))\n* **picker:** initial preview state when main ([cd6e336](https://github.com/folke/snacks.nvim/commit/cd6e336ec0dc8b95e7a75c86cba297a16929370e))\n* **picker:** only show extmark errors when debug is enabled. Closes [#988](https://github.com/folke/snacks.nvim/issues/988) ([f6d9af7](https://github.com/folke/snacks.nvim/commit/f6d9af7410963780c48772f7bd9ee3f5e7be8599))\n* **win:** apply win-local window options for new buffers displayed in a window. Fixes [#925](https://github.com/folke/snacks.nvim/issues/925) ([cb99c46](https://github.com/folke/snacks.nvim/commit/cb99c46fa171134f582f6b13bef32f6d25ebda59))\n* **win:** better handling of alien buffers opening in managed windows. See [#886](https://github.com/folke/snacks.nvim/issues/886) ([c8430fd](https://github.com/folke/snacks.nvim/commit/c8430fdd8dec6aa21c73f293cbd363084fb56ab0))\n\n\n### Performance Improvements\n\n* **explorer:** disable watchdirs fallback watcher ([5d34380](https://github.com/folke/snacks.nvim/commit/5d34380310861cd42e32ce0865bd8cded9027b41))\n* **explorer:** early exit for tree calculation ([1a30610](https://github.com/folke/snacks.nvim/commit/1a30610ab78cce8bb184166de2ef35ee2ca1987a))\n* **explorer:** no need to get git status with `-uall`. Closes [#983](https://github.com/folke/snacks.nvim/issues/983) ([bc087d3](https://github.com/folke/snacks.nvim/commit/bc087d36d6126ccf25f8bb3ead405ec32547d85d))\n* **picker.list:** only re-render when visible items changed ([c72e62e](https://github.com/folke/snacks.nvim/commit/c72e62ef9012161ec6cd86aa749d780f77d1cc87))\n* **picker:** cache treesitter line highlights ([af31c31](https://github.com/folke/snacks.nvim/commit/af31c312872cab2a47e17ed2ee67bf5940a522d4))\n* **picker:** cache wether ts lang exists and disable smooth scrolling on big files ([719b36f](https://github.com/folke/snacks.nvim/commit/719b36fa70c35a7015537aa0bfd2956f6128c87d))\n\n## [2.18.0](https://github.com/folke/snacks.nvim/compare/v2.17.0...v2.18.0) (2025-02-03)\n\n\n### Features\n\n* **dashboard:** play nice with file explorer netrw replacement ([5420a64](https://github.com/folke/snacks.nvim/commit/5420a64b66fd7350f5bb9d5dea2372850ea59969))\n* **explorer.git:** added git_status_open. When false, then dont show recursive git status in open directories ([8646ba4](https://github.com/folke/snacks.nvim/commit/8646ba469630b73a34c06243664fb5607c0a43fa))\n* **explorer:** added `]g` and `[g` to jump between files mentioned in `git status` ([263dfde](https://github.com/folke/snacks.nvim/commit/263dfde1b598e0fbba5f0031b8976e3c979f553c))\n* **explorer:** added git status. Closes [#817](https://github.com/folke/snacks.nvim/issues/817) ([5cae48d](https://github.com/folke/snacks.nvim/commit/5cae48d93c875efa302bdffa995e4b057e2c3731))\n* **explorer:** hide git status for open directories by default. it's mostly redundant ([b40c0d4](https://github.com/folke/snacks.nvim/commit/b40c0d4ee4e53aadc5fcf0900e58690c49f9763f))\n* **explorer:** keep expanded dir state. Closes [#816](https://github.com/folke/snacks.nvim/issues/816) ([31984e8](https://github.com/folke/snacks.nvim/commit/31984e88a51652bda4997456c53113cbdc811cb4))\n* **explorer:** more keymaps and tree rework. See [#837](https://github.com/folke/snacks.nvim/issues/837) ([2ff3893](https://github.com/folke/snacks.nvim/commit/2ff389312a78a8615754c2dee32b96211c9f9c54))\n* **explorer:** navigate with h/l to close/open directories. Closes [#833](https://github.com/folke/snacks.nvim/issues/833) ([4b29ddc](https://github.com/folke/snacks.nvim/commit/4b29ddc5d9856ff49a07d77a43634e00b06f4d31))\n* **explorer:** new `explorer` module with shortcut to start explorer picker and netrw replacement functionlity ([670c673](https://github.com/folke/snacks.nvim/commit/670c67366f0025fc4ebb78ba35a7586b7477989a))\n* **explorer:** recursive copy and copying of selected items with `c` ([2528fcb](https://github.com/folke/snacks.nvim/commit/2528fcb02ceab7b19ee72a94b93c620259881e65))\n* **explorer:** update on cwd change ([8dea225](https://github.com/folke/snacks.nvim/commit/8dea2252094ca3dc6d2073ab0015b7bcee396e24))\n* **explorer:** update status when saving a file that is currently visible ([78d4116](https://github.com/folke/snacks.nvim/commit/78d4116662d38acb8456ffc6869204b487b472f8))\n* **picker.commands:** do not autorun commands that require arguments ([#879](https://github.com/folke/snacks.nvim/issues/879)) ([62d99ed](https://github.com/folke/snacks.nvim/commit/62d99ed2a3711769e34fbc33c7072075824d256a))\n* **picker.frecency:** added frecency support for directories ([ce67fa9](https://github.com/folke/snacks.nvim/commit/ce67fa9e31467590c750e203e27d3e6df293f2ad))\n* **picker.input:** search syntax highlighting ([4242f90](https://github.com/folke/snacks.nvim/commit/4242f90268c93e7e546c195df26d9f0ee6b10645))\n* **picker.lines:** jump to first position of match. Closes [#806](https://github.com/folke/snacks.nvim/issues/806) ([ae897f3](https://github.com/folke/snacks.nvim/commit/ae897f329f06695ee3482e2cd768797d5af3e277))\n* **picker.list:** use regular CursorLine when picker window is not focused ([8a570bb](https://github.com/folke/snacks.nvim/commit/8a570bb48ba3536dcfe51f08547896b55fcb0e4d))\n* **picker.matcher:** added opts.matcher.history_bonus that does fzf's scheme=history. Closes [#882](https://github.com/folke/snacks.nvim/issues/882). Closes [#872](https://github.com/folke/snacks.nvim/issues/872) ([78c2853](https://github.com/folke/snacks.nvim/commit/78c28535ddd4e3973bcdc9bdf342a37979010918))\n* **picker.pickwin:** optional win/buf filter. Closes [#877](https://github.com/folke/snacks.nvim/issues/877) ([5c5b40b](https://github.com/folke/snacks.nvim/commit/5c5b40b5d0ed4166c751a11a839b015f4ad6e26a))\n* **picker.projects:** added `&lt;c-t&gt;` to open a new tab page with the projects picker ([ced377a](https://github.com/folke/snacks.nvim/commit/ced377a05783073ab3a8506b5a9b0ffaf8293773))\n* **picker.projects:** allow disabling projects from recent files ([c2dedb6](https://github.com/folke/snacks.nvim/commit/c2dedb647f6e170ee4defed647c7f89a51ee9fd0))\n* **picker.projects:** default to tcd instead of cd ([3d2a075](https://github.com/folke/snacks.nvim/commit/3d2a07503f0724794a7e262a2f570a13843abedf))\n* **picker.projects:** enabled frecency for projects picker ([5a20565](https://github.com/folke/snacks.nvim/commit/5a2056575faa50f788293ee787b803c15f42a9e0))\n* **picker.projects:** projects now automatically processes dev folders and added a bunch of actions/keymaps. Closes [#871](https://github.com/folke/snacks.nvim/issues/871) ([6f8f0d3](https://github.com/folke/snacks.nvim/commit/6f8f0d3c727fe035dffc0bc4c1843e2a06eee1f2))\n* **picker.undo:** better undo tree visualization. Closes [#863](https://github.com/folke/snacks.nvim/issues/863) ([44b8e38](https://github.com/folke/snacks.nvim/commit/44b8e3820493ca774cd220e3daf85c16954e74c7))\n* **picker.undo:** make diff opts for undo configurable ([d61fb45](https://github.com/folke/snacks.nvim/commit/d61fb453c6c23976759e16a33fd8d6cb79cc59bc))\n* **picker:** added support for double cliking and confirm ([8b26bae](https://github.com/folke/snacks.nvim/commit/8b26bae6bb01db22dbd3c6f868736487265025c0))\n* **picker:** automatically download sqlite3.dll on Windows when using frecency / history for the first time. ([65907f7](https://github.com/folke/snacks.nvim/commit/65907f75ba52c09afc16e3d8d3c7ac67a3916237))\n* **picker:** beter API to interact with active pickers. Closes [#851](https://github.com/folke/snacks.nvim/issues/851) ([6a83373](https://github.com/folke/snacks.nvim/commit/6a8337396ad843b27bdfe0a03ac2ce26ccf13092))\n* **picker:** better health checks. Fixes [#855](https://github.com/folke/snacks.nvim/issues/855) ([d245925](https://github.com/folke/snacks.nvim/commit/d2459258f1a56109a2ad506f4a4dd6c69f2bb9f2))\n* **picker:** history per source. Closes [#843](https://github.com/folke/snacks.nvim/issues/843) ([35295e0](https://github.com/folke/snacks.nvim/commit/35295e0eb2ee261e6173545190bc6c181fd08067))\n\n\n### Bug Fixes\n\n* **dashboard:** open pull requests with P instead of p in the github exmaple ([b2815d7](https://github.com/folke/snacks.nvim/commit/b2815d7f79e82d09cde5c9bb8e6fd13976b4d618))\n* **dashboard:** update on VimResized and WinResized ([558b0ee](https://github.com/folke/snacks.nvim/commit/558b0ee04d0c6e1acf842774fbf9e02cce3efb0e))\n* **explorer:** after search, cursor always jumped to top. Closes [#827](https://github.com/folke/snacks.nvim/issues/827) ([d17449e](https://github.com/folke/snacks.nvim/commit/d17449ee90b78843a22ee12ae29c3c110b28eac7))\n* **explorer:** always use `--follow` to make sure we see dir symlinks as dirs. Fixes [#814](https://github.com/folke/snacks.nvim/issues/814) ([151fd3d](https://github.com/folke/snacks.nvim/commit/151fd3d62d73e0ec122bb243003c3bd59d53f8ef))\n* **explorer:** cwd is now changed automatically, so no need to update state. ([5549d4e](https://github.com/folke/snacks.nvim/commit/5549d4e848b865ad4cc5bbb9bdd9487d631c795b))\n* **explorer:** don't disable netrw fully. Just the autocmd that loads a directory ([836eb9a](https://github.com/folke/snacks.nvim/commit/836eb9a4e9ca0d7973f733203871d70691447c2b))\n* **explorer:** don't try to show when closed. Fixes [#836](https://github.com/folke/snacks.nvim/issues/836) ([6921cd0](https://github.com/folke/snacks.nvim/commit/6921cd06ac7b530d786b2282afdfce67762008f1))\n* **explorer:** fix git status sorting ([551d053](https://github.com/folke/snacks.nvim/commit/551d053c7ccc635249c262a5ea38b5d7aa814b3a))\n* **explorer:** fixed hierarchical sorting. Closes [#828](https://github.com/folke/snacks.nvim/issues/828) ([fa32e20](https://github.com/folke/snacks.nvim/commit/fa32e20e9910f8071979f16788832027d1e25850))\n* **explorer:** keep global git status cache ([a54a21a](https://github.com/folke/snacks.nvim/commit/a54a21adc0e67b97fb787adcbaaf4578c6f44476))\n* **explorer:** remove sleep :) ([efbc4a1](https://github.com/folke/snacks.nvim/commit/efbc4a12af6aae39dadeab0badb84d04a94d5f85))\n* **git:** use os.getenv to get env var. Fixes [#5504](https://github.com/folke/snacks.nvim/issues/5504) ([16d700e](https://github.com/folke/snacks.nvim/commit/16d700eb65fc320a5ab8e131d8f5d185b241887b))\n* **layout:** adjust zindex when needed when another layout is already open. Closes [#826](https://github.com/folke/snacks.nvim/issues/826) ([ab8af1b](https://github.com/folke/snacks.nvim/commit/ab8af1bb32a4d9f82156122056d07a0850c2a828))\n* **layout:** destroy in schedule. Fixes [#861](https://github.com/folke/snacks.nvim/issues/861) ([c955a8d](https://github.com/folke/snacks.nvim/commit/c955a8d1ef543fd56907d5291e92e62fd944db9b))\n* **picker.actions:** fix split/vsplit/tab. Closes [#818](https://github.com/folke/snacks.nvim/issues/818) ([ff02241](https://github.com/folke/snacks.nvim/commit/ff022416dd6e6dade2ee822469d0087fcf3e0509))\n* **picker.actions:** pass edit commands to jump. Closes [#859](https://github.com/folke/snacks.nvim/issues/859) ([df0e3e3](https://github.com/folke/snacks.nvim/commit/df0e3e3d861fd7b8ab85b3e8dbca97817b3d6604))\n* **picker.actions:** reworked split/vsplit/drop/tab cmds to better do what's intended. Closes [#854](https://github.com/folke/snacks.nvim/issues/854) ([2946875](https://github.com/folke/snacks.nvim/commit/2946875af09f5f439ce64b78da8da6cf28523b8c))\n* **picker.actions:** tab -&gt; tabnew. Closes [#842](https://github.com/folke/snacks.nvim/issues/842) ([d962d5f](https://github.com/folke/snacks.nvim/commit/d962d5f3359dc91da7aa54388515fd0b03a2fe8b))\n* **picker.explorer:** do LSP stuff on move ([894ff74](https://github.com/folke/snacks.nvim/commit/894ff749300342593007e6366894b681b3148f19))\n* **picker.explorer:** use cached git status ([1ce435c](https://github.com/folke/snacks.nvim/commit/1ce435c6eb161feae63c8ddfe3e1aaf98b2aa41d))\n* **picker.format:** extra slash in path ([dad3e00](https://github.com/folke/snacks.nvim/commit/dad3e00e83ec8a8af92e778e29f2fe200ad0d969))\n* **picker.format:** use item.file for filename_only ([e784a9e](https://github.com/folke/snacks.nvim/commit/e784a9e6723371f8f453a92edb03d68428da74cc))\n* **picker.git_log:** add extra space between the date and the message ([#885](https://github.com/folke/snacks.nvim/issues/885)) ([d897ead](https://github.com/folke/snacks.nvim/commit/d897ead2b78a73a186a3cb1b735a10f2606ddb36))\n* **picker.keymaps:** added normalized lhs to search text ([fbd39a4](https://github.com/folke/snacks.nvim/commit/fbd39a48df085a7df979a06b1003faf86625c157))\n* **picker.lazy:** don't use live searches. Fixes [#809](https://github.com/folke/snacks.nvim/issues/809) ([1a5fd93](https://github.com/folke/snacks.nvim/commit/1a5fd93b89904b8f8029e6ee74e6d6ada87f28c5))\n* **picker.lines:** col is first non-whitespace. Closes [#806](https://github.com/folke/snacks.nvim/issues/806) ([ec8eb60](https://github.com/folke/snacks.nvim/commit/ec8eb6051530261e7d0e5566721e5c396c1ed6cd))\n* **picker.list:** better virtual scrolling that works from any window ([4a50291](https://github.com/folke/snacks.nvim/commit/4a502914486346940389a99690578adca9a820bb))\n* **picker.matcher:** fix cwd_bonus check ([00af290](https://github.com/folke/snacks.nvim/commit/00af2909064433ee84280dd64233a34b0f8d6027))\n* **picker.matcher:** regex offset by one. Fixes [#878](https://github.com/folke/snacks.nvim/issues/878) ([9a82f0a](https://github.com/folke/snacks.nvim/commit/9a82f0a564df3e4f017e9b66da6baa41196962b7))\n* **picker.undo:** add newlines ([72826a7](https://github.com/folke/snacks.nvim/commit/72826a72de93f49b2446c691e3bef04df1a44dde))\n* **picker.undo:** cleanup tmp buffer ([8368176](https://github.com/folke/snacks.nvim/commit/83681762435a425ab1edb10fe3244b3e8b1280c2))\n* **picker.undo:** copy over buffer lines instead of just the file ([c900e2c](https://github.com/folke/snacks.nvim/commit/c900e2cb3ab83c299c95756fc34e4ae52f4e72e9))\n* **picker.undo:** disable swap for tmp undo buffer ([033db25](https://github.com/folke/snacks.nvim/commit/033db250cd688872724a84deb623b599662d79c5))\n* **picker:** better main window management. Closes [#842](https://github.com/folke/snacks.nvim/issues/842) ([f0f053a](https://github.com/folke/snacks.nvim/commit/f0f053a1d9b16edaa27f05e20ad6fd862db8c6f7))\n* **picker:** improve resume. Closes [#853](https://github.com/folke/snacks.nvim/issues/853) ([0f5b30b](https://github.com/folke/snacks.nvim/commit/0f5b30b41196d831cda84e4b792df2ce765fd856))\n* **picker:** make pick_win work with split layouts. Closes [#834](https://github.com/folke/snacks.nvim/issues/834) ([6dbc267](https://github.com/folke/snacks.nvim/commit/6dbc26757cb043c8153a4251a1f75bff4dcadf68))\n* **picker:** multi layouts that need async task work again. ([cd44efb](https://github.com/folke/snacks.nvim/commit/cd44efb60ce70382de02d069e269bb40e5e7fa22))\n* **picker:** no auto-close when entering a floating window ([08e6c12](https://github.com/folke/snacks.nvim/commit/08e6c12358d57dfb497f8ce7de7eb09134868dc7))\n* **picker:** no need to track jumping ([b37ea74](https://github.com/folke/snacks.nvim/commit/b37ea748b6ff56cd479600b1c39d19a308ee7eae))\n* **picker:** propagate WinEnter when going to the real window after entering the layout split window ([8555789](https://github.com/folke/snacks.nvim/commit/8555789d86f7f6127fdf023723775207972e0c44))\n* **picker:** show regex matches in list when needed. Fixes [#878](https://github.com/folke/snacks.nvim/issues/878) ([1d99bac](https://github.com/folke/snacks.nvim/commit/1d99bac9bcf75a11adc6cd78cde4477a95f014bd))\n* **picker:** show_empty for files / grep. Closes [#808](https://github.com/folke/snacks.nvim/issues/808) ([a13ff6f](https://github.com/folke/snacks.nvim/commit/a13ff6fe0f68c3242d6be5e352d762b6037a9695))\n* **util:** better default icons when no icon plugin is installed ([0e4ddfd](https://github.com/folke/snacks.nvim/commit/0e4ddfd3ee1d81def4028e52e44e45ac3ce98cfc))\n* **util:** better keymap normalization ([e1566a4](https://github.com/folke/snacks.nvim/commit/e1566a483df1badc97729f66b1faf358d2bd3362))\n* **util:** normkey. Closes [#763](https://github.com/folke/snacks.nvim/issues/763) ([6972960](https://github.com/folke/snacks.nvim/commit/69729608e101923810a13942f0b3bef98f253592))\n* **win:** close help when leaving the win buffer ([4aba559](https://github.com/folke/snacks.nvim/commit/4aba559c6e321f90524a2e8164b8fd1f9329552e))\n\n\n### Performance Improvements\n\n* **explorer:** don't wait till git status finished. Update tree when needed. See [#869](https://github.com/folke/snacks.nvim/issues/869) ([287db30](https://github.com/folke/snacks.nvim/commit/287db30ed21dc2a8be80fabcffcec9b1b878e04e))\n* **explorer:** use cache when possible for opening/closing directories. Closes [#869](https://github.com/folke/snacks.nvim/issues/869) ([cef4fc9](https://github.com/folke/snacks.nvim/commit/cef4fc91813ba6e6932db88a1c9e82a30ea51349))\n* **git:** also check top-level path to see if it's a git root. Closes [#807](https://github.com/folke/snacks.nvim/issues/807) ([b9e7c51](https://github.com/folke/snacks.nvim/commit/b9e7c51e8f7eea876275e52f1083b58f9d2df92f))\n* **picker.files:** no need to check every time for cmd availability ([8f87c2c](https://github.com/folke/snacks.nvim/commit/8f87c2c32bbb75a4fad4f5768d5faa963c4f66d8))\n* **picker.undo:** more performance improvements for the undo picker ([3d4b8ee](https://github.com/folke/snacks.nvim/commit/3d4b8eeea9380eb7488217af74f9448eaa7b376e))\n* **picker.undo:** use a tmp buffer to get diffs. Way faster than before. Closes [#863](https://github.com/folke/snacks.nvim/issues/863) ([d4a5449](https://github.com/folke/snacks.nvim/commit/d4a54498131a5d9027bccdf2cd0edd2d22594ce7))\n\n## [2.17.0](https://github.com/folke/snacks.nvim/compare/v2.16.0...v2.17.0) (2025-01-30)\n\n\n### Features\n\n* **picker.actions:** allow selecting the visual selection with `&lt;Tab&gt;` ([96c76c6](https://github.com/folke/snacks.nvim/commit/96c76c6d9d401c2205d73639389b32470c550e6a))\n* **picker.explorer:** focus dir on confirm from search ([605f745](https://github.com/folke/snacks.nvim/commit/605f7451984f0011635423571ad83ab74f342ed8))\n\n\n### Bug Fixes\n\n* **git:** basic support for git work trees ([d76d9aa](https://github.com/folke/snacks.nvim/commit/d76d9aaaf2399c6cf15c5b37a9183680b106a4af))\n* **picker.preview:** properly refresh the preview after layout changes. Fixes [#802](https://github.com/folke/snacks.nvim/issues/802) ([47993f9](https://github.com/folke/snacks.nvim/commit/47993f9a809ce13e98c3132d463d5a3002289fd6))\n* **picker:** add proper close ([15a9411](https://github.com/folke/snacks.nvim/commit/15a94117e17d78c8c2e579d20988d4cb9e85d098))\n* **picker:** make jumping work again... ([f40f338](https://github.com/folke/snacks.nvim/commit/f40f338d669bf2d54b224e4a973c52c8157fe505))\n* **picker:** show help for input / list window with `?`. ([87dab7e](https://github.com/folke/snacks.nvim/commit/87dab7eca7949b85c0ee688f86c08b8c437f9433))\n* **win:** properly handle closing the last window. Fixes [#793](https://github.com/folke/snacks.nvim/issues/793) ([18de5bb](https://github.com/folke/snacks.nvim/commit/18de5bb23898fa2055afee5035c97a2abe4aae6b))\n\n## [2.16.0](https://github.com/folke/snacks.nvim/compare/v2.15.0...v2.16.0) (2025-01-30)\n\n\n### Features\n\n* **layout:** added support for split layouts (root box can be a split) ([6da592e](https://github.com/folke/snacks.nvim/commit/6da592e130295388ee64fe282eb0dafa0b99fa2f))\n* **layout:** make fullscreen work for split layouts ([115f8c6](https://github.com/folke/snacks.nvim/commit/115f8c6ae9c9a57b36677b728a6f6cc9207c6489))\n* **picker.actions:** added separate hl group for pick win current ([9b80e44](https://github.com/folke/snacks.nvim/commit/9b80e444f548aea26214a95ad9e1affc4ef5d91c))\n* **picker.actions:** separate edit_split etc in separate split and edit actions. Fixes [#791](https://github.com/folke/snacks.nvim/issues/791) ([3564f4f](https://github.com/folke/snacks.nvim/commit/3564f4fede6feefdfe1dc200295eb3b162996d6d))\n* **picker.config:** added `opts.config` which can be a function that can change the resolved options ([b37f368](https://github.com/folke/snacks.nvim/commit/b37f368a81d0a6ce8b7c9f683f9c0c736af6de36))\n* **picker.explorer:** added `explorer_move` action mapped to `m` ([08b9083](https://github.com/folke/snacks.nvim/commit/08b9083f4759c87c93f6afb4af0a1f3d2b8ad1fa))\n* **picker.explorer:** live search ([db52796](https://github.com/folke/snacks.nvim/commit/db52796e79c63dfa0d5d689d5d13b120f6184642))\n* **picker.files:** allow forcing the files finder to use a certain cmd ([3e1dc30](https://github.com/folke/snacks.nvim/commit/3e1dc300cc98815ad74ae11c98f7ebebde966c39))\n* **picker.format:** better path formatting for directories ([08f3c32](https://github.com/folke/snacks.nvim/commit/08f3c32c7d64a81ea35d1cb0d22fc140d25c9088))\n* **picker.format:** directory formatting ([847509e](https://github.com/folke/snacks.nvim/commit/847509e12c0cd95355cb05c97e1bc8bedde29957))\n* **picker.jump:** added `opts.jump.close`, which default to true, but is false for explorer ([a9591ed](https://github.com/folke/snacks.nvim/commit/a9591ed43f4de3b611028eadce7d36c4b3dedca8))\n* **picker.list:** added support for setting a cursor/topline target for the next render. Target clears when reached, or when finders finishes. ([da08379](https://github.com/folke/snacks.nvim/commit/da083792053e41c57e8ca5e9fe9e5b175b1e378d))\n* **picker:** `opts.focus = \"input\"|\"list\"|false` to configure what to focus (if anything) when showing the picker ([5a8d798](https://github.com/folke/snacks.nvim/commit/5a8d79847b1959f9c9515b51a062f6acbe22f1a4))\n* **picker:** `picker:iter()` now also returns `idx` ([118d908](https://github.com/folke/snacks.nvim/commit/118d90899d7b2bb0a28a799dbf2a21ed39516e66))\n* **picker:** added `edit_win` action bound to `ctrl+enter` to pick a window and edit ([2ba5be8](https://github.com/folke/snacks.nvim/commit/2ba5be84910d14454292423f08ad83ea213de2ba))\n* **picker:** added `git_stash` picker. Closes [#762](https://github.com/folke/snacks.nvim/issues/762) ([bb3db11](https://github.com/folke/snacks.nvim/commit/bb3db117a45da1dabe76f08a75144b028314e6b6))\n* **picker:** added `notifications` picker. Closes [#738](https://github.com/folke/snacks.nvim/issues/738) ([32cffd2](https://github.com/folke/snacks.nvim/commit/32cffd2e603ccace129b62c777933a42203c5c77))\n* **picker:** added support for split layouts to picker (sidebar and ivy_split) ([5496c22](https://github.com/folke/snacks.nvim/commit/5496c22b6e20a26d2252543029faead946cc2ce9))\n* **picker:** added support to keep the picker open when focusing another window (auto_close = false) ([ad8f166](https://github.com/folke/snacks.nvim/commit/ad8f16632c63a3082ea0e80a39cdbd774624532a))\n* **picker:** new file explorer `Snacks.picker.explorer()` ([00613bd](https://github.com/folke/snacks.nvim/commit/00613bd4163c89e01c1d534d283cfe531773fdc8))\n* **picker:** opening a picker with the same source as an active picker, will close it instead (toggle) ([b6cf033](https://github.com/folke/snacks.nvim/commit/b6cf033051aa2f859d9d217bc60e89806fcf5377))\n* **picker:** reworked toggles (flags). they're now configurable. Closes [#770](https://github.com/folke/snacks.nvim/issues/770) ([e16a6a4](https://github.com/folke/snacks.nvim/commit/e16a6a441322c944b41e9ae5b30ba816145218cd))\n* **rename:** optional `file`, `on_rename` for `Snacks.rename.rename_file()` ([9d8c277](https://github.com/folke/snacks.nvim/commit/9d8c277bebb9483b1d46c7eeeff348966076347f))\n\n\n### Bug Fixes\n\n* **bigfile:** check if buf still exists when applying syntax. Fixes [#737](https://github.com/folke/snacks.nvim/issues/737) ([08852ac](https://github.com/folke/snacks.nvim/commit/08852ac7f811f51d47a590d62df805d0e84e611a))\n* **bufdelete:** ctrl-c throw error in `fn.confirm` ([#750](https://github.com/folke/snacks.nvim/issues/750)) ([3a3e795](https://github.com/folke/snacks.nvim/commit/3a3e79535bb085815d3add9f678d30b98b5f900f))\n* **bufdelete:** invalid lua ([b1f4f99](https://github.com/folke/snacks.nvim/commit/b1f4f99a51ef1ca11a0c802b847501b71f09161b))\n* **dashboard:** better handling of closed dashboard win ([6cb7fdf](https://github.com/folke/snacks.nvim/commit/6cb7fdfb036239b9c1b6d147633e494489a45191))\n* **dashboard:** don't override user configuration ([#774](https://github.com/folke/snacks.nvim/issues/774)) ([5ff2ad3](https://github.com/folke/snacks.nvim/commit/5ff2ad320b0cd1e17d48862c74af0df205894f37))\n* **dashboard:** fix dasdhboard when opening in a new win. Closes [#767](https://github.com/folke/snacks.nvim/issues/767) ([d44b978](https://github.com/folke/snacks.nvim/commit/d44b978d7dbe7df8509a172cef0913b5a9ac77e3))\n* **dashboard:** prevent starting picker twice when no session manager. Fixes [#783](https://github.com/folke/snacks.nvim/issues/783) ([2f396b3](https://github.com/folke/snacks.nvim/commit/2f396b341dc1a072643eb402bfaa8a73f6be19a1))\n* **filter:** insert path from `filter.paths` into `self.paths` ([#761](https://github.com/folke/snacks.nvim/issues/761)) ([ac20c6f](https://github.com/folke/snacks.nvim/commit/ac20c6ff5d0ac8747e164d592e8ae7e8f2581b2e))\n* **layout:** better handling of resizing of split layouts ([c8ce9e2](https://github.com/folke/snacks.nvim/commit/c8ce9e2b33623d21901e02213319270936e4545f))\n* **layout:** better update check for split layouts ([b50d697](https://github.com/folke/snacks.nvim/commit/b50d697ce45dbee5efe25371428b7f23b037d0ed))\n* **layout:** make sure split layouts are still visible when a float layout with backdrop opens ([696d198](https://github.com/folke/snacks.nvim/commit/696d1981b18ad1f0cc0e480aafed78a064730417))\n* **matcher.score:** correct prev_class for transition bonusses when in a gap. Fixes [#787](https://github.com/folke/snacks.nvim/issues/787) ([b45d0e0](https://github.com/folke/snacks.nvim/commit/b45d0e03579c80ac901261e0e2705a1be3dfcb20))\n* **picker.actions:** detect and report circular action references ([0ffc003](https://github.com/folke/snacks.nvim/commit/0ffc00367a957c1602df745c2038600d48d96305))\n* **picker.actions:** proper cr check ([6c9f866](https://github.com/folke/snacks.nvim/commit/6c9f866b3123cbc8cbef91f55593d30d98d4f26a))\n* **picker.actions:** stop pick_win when no target and only unhide when picker wasn't stopped ([4a1d189](https://github.com/folke/snacks.nvim/commit/4a1d189f9f7afac4180f7a459597bb094d11435b))\n* **picker.actions:** when only 1 win, `pick_win` will select that automatically. Show warning when no windows. See [#623](https://github.com/folke/snacks.nvim/issues/623) ([ba5a70b](https://github.com/folke/snacks.nvim/commit/ba5a70b84d12aa9e497cfea86d5358aa5cf0aad3))\n* **picker.config:** fix wrong `opts.cwd = true` config. Closes [#757](https://github.com/folke/snacks.nvim/issues/757) ([ea44a2d](https://github.com/folke/snacks.nvim/commit/ea44a2d4c21aa10fb57fe08b974999f7b8d117d2))\n* **picker.config:** normalize `opts.cwd` ([69c013e](https://github.com/folke/snacks.nvim/commit/69c013e1b27e2f70def48576aaffcc1081fa0e47))\n* **picker.explorer:** fix cwd for items ([71070b7](https://github.com/folke/snacks.nvim/commit/71070b78f0482a42448da2cee64ed0d84c507314))\n* **picker.explorer:** stop file follow when picker was closed ([89fcb3b](https://github.com/folke/snacks.nvim/commit/89fcb3bb2025cb1c986e9af3478715f6e0bdf425))\n* **picker.explorer:** when searching, go to first non internal node in the list ([276497b](https://github.com/folke/snacks.nvim/commit/276497b3969cdefd18aa731c5e3d5c1bb8289cca))\n* **picker.filter:** proper cwd check. See [#757](https://github.com/folke/snacks.nvim/issues/757) ([e4ae9e3](https://github.com/folke/snacks.nvim/commit/e4ae9e32295d688a1e0d3f59ab1ba4cc78d1ba89))\n* **picker.git:** better stash pattern. Closes [#775](https://github.com/folke/snacks.nvim/issues/775) ([e960010](https://github.com/folke/snacks.nvim/commit/e960010496f860d1077f1e54d193e127ad7e26ad))\n* **picker.git:** default to git root for `git_files`. Closes [#751](https://github.com/folke/snacks.nvim/issues/751) ([3cdebee](https://github.com/folke/snacks.nvim/commit/3cdebee880742970df1a1f685be4802b90642c7d))\n* **picker.git:** ignore autostash ([2b15357](https://github.com/folke/snacks.nvim/commit/2b15357c25db315567f08e7ec8d5c85c94d0753f))\n* **picker.git:** show diff for staged files. Fixes [#747](https://github.com/folke/snacks.nvim/issues/747) ([e87f0ff](https://github.com/folke/snacks.nvim/commit/e87f0ffcd100a3a6686549e25f480c1311f08d8f))\n* **picker.layout:** fix list cursorline when layout updates ([3f43026](https://github.com/folke/snacks.nvim/commit/3f43026f579f33b679a924dea699df86e8b965b2))\n* **picker.layout:** make split layouts work in layout preview ([215ae72](https://github.com/folke/snacks.nvim/commit/215ae72daaed5d7ee18b72e8b14bfd6a727bc939))\n* **picker.lsp:** remove symbol detail from search text. too noisy ([92710df](https://github.com/folke/snacks.nvim/commit/92710dfb0bacc72a82e0172bb06f5eb9ad82964a))\n* **picker.multi:** apply source filter settings for multi source pickers. See [#761](https://github.com/folke/snacks.nvim/issues/761) ([4e30ff0](https://github.com/folke/snacks.nvim/commit/4e30ff0f1ed58b0bdc8fd3f5f1a9a440959eb998))\n* **picker.preview:** don't enable numbers when minimal=true ([04e2995](https://github.com/folke/snacks.nvim/commit/04e2995bbfc505d0fc91263712d0255f102f404e))\n* **picker.preview:** don't error on invalid start positions for regex. Fixes [#784](https://github.com/folke/snacks.nvim/issues/784) ([460b58b](https://github.com/folke/snacks.nvim/commit/460b58bdbd57e32a1bed22b3e176fa53befeafaa))\n* **picker.preview:** only show binary message when binary and no ft. Closes [#729](https://github.com/folke/snacks.nvim/issues/729) ([ea838e2](https://github.com/folke/snacks.nvim/commit/ea838e28383d74d60cd6d714cac9b007a4a4469a))\n* **picker.resume:** fix picker is nil ([#772](https://github.com/folke/snacks.nvim/issues/772)) ([1a5a087](https://github.com/folke/snacks.nvim/commit/1a5a0871c822e5de8e69c10bb1d6cb3dfc2f5c86))\n* **picker.score:** scoring closer to fzf. See [#787](https://github.com/folke/snacks.nvim/issues/787) ([390f017](https://github.com/folke/snacks.nvim/commit/390f017c3b227377c09ae6572f88b7c42304b811))\n* **picker.select:** allow configuring `vim.ui.select` with the `select` source. Closes [#776](https://github.com/folke/snacks.nvim/issues/776) ([d70af2d](https://github.com/folke/snacks.nvim/commit/d70af2d253f61164a44a8676a5fc316cad10497f))\n* **picker.util:** proper cwd check for paths. Fixes [#754](https://github.com/folke/snacks.nvim/issues/754). See [#757](https://github.com/folke/snacks.nvim/issues/757) ([1069d78](https://github.com/folke/snacks.nvim/commit/1069d783347a7a5213240e2e12e485ab57e15bd8))\n* **picker:** better handling of win Enter/Leave mostly for split layouts ([046653a](https://github.com/folke/snacks.nvim/commit/046653a4f166633339a276999738bac43c3c1388))\n* **picker:** don't destroy active pickers (only an issue when multiple pickers were open) ([b479f10](https://github.com/folke/snacks.nvim/commit/b479f10b24a8cf5325bc575e1bab2fc51ebfde45))\n* **picker:** only do preview scrolling when preview is scrolling and removed default preview horizontal scrolling keymaps ([a998c71](https://github.com/folke/snacks.nvim/commit/a998c714c31ab92a06b9edafef71251b63f0eb5b))\n* **picker:** show new notifications on top ([0df7c0b](https://github.com/folke/snacks.nvim/commit/0df7c0bef59be861f3e6682aa1381f6422f4a0af))\n* **picker:** split edit_win in `{\"pick_win\", \"jump\"}` ([dcd3bc0](https://github.com/folke/snacks.nvim/commit/dcd3bc03295a8521773c04671298bd3fdcb14f7b))\n* **picker:** stopinsert again ([2250c57](https://github.com/folke/snacks.nvim/commit/2250c57529b1a8da4d96966db1cd9a46b73d8007))\n* **win:** don't destroy opts. Fixes [#726](https://github.com/folke/snacks.nvim/issues/726) ([473be03](https://github.com/folke/snacks.nvim/commit/473be039e59730b0554a7dfda2eb800ecf7a948e))\n* **win:** error when enabling padding with `listchars=\"\"` ([#786](https://github.com/folke/snacks.nvim/issues/786)) ([6effbcd](https://github.com/folke/snacks.nvim/commit/6effbcdff110c16f49c2cef5d211db86f6db5820))\n\n\n### Performance Improvements\n\n* **picker.matcher:** optimize matcher priorities and skip items that can't match for pattern subset ([dfaa18d](https://github.com/folke/snacks.nvim/commit/dfaa18d1c72a78cacfe0a682c853b7963641444c))\n* **picker.recent:** correct generator for old files ([5f32414](https://github.com/folke/snacks.nvim/commit/5f32414dd645ab7650dc60379f422b00aaecea4f))\n* **picker.score:** no need to track `in_gap` status. redundant since we can depend on `gap` instead ([fdf4b0b](https://github.com/folke/snacks.nvim/commit/fdf4b0bf26743eef23e645235915aa4920827daf))\n\n## [2.15.0](https://github.com/folke/snacks.nvim/compare/v2.14.0...v2.15.0) (2025-01-23)\n\n\n### Features\n\n* **debug:** truncate inspect to 2000 lines max ([570d219](https://github.com/folke/snacks.nvim/commit/570d2191d598d344ddd5b2a85d8e79d207955cc3))\n* **input:** input history. Closes [#591](https://github.com/folke/snacks.nvim/issues/591) ([80db91f](https://github.com/folke/snacks.nvim/commit/80db91f03e3493e9b3aa09d1cd90b063ae0ec31c))\n* **input:** persistent history. Closes [#591](https://github.com/folke/snacks.nvim/issues/591) ([0ed68bd](https://github.com/folke/snacks.nvim/commit/0ed68bdf7268bf1baef7a403ecc799f2c016b656))\n* **picker.debug:** more info about potential leaks ([8d9677f](https://github.com/folke/snacks.nvim/commit/8d9677fc479710ae1f531fc52b0ac368def55b0b))\n* **picker.filter:** Filter arg for filter ([5a4b684](https://github.com/folke/snacks.nvim/commit/5a4b684c0dd3eda10ce86f9710e085431a7656f2))\n* **picker.finder:** optional transform function ([5e69fb8](https://github.com/folke/snacks.nvim/commit/5e69fb83d50bb79ff62352418733d11562e488d0))\n* **picker.format:** `filename_only` option ([0396bdf](https://github.com/folke/snacks.nvim/commit/0396bdfc3eece8438ed6a978f1dbddf3f675ca36))\n* **picker.git:** git_log, git_log_file, git_log_line now do git_checkout as confirm. Closes [#722](https://github.com/folke/snacks.nvim/issues/722) ([e6fb538](https://github.com/folke/snacks.nvim/commit/e6fb5381a9bfcbd0f1693ea9c7e3711045380187))\n* **picker.help:** add more color to help tags ([5778234](https://github.com/folke/snacks.nvim/commit/5778234e3917999a0be1a5b8145dd83ab41035b3))\n* **picker.keymaps:** add global + buffer toggles ([#705](https://github.com/folke/snacks.nvim/issues/705)) ([b7c08df](https://github.com/folke/snacks.nvim/commit/b7c08df2b8ff23e0293cfe06beaf60aa6fd14efc))\n* **picker.keymaps:** improvements to keymaps picker ([2762c37](https://github.com/folke/snacks.nvim/commit/2762c37eb09bc434eba647d4ec079d6064d3c563))\n* **picker.matcher:** frecency and cwd bonus can now be enabled on any picker ([7b85dfc](https://github.com/folke/snacks.nvim/commit/7b85dfc6f60538b0419ca1b969553891b64cd9b8))\n* **picker.multi:** multi now also merges keymaps ([8b2c78a](https://github.com/folke/snacks.nvim/commit/8b2c78a3bf5a3ca52c8c9e46b9d15c288c59c5c1))\n* **picker.preview:** better positioning of preview location ([3864955](https://github.com/folke/snacks.nvim/commit/38649556ee9f831e5d456043a796ae0fb115f8eb))\n* **picker.preview:** fallback highlight of results when no `end_pos`. Mostly useful for grep. ([d12e454](https://github.com/folke/snacks.nvim/commit/d12e45433960210a16a37adc116e645e253578c1))\n* **picker.smart:** add bufdelete actions from buffers picker ([#679](https://github.com/folke/snacks.nvim/issues/679)) ([67fbab1](https://github.com/folke/snacks.nvim/commit/67fbab1bf7f5c436e28af715097eecb2232eea59))\n* **picker.smart:** re-implemented smart as multi-source picker ([450d1d4](https://github.com/folke/snacks.nvim/commit/450d1d4d4c218ac1df63924d29717caa61c98f27))\n* **picker.util:** smart path truncate. Defaults to 40. Closes [#708](https://github.com/folke/snacks.nvim/issues/708) ([bab8243](https://github.com/folke/snacks.nvim/commit/bab8243395de8d8748a7295906bee7723b7b8190))\n* **picker:** added `lazy` source to search for a plugin spec. Closes [#680](https://github.com/folke/snacks.nvim/issues/680) ([d03bd00](https://github.com/folke/snacks.nvim/commit/d03bd00ced5a03f1cb317b9227a6a1812e35a6c2))\n* **picker:** added `opts.rtp` (bool) to find/grep over files in the rtp. See [#680](https://github.com/folke/snacks.nvim/issues/680) ([9d5d3bd](https://github.com/folke/snacks.nvim/commit/9d5d3bdb1747669d74587662cce93021fc99f878))\n* **picker:** added new `icons` picker for nerd fonts and emoji. Closes [#703](https://github.com/folke/snacks.nvim/issues/703) ([97898e9](https://github.com/folke/snacks.nvim/commit/97898e910d92e50e886ba044ab255360e4271ffc))\n* **picker:** getters and setters for cwd ([2c2ff4c](https://github.com/folke/snacks.nvim/commit/2c2ff4caf85ba1cfee3d946ea6ab9fd595ec3667))\n* **picker:** multi source picker. Combine multiple pickers (as opposed to just finders) in one picker ([9434986](https://github.com/folke/snacks.nvim/commit/9434986ff15acfca7e010f159460f9ecfee81363))\n* **picker:** persistent history. Closes [#528](https://github.com/folke/snacks.nvim/issues/528) ([ea665eb](https://github.com/folke/snacks.nvim/commit/ea665ebad18a8ccd6444df7476237de4164af64a))\n* **picker:** preview window horizontal scrolling ([#686](https://github.com/folke/snacks.nvim/issues/686)) ([bc47e0b](https://github.com/folke/snacks.nvim/commit/bc47e0b1dd0102b58a90aba87f22e0cc0a48985c))\n* **picker:** syntax highlighting for command and search history ([efb6d1f](https://github.com/folke/snacks.nvim/commit/efb6d1f8b8057e5f8455452c47ab769358902a18))\n* **profiler:** added support for `Snacks.profiler` and dropped support for fzf-lua / telescope. Closes [#695](https://github.com/folke/snacks.nvim/issues/695) ([ada83de](https://github.com/folke/snacks.nvim/commit/ada83de9528d0825928726a6d252fb97271fb73a))\n\n\n### Bug Fixes\n\n* **picker.actions:** `checktime` after `git_checkout` ([b86d90e](https://github.com/folke/snacks.nvim/commit/b86d90e3e9c68f4d24a0208e873d35b0074c12b0))\n* **picker.async:** better handling of abort and schedule/defer util function ([dfcf27e](https://github.com/folke/snacks.nvim/commit/dfcf27e2a90d4b262d2bd0e54c1b576dba296c73))\n* **picker.async:** fixed aborting a coroutine from the coroutine itself. See [#665](https://github.com/folke/snacks.nvim/issues/665) ([c1e2c61](https://github.com/folke/snacks.nvim/commit/c1e2c619b229a3f2ccdc000a6fadddc7ca9f482d))\n* **picker.files:** include symlinks ([dc9c6fb](https://github.com/folke/snacks.nvim/commit/dc9c6fbd028e0488a9292e030c788b72b16cbeca))\n* **picker.frecency:** track visit on BufWinEnter instead of BufReadPost and exclude floating windows ([024a448](https://github.com/folke/snacks.nvim/commit/024a448e52563aadf9e5b234ddfb17168aa5ada7))\n* **picker.git_branches:** handle detached HEAD ([#671](https://github.com/folke/snacks.nvim/issues/671)) ([390f687](https://github.com/folke/snacks.nvim/commit/390f6874318addcf48b668f900ef62d316c44602))\n* **picker.git:** `--follow` only works for `git_log_file`. Closes [#666](https://github.com/folke/snacks.nvim/issues/666) ([23a8668](https://github.com/folke/snacks.nvim/commit/23a8668ef0b0c9d910c7bbcd57651d8889b0fa65))\n* **picker.git:** parse all detached states. See [#671](https://github.com/folke/snacks.nvim/issues/671) ([2cac667](https://github.com/folke/snacks.nvim/commit/2cac6678a95f89a7e23ed668c9634eff9e60dbe5))\n* **picker.grep:** off-by-one for grep results col ([e3455ef](https://github.com/folke/snacks.nvim/commit/e3455ef4dc96fac3b53f76e12c487007a5fca9e7))\n* **picker.icons:** bump build for nerd fonts ([ba108e2](https://github.com/folke/snacks.nvim/commit/ba108e2aa86909168905e522342859ec9ed4e220))\n* **picker.icons:** fix typo in Nerd Fonts and display the full category name ([#716](https://github.com/folke/snacks.nvim/issues/716)) ([a4b0a85](https://github.com/folke/snacks.nvim/commit/a4b0a85e3bc68fe1aeca1ee4161053dabaeb856c))\n* **picker.icons:** opts.icons -&gt; opts.icon_sources. Fixes [#715](https://github.com/folke/snacks.nvim/issues/715) ([9e7bfc0](https://github.com/folke/snacks.nvim/commit/9e7bfc05d5e4a0f079f695cdd6869c219c762224))\n* **picker.input:** better handling of `stopinsert` with prompt buffers. Closes [#723](https://github.com/folke/snacks.nvim/issues/723) ([c2916cb](https://github.com/folke/snacks.nvim/commit/c2916cb526d42fb6726cf9f7252ceb44516fc2f6))\n* **picker.input:** correct cursor position in input when cycling / focus. Fixes [#688](https://github.com/folke/snacks.nvim/issues/688) ([93cca7a](https://github.com/folke/snacks.nvim/commit/93cca7a4b3923c291726305b301a51316b275bf2))\n* **picker.lsp:** include_current on Windows. Closes [#678](https://github.com/folke/snacks.nvim/issues/678) ([66d2854](https://github.com/folke/snacks.nvim/commit/66d2854ea0c83339042b6340b29dfdc48982e75a))\n* **picker.lsp:** make `lsp_symbols` work for unloaded buffers ([9db49b7](https://github.com/folke/snacks.nvim/commit/9db49b7e6c5ded7edeff8bec6327322fb6125695))\n* **picker.lsp:** schedule_wrap cancel functions and resume when no clients ([6cbca8a](https://github.com/folke/snacks.nvim/commit/6cbca8adffd4014e9f67ba327f9c164f0412b685))\n* **picker.lsp:** use async from ctx ([b878caa](https://github.com/folke/snacks.nvim/commit/b878caaddc7b91386ec95b3b2f034b275dc7f49a))\n* **picker.lsp:** use correct buf/win ([8006caa](https://github.com/folke/snacks.nvim/commit/8006caadb3eedf2553a587497c508c01aadf098b))\n* **picker.preview:** clear buftype for file previews ([5429dff](https://github.com/folke/snacks.nvim/commit/5429dff1cd51ceaa10134dbff4faf447823de017))\n* **picker.undo:** use new API. Closes [#707](https://github.com/folke/snacks.nvim/issues/707) ([79a6eab](https://github.com/folke/snacks.nvim/commit/79a6eabd318d2b65d5786c4e3c2419eaa91c6240))\n* **picker.util:** for `--` args require a space before ([ee6f21b](https://github.com/folke/snacks.nvim/commit/ee6f21bbd636e82691a0386f39f0c8310f8cadd8))\n* **picker.util:** more relaxed resolve_loc ([964beb1](https://github.com/folke/snacks.nvim/commit/964beb11489afc2a2a1004bbb1b2b3286da9a8ac))\n* **picker.util:** prevent empty shortened paths if it's the cwd. Fixes [#721](https://github.com/folke/snacks.nvim/issues/721) ([14f16ce](https://github.com/folke/snacks.nvim/commit/14f16ceb5d4dc53ddbd9b56992335658105d1d5f))\n* **picker:** better handling of buffers with custom URIs. Fixes [#677](https://github.com/folke/snacks.nvim/issues/677) ([cd5eddb](https://github.com/folke/snacks.nvim/commit/cd5eddb1dea0ab69a451702395104cf716678b36))\n* **picker:** don't jump to invalid positions. Fixes [#712](https://github.com/folke/snacks.nvim/issues/712) ([51adb67](https://github.com/folke/snacks.nvim/commit/51adb6792e1819c9cf0153606f506403f97647fe))\n* **picker:** don't try showing the preview when the picker is closed. Fixes [#714](https://github.com/folke/snacks.nvim/issues/714) ([11c0761](https://github.com/folke/snacks.nvim/commit/11c07610557f0a6c6eb40bca60c60982ff6e3b93))\n* **picker:** resume. Closes [#709](https://github.com/folke/snacks.nvim/issues/709) ([9b55a90](https://github.com/folke/snacks.nvim/commit/9b55a907bd0468752c3e5d9cd7e607cab206a6d7))\n* **picker:** starting a picker from the picker sometimes didnt start in insert mode. Fixes [#718](https://github.com/folke/snacks.nvim/issues/718) ([08d4f14](https://github.com/folke/snacks.nvim/commit/08d4f14cd85466fd37d63b7123437c7d15bc87f6))\n* **picker:** update title on find. Fixes [#717](https://github.com/folke/snacks.nvim/issues/717) ([431a24e](https://github.com/folke/snacks.nvim/commit/431a24e24e2a7066e44272f83410d7b44f497e26))\n* **scroll:** handle buffer changes while animating ([3da0b0e](https://github.com/folke/snacks.nvim/commit/3da0b0ec11dff6c88e68c91194688c9ff3513e86))\n* **win:** better way of finding a main window when fixbuf is `true` ([84ee7dd](https://github.com/folke/snacks.nvim/commit/84ee7ddf543aa1249ca4e29873200073e28f693f))\n* **zen:** parent buf. Fixes [#720](https://github.com/folke/snacks.nvim/issues/720) ([f314676](https://github.com/folke/snacks.nvim/commit/f31467637ac91406efba15981d53cd6da09718e0))\n\n\n### Performance Improvements\n\n* **picker.frecency:** cache all deadlines on load ([5b3625b](https://github.com/folke/snacks.nvim/commit/5b3625bcea5ed78e7cddbeb038159a0041110c71))\n* **picker:** gc optims ([3fa2ea3](https://github.com/folke/snacks.nvim/commit/3fa2ea3115c2e8203ec44025ff4be054c5f1e917))\n* **picker:** small optims ([ee76e9b](https://github.com/folke/snacks.nvim/commit/ee76e9ba674e6b67a3d687868f27751745e2baad))\n* **picker:** small optims for abort ([317a209](https://github.com/folke/snacks.nvim/commit/317a2093ea0cdd62a34f3a414e625f3313e5e2e8))\n* **picker:** use picker ref in progress updater. Fixes [#710](https://github.com/folke/snacks.nvim/issues/710) ([37f3888](https://github.com/folke/snacks.nvim/commit/37f3888dccc922e4044ee1713c25dba51b4290d2))\n\n## [2.14.0](https://github.com/folke/snacks.nvim/compare/v2.13.0...v2.14.0) (2025-01-20)\n\n\n### Features\n\n* **picker.buffer:** add filetype to bufname for buffers without name ([83baea0](https://github.com/folke/snacks.nvim/commit/83baea06d65d616f1f800501d0d82e4ad117abf2))\n* **picker.debug:** debug option to detect garbage collection leaks ([b59f4ff](https://github.com/folke/snacks.nvim/commit/b59f4ff477a18cdc3673a240c2e992a2bccd48fe))\n* **picker.matcher:** new `opts.matcher.file_pos` which defaults to `true` to support patterns like `file:line:col` or `file:line`. Closes [#517](https://github.com/folke/snacks.nvim/issues/517). Closes [#496](https://github.com/folke/snacks.nvim/issues/496). Closes [#651](https://github.com/folke/snacks.nvim/issues/651) ([5e00b0a](https://github.com/folke/snacks.nvim/commit/5e00b0ab271149f1bd74d5d5afe106b440f45a9d))\n* **picker:** added `args` option for `files` and `grep`. Closes [#621](https://github.com/folke/snacks.nvim/issues/621) ([781b6f6](https://github.com/folke/snacks.nvim/commit/781b6f6ab0cd5f65a685bf8bac284f4a12e43589))\n* **picker:** added `undo` picker to navigate the undo tree. Closes [#638](https://github.com/folke/snacks.nvim/issues/638) ([5c45f1c](https://github.com/folke/snacks.nvim/commit/5c45f1c193f2ed2fa639146df373f341d7410e8b))\n* **picker:** added support for item.resolve that gets called if needed during list rendering / preview ([b0d3266](https://github.com/folke/snacks.nvim/commit/b0d32669856b8ad9c75fa7c6c4b643566001c8bc))\n* **terminal:** allow overriding default shell. Closes [#450](https://github.com/folke/snacks.nvim/issues/450) ([3146fd1](https://github.com/folke/snacks.nvim/commit/3146fd139b89760526f32fd9d3ac4c91af010f0c))\n* **terminal:** close terminals on `ExitPre`. Fixes [#419](https://github.com/folke/snacks.nvim/issues/419) ([2abf208](https://github.com/folke/snacks.nvim/commit/2abf208f2c43a387ca6c55c33b5ebbc7869c189c))\n\n\n### Bug Fixes\n\n* **dashboard:** added optional filter for recent files ([32cd343](https://github.com/folke/snacks.nvim/commit/32cd34383c8ac5d0e43408aba559308546555962))\n* **debug.run:** schedule only nvim_buf_set_extmark in on_print ([#425](https://github.com/folke/snacks.nvim/issues/425)) ([81572b5](https://github.com/folke/snacks.nvim/commit/81572b5f97c3cb2f2e254972762f4b816e790fde))\n* **indent:** use correct hl based on indent. Fixes [#422](https://github.com/folke/snacks.nvim/issues/422) ([627af73](https://github.com/folke/snacks.nvim/commit/627af7342cf5bea06793606c33992d2cc882655b))\n* **input:** put the cursor right after the default prompt ([#549](https://github.com/folke/snacks.nvim/issues/549)) ([f904481](https://github.com/folke/snacks.nvim/commit/f904481439706e678e93225372b30f97281cfc2c))\n* **notifier:** added `SnacksNotifierMinimal`. Closes [#410](https://github.com/folke/snacks.nvim/issues/410) ([daa575e](https://github.com/folke/snacks.nvim/commit/daa575e3cd42f003e171dbb8a3e992e670d5032c))\n* **notifier:** win:close instead of win:hide ([f29f7a4](https://github.com/folke/snacks.nvim/commit/f29f7a433a2d9ea95f43c163d57df2f647700115))\n* **picker.buffers:** add buf number to text ([70106a7](https://github.com/folke/snacks.nvim/commit/70106a79306525a281a3156bae1499f70c183d1d))\n* **picker.buffer:** unselect on delete. Fixes [#653](https://github.com/folke/snacks.nvim/issues/653) ([0ac5605](https://github.com/folke/snacks.nvim/commit/0ac5605bfbeb31cee4bb91a6ca7a2bfe8c4d468f))\n* **picker.grep:** correctly insert args from pattern. See [#601](https://github.com/folke/snacks.nvim/issues/601) ([8601a8c](https://github.com/folke/snacks.nvim/commit/8601a8ced398145d95f118737b29f3bd5f7eb700))\n* **picker.grep:** debug ([f0d51ce](https://github.com/folke/snacks.nvim/commit/f0d51ce03835815aba0a6d748b54c3277ff38b70))\n* **picker.lsp.symbols:** only include filename for search with workspace symbols ([eb0e5b7](https://github.com/folke/snacks.nvim/commit/eb0e5b7efe603bea7a0823ffaed13c52b395d04b))\n* **picker.lsp:** backward compat with Neovim 0.95 ([3df2408](https://github.com/folke/snacks.nvim/commit/3df2408713efdbc72f9a8fcedc8586aed50ab023))\n* **picker.lsp:** lazy resolve item lsp locations. Fixes [#650](https://github.com/folke/snacks.nvim/issues/650) ([d0a0046](https://github.com/folke/snacks.nvim/commit/d0a0046e37d274d8acdfcde653f3cadb12be6ba1))\n* **picker.preview:** disable relativenumber by default. Closes [#664](https://github.com/folke/snacks.nvim/issues/664) ([384b9a7](https://github.com/folke/snacks.nvim/commit/384b9a7a36b5e30959fd89d3d857855105f65611))\n* **picker.preview:** off-by-one for cmd output ([da5556a](https://github.com/folke/snacks.nvim/commit/da5556aa6bceb3428700607ab3005e5b44cb8b2e))\n* **picker.preview:** reset before notify ([e50f2e3](https://github.com/folke/snacks.nvim/commit/e50f2e39094d4511506329044713ac69541f4135))\n* **picker.undo:** disable number and signcolumn in preview ([40cea79](https://github.com/folke/snacks.nvim/commit/40cea79697acc97c3e4f814ca99a2d261fd6a4ee))\n* **picker.util:** item.resolve for nil item ([2ff21b4](https://github.com/folke/snacks.nvim/commit/2ff21b4394d1f34887cb3425e32f18a793b749c7))\n* **picker.util:** relax pattern for args ([6b7705c](https://github.com/folke/snacks.nvim/commit/6b7705c7edc9b93f16179d1343f9b2ae062340f9))\n* **scope:** parse treesitter injections. Closes [#430](https://github.com/folke/snacks.nvim/issues/430) ([985ada3](https://github.com/folke/snacks.nvim/commit/985ada3c14346cc6df6a6013564a6541c66f6ce9))\n* **statusline:** fix status line cache key ([#656](https://github.com/folke/snacks.nvim/issues/656)) ([af55934](https://github.com/folke/snacks.nvim/commit/af559349e591afaaaf75a8b3ecf5ee6f6711dde0))\n* **win:** always close created scratch buffers when win closes ([abd7e61](https://github.com/folke/snacks.nvim/commit/abd7e61b7395af10a7862cec5bc746253a3b7917))\n* **zen:** properly handle close ([920a9d2](https://github.com/folke/snacks.nvim/commit/920a9d28f1b1bf5ca06755236f9bbb8853adfea8))\n* **zen:** sync cursor with parent window ([#547](https://github.com/folke/snacks.nvim/issues/547)) ([ba45c28](https://github.com/folke/snacks.nvim/commit/ba45c280dd9a35a6441a89d830b72f7cc8849b74)), closes [#539](https://github.com/folke/snacks.nvim/issues/539)\n\n\n### Performance Improvements\n\n* **picker:** fixed some issues with closed pickers not always being garbage collected ([eebf44a](https://github.com/folke/snacks.nvim/commit/eebf44a34e9e004f988437116140712834efd745))\n\n## [2.13.0](https://github.com/folke/snacks.nvim/compare/v2.12.0...v2.13.0) (2025-01-19)\n\n\n### Features\n\n* **picker.actions:** added support for action options. Fixes [#598](https://github.com/folke/snacks.nvim/issues/598) ([8035398](https://github.com/folke/snacks.nvim/commit/8035398e523588df7eac928fd23e6692522f6e1e))\n* **picker.buffers:** del buffer with ctrl+x ([2479ff7](https://github.com/folke/snacks.nvim/commit/2479ff7cf41392130bd660fb787e3b1730863657))\n* **picker.buffers:** delete buffers with dd ([2ab18a0](https://github.com/folke/snacks.nvim/commit/2ab18a0b9f425ccbc697adc53a01b26ea38abe0d))\n* **picker.commands:** added builtin commands. Fixes [#634](https://github.com/folke/snacks.nvim/issues/634) ([ee988fa](https://github.com/folke/snacks.nvim/commit/ee988fa4b018ae617a16e2a4078b4586f08364f2))\n* **picker.frecency:** cleanup old entries from sqlite3 database ([320a4a6](https://github.com/folke/snacks.nvim/commit/320a4a62a159f9d3177251e21d81cb96156291b9))\n* **picker.git:** added `git_diff` picker for diff hunks ([#519](https://github.com/folke/snacks.nvim/issues/519)) ([cc69043](https://github.com/folke/snacks.nvim/commit/cc690436892d6ab0b8d5ee51ad60ff91c3a5d640))\n* **picker.git:** git diff/show can now use native or neovim for preview. defaults to neovim. Closes [#500](https://github.com/folke/snacks.nvim/issues/500). Closes [#494](https://github.com/folke/snacks.nvim/issues/494). Closes [#491](https://github.com/folke/snacks.nvim/issues/491). Closes [#478](https://github.com/folke/snacks.nvim/issues/478) ([e36e6af](https://github.com/folke/snacks.nvim/commit/e36e6af96cb2ac0574199ab9d229735fb6d9f016))\n* **picker.git:** stage/unstage files in git status with `&lt;tab&gt;` key ([0892db4](https://github.com/folke/snacks.nvim/commit/0892db4f42fc538df0a0b8fd66600d1e2d41b9e4))\n* **picker.grep:** added `ft` (rg's `type`) and `regex` (rg's `--fixed-strings`) options ([0437cfd](https://github.com/folke/snacks.nvim/commit/0437cfd98ea9767836685ef8f100b7a758239624))\n* **picker.list:** added debug option to show scores ([821e231](https://github.com/folke/snacks.nvim/commit/821e23101fdfcc28819e27596177eaa64eebf0c2))\n* **picker.list:** added select_all action mapped to ctrl+a ([c9e2695](https://github.com/folke/snacks.nvim/commit/c9e2695969687285fbf53c86336b75c4dae3b609))\n* **picker.list:** better way of highlighting field patterns ([924a988](https://github.com/folke/snacks.nvim/commit/924a988d9af72bf1abba122fa9f02a4eb917f15a))\n* **picker.list:** make `conceallevel` configurable. Fixes [#635](https://github.com/folke/snacks.nvim/issues/635) ([d88eab6](https://github.com/folke/snacks.nvim/commit/d88eab6e3fec20e162f52e618114b869f561e3fd))\n* **picker.lsp:** added `lsp_workspace_symbols`. Supports live search. Closes [#473](https://github.com/folke/snacks.nvim/issues/473) ([348307a](https://github.com/folke/snacks.nvim/commit/348307a82e4ae139fcb02b4cd4ae95dbf46f32c7))\n* **picker.matcher:** added opts.matcher.sort_empty and opts.matcher.filename_bonus ([ed91078](https://github.com/folke/snacks.nvim/commit/ed91078625996106ddd31dfb4bac634d2895f61f))\n* **picker.matcher:** better scoring algorithm based on fzf. Closes [#512](https://github.com/folke/snacks.nvim/issues/512). Fixes [#513](https://github.com/folke/snacks.nvim/issues/513) ([e4e2e88](https://github.com/folke/snacks.nvim/commit/e4e2e88c769d54094194d6e3d68fbfae87b20cbe))\n* **picker.matcher:** integrate custom item scores ([7267e24](https://github.com/folke/snacks.nvim/commit/7267e2493b5962a550d874f142aaf64c3873fb7e))\n* **picker.matcher:** moved length tiebreak to sorter instead ([d5ccb30](https://github.com/folke/snacks.nvim/commit/d5ccb301c1fe2adb874dd8f4f675797d983a8284))\n* **picker.recent:** include open files in recent files. Closes [#487](https://github.com/folke/snacks.nvim/issues/487) ([96ffaba](https://github.com/folke/snacks.nvim/commit/96ffaba71bed87cf8bf75c6d945dedae3fa40af2))\n* **picker.score:** prioritize matches in filenames ([5cf5ec1](https://github.com/folke/snacks.nvim/commit/5cf5ec1a314b38d4e361f7f26cb6eb14febd4d69))\n* **picker.smart:** better frecency bonus ([74feefc](https://github.com/folke/snacks.nvim/commit/74feefc52284e2ebf93ad815ec5aaeec918d4dc2))\n* **picker.sort:** default sorter can now sort by len of a field ([6ae87d9](https://github.com/folke/snacks.nvim/commit/6ae87d9f62a17124db9283c789b1bd968a55a85a))\n* **picker.sources:** lines just sorts by score/idx. Smart sorts on empty ([be42182](https://github.com/folke/snacks.nvim/commit/be421822d5498ad962481b134e6272defff9118e))\n* **picker:** add qflist_all action to send all even when already sel… ([#600](https://github.com/folke/snacks.nvim/issues/600)) ([c7354d8](https://github.com/folke/snacks.nvim/commit/c7354d83486d60d0a965426fa920d341759b69c6))\n* **picker:** add some source aliases like the Telescope / FzfLua names ([5a83a8e](https://github.com/folke/snacks.nvim/commit/5a83a8e32885d6b923319cb8dc5ff1d1d97d0b10))\n* **picker:** added `{preview}` and `{flags}` title placeholders. Closes [#557](https://github.com/folke/snacks.nvim/issues/557), Closes [#540](https://github.com/folke/snacks.nvim/issues/540) ([2e70b7f](https://github.com/folke/snacks.nvim/commit/2e70b7f42364e50df25407ddbd897e157a44c526))\n* **picker:** added `git_branches` picker. Closes [#614](https://github.com/folke/snacks.nvim/issues/614) ([8563dfc](https://github.com/folke/snacks.nvim/commit/8563dfce682eeb260fa17e554b3e02de47e61f35))\n* **picker:** added `inspect` action mapped to `&lt;c-i&gt;`. Useful to see what search fields are available on an item. ([2ba165b](https://github.com/folke/snacks.nvim/commit/2ba165b826d31ab0ebeaaff26632efe7013042b6))\n* **picker:** added `smart` picker ([772f3e9](https://github.com/folke/snacks.nvim/commit/772f3e9b8970123db4050e9f7a5bdf2270575c6c))\n* **picker:** added exclude option for files and grep. Closes [#581](https://github.com/folke/snacks.nvim/issues/581) ([192fb31](https://github.com/folke/snacks.nvim/commit/192fb31c4beda9aa4ebbc8bad0abe59df8bdde85))\n* **picker:** added jump options jumplist(true for all), reuse_win & tagstack (true for lsp). Closes [#589](https://github.com/folke/snacks.nvim/issues/589). Closes [#568](https://github.com/folke/snacks.nvim/issues/568) ([84c3738](https://github.com/folke/snacks.nvim/commit/84c3738fb04fff83aba8e91c3af8ad9e74b089fd))\n* **picker:** added preliminary support for combining finder results. More info coming soon ([000db17](https://github.com/folke/snacks.nvim/commit/000db17bf9f8bd243bbe944c0ae7e162d8cad572))\n* **picker:** added spelling picker. Closes [#625](https://github.com/folke/snacks.nvim/issues/625) ([b170ced](https://github.com/folke/snacks.nvim/commit/b170ced527c03911a4658fb2df7139fa7040bcef))\n* **picker:** added support for live args for `grep` and `files`. Closes [#601](https://github.com/folke/snacks.nvim/issues/601) ([50f3c3e](https://github.com/folke/snacks.nvim/commit/50f3c3e5b1c52c223a2689b1b2828c1ddae9e866))\n* **picker:** added toggle/flag/action for `follow`. Closes [#633](https://github.com/folke/snacks.nvim/issues/633) ([aa53f6c](https://github.com/folke/snacks.nvim/commit/aa53f6c0799f1f6b80f6fb46472ec4773763f6b8))\n* **picker:** allow disabling file icons ([76fbf9e](https://github.com/folke/snacks.nvim/commit/76fbf9e8a85485abfe1c53d096c74faad3fa2a6b))\n* **picker:** allow setting a custom `opts.title`. Fixes [#620](https://github.com/folke/snacks.nvim/issues/620) ([6001fb2](https://github.com/folke/snacks.nvim/commit/6001fb2077306655afefba6593ec8b55e18abc39))\n* **picker:** custom icon for unselected entries ([#588](https://github.com/folke/snacks.nvim/issues/588)) ([6402687](https://github.com/folke/snacks.nvim/commit/64026877ad8dac658eb5056e0c56f66e17401bdb))\n* **picker:** restore cursor / topline on resume ([ca54948](https://github.com/folke/snacks.nvim/commit/ca54948f79917113dfcdf1c4ccaec573244a02aa))\n* **pickers.format:** added `opts.picker.formatters.file.filename_first` ([98562ae](https://github.com/folke/snacks.nvim/commit/98562ae6a112bf1d80a9bec7fb2849605234a9d5))\n* **picker:** use an sqlite3 database for frecency data when available ([c43969d](https://github.com/folke/snacks.nvim/commit/c43969dabd42e261c570f533c2f343f99a9d1f01))\n* **scroll:** faster animations for scroll repeats after delay. (replaces spamming handling) ([d494a9e](https://github.com/folke/snacks.nvim/commit/d494a9e66447e9ae22e40c374e2e7d9a24b64d93))\n* **snacks:** added `snacks.picker` ([#445](https://github.com/folke/snacks.nvim/issues/445)) ([559d6c6](https://github.com/folke/snacks.nvim/commit/559d6c6bf207e4e768a88e7f727ac12a87c768b7))\n* **toggle:** allow toggling global options. Fixes [#534](https://github.com/folke/snacks.nvim/issues/534) ([b50effc](https://github.com/folke/snacks.nvim/commit/b50effc96763f0b84473b68c733ef3eff8a14be5))\n* **win:** warn on duplicate keymaps that differ in case. See [#554](https://github.com/folke/snacks.nvim/issues/554) ([a71b7c0](https://github.com/folke/snacks.nvim/commit/a71b7c0d26b578ad2b758ad74139b2ddecf8c15f))\n\n\n### Bug Fixes\n\n* **animate:** never animate stopped animations ([197b0a9](https://github.com/folke/snacks.nvim/commit/197b0a9be93a6fa49b840fe159ce6373c7edcf98))\n* **bigfile:** check existence of NoMatchParen before executing ([#561](https://github.com/folke/snacks.nvim/issues/561)) ([9b8f57b](https://github.com/folke/snacks.nvim/commit/9b8f57b96f823b83848572bf3384754f8ab46217))\n* **config:** better vim.tbl_deep_extend that prevents issues with list-like tables. Fixes [#554](https://github.com/folke/snacks.nvim/issues/554) ([75eb16f](https://github.com/folke/snacks.nvim/commit/75eb16fd9c746bbd5e21992b6eb986d389dd246e))\n* **config:** dont exclude metatables ([2d4a0b5](https://github.com/folke/snacks.nvim/commit/2d4a0b594a69c535704c15fc41c74d18c5f4d08b))\n* **grep:** explicitely set `--no-hidden` because of the git filter ([ae2de9a](https://github.com/folke/snacks.nvim/commit/ae2de9aa8101dbff1ee1ab101d53e916f6e217dd))\n* **indent:** dont redraw when list/shiftwidth/listchars change. Triggered way too often. Fixes [#613](https://github.com/folke/snacks.nvim/issues/613). Closes [#627](https://github.com/folke/snacks.nvim/issues/627) ([d212e3c](https://github.com/folke/snacks.nvim/commit/d212e3c99090eec3211e84e526b9fbdd000e020c))\n* **input:** bring back `&lt;c-w&gt;`. Fixes [#426](https://github.com/folke/snacks.nvim/issues/426). Closes [#429](https://github.com/folke/snacks.nvim/issues/429) ([5affa72](https://github.com/folke/snacks.nvim/commit/5affa7214f621144526b9a7d93b83302fa6ea6f4))\n* **layout:** allow root with relative=cursor. Closes [#479](https://github.com/folke/snacks.nvim/issues/479) ([f06f14c](https://github.com/folke/snacks.nvim/commit/f06f14c4ae4d131eb5e15f4c49994f8debddff42))\n* **layout:** don't trigger events during re-layout on resize. Fixes [#552](https://github.com/folke/snacks.nvim/issues/552) ([d73a4a6](https://github.com/folke/snacks.nvim/commit/d73a4a64dfd203b9f3a4a9dedd76af398faa6652))\n* **layout:** open/update windows in order of the layout to make sure offsets are correct ([034d50d](https://github.com/folke/snacks.nvim/commit/034d50d44e98af433260292001a88ac54d2466b6))\n* **layout:** use eventignore when updating windows that are already visible to fix issues with synatx. Fixes [#552](https://github.com/folke/snacks.nvim/issues/552) ([f7d967c](https://github.com/folke/snacks.nvim/commit/f7d967c5157ee621168153502812a266c509bd97))\n* **lsp:** use treesitter highlights for LSP locations ([fc06a36](https://github.com/folke/snacks.nvim/commit/fc06a363b95312eba0f3335f1190c745d0e5ea26))\n* **notifier:** content width. Fixes [#631](https://github.com/folke/snacks.nvim/issues/631) ([0e27737](https://github.com/folke/snacks.nvim/commit/0e277379ea7d25c97d109d31da33abacf26da841))\n* **picker.actions:** added hack to make folds work. Fixes [#514](https://github.com/folke/snacks.nvim/issues/514) ([df1060f](https://github.com/folke/snacks.nvim/commit/df1060fa501d06758d588a341d5cdec650cbfc67))\n* **picker.actions:** close existing empty buffer if it's the current buffer ([0745505](https://github.com/folke/snacks.nvim/commit/0745505f2f43d2983867f48805bd4f700ad06c73))\n* **picker.actions:** full path for qflist and loclist actions ([3e39250](https://github.com/folke/snacks.nvim/commit/3e392507963f784a4d57708585f8e012f1b95768))\n* **picker.actions:** only delete empty buffer if it's not displayed in a window. Fixes [#566](https://github.com/folke/snacks.nvim/issues/566) ([b7ab888](https://github.com/folke/snacks.nvim/commit/b7ab888dd0c5bb0bafe9d01209f6a63320621b11))\n* **picker.actions:** return action result. Fixes [#612](https://github.com/folke/snacks.nvim/issues/612). See [#611](https://github.com/folke/snacks.nvim/issues/611) ([4a482be](https://github.com/folke/snacks.nvim/commit/4a482bea3c5cac7af66a7a3d5cee5f97fca6c9d8))\n* **picker.colorscheme:** nil check. Fixes [#575](https://github.com/folke/snacks.nvim/issues/575) ([de01907](https://github.com/folke/snacks.nvim/commit/de01907930bb125d1b67b4a1fb372f21d972f70b))\n* **picker.config:** allow merging list-like layouts with table layout options ([706b1ab](https://github.com/folke/snacks.nvim/commit/706b1abc1697ca050314dc667e0900d53cad8aa4))\n* **picker.config:** better config merging and tests ([9986b47](https://github.com/folke/snacks.nvim/commit/9986b47707bbe76cf3b901c3048e55b2ba2bb4a8))\n* **picker.config:** normalize keys before merging so you can override `&lt;c-s&gt;` with `<C-S>` ([afef949](https://github.com/folke/snacks.nvim/commit/afef949d88b6fa3dde8515b27066b132cfdb0a70))\n* **picker.db:** remove tests ([71f69e5](https://github.com/folke/snacks.nvim/commit/71f69e5e57f355f40251e274d45560af7d8dd365))\n* **picker.diagnostics:** sort on empty pattern. Fixes [#641](https://github.com/folke/snacks.nvim/issues/641) ([c6c76a6](https://github.com/folke/snacks.nvim/commit/c6c76a6aa338af47e2725cff35cc814fcd2ad1e7))\n* **picker.files:** ignore errors since it's not possible to know if the error isbecause of an incomplete pattern. Fixes [#551](https://github.com/folke/snacks.nvim/issues/551) ([4823f2d](https://github.com/folke/snacks.nvim/commit/4823f2da65a5e11c10242af5e0d1c977474288b3))\n* **picker.format:** filename ([a194bbc](https://github.com/folke/snacks.nvim/commit/a194bbc3747f73416ec2fd25cb39c233fcc7a656))\n* **picker.format:** use forward slashes for paths. Closes [#624](https://github.com/folke/snacks.nvim/issues/624) ([5f82ffd](https://github.com/folke/snacks.nvim/commit/5f82ffde0befbcbfbedb9b5066bf4f3a5d667495))\n* **picker.git:** git log file/line for a file not in cwd. Fixes [#616](https://github.com/folke/snacks.nvim/issues/616) ([9034319](https://github.com/folke/snacks.nvim/commit/903431903b0151f97f45087353ebe8fa1cb8ef80))\n* **picker.git:** git_file and git_line should only show diffs including the file. Fixes [#522](https://github.com/folke/snacks.nvim/issues/522) ([1481a90](https://github.com/folke/snacks.nvim/commit/1481a90affedb24291997f5ebde0637cafc1d20e))\n* **picker.git:** use Snacks.git.get_root instead vim.fs.root for backward compatibility ([a2fb70e](https://github.com/folke/snacks.nvim/commit/a2fb70e8ba2bb2ce5c60ed1ee7505d6f6d7be061))\n* **picker.highlight:** properly deal with multiline treesitter captures ([27b72ec](https://github.com/folke/snacks.nvim/commit/27b72ecd005743ecf5855cd3b430fce74bd4f2e3))\n* **picker.input:** don't set prompt interrupt, but use a `&lt;c-c&gt;` mapping instead that can be changed ([123f0d9](https://github.com/folke/snacks.nvim/commit/123f0d9e5d7be5b23ef9b28b9ddde403e4b2d061))\n* **picker.input:** leave insert mode when closing and before executing confirm. Fixes [#543](https://github.com/folke/snacks.nvim/issues/543) ([05eb22c](https://github.com/folke/snacks.nvim/commit/05eb22c4fbe09f9bed53a752301d5c2a8a060a4e))\n* **picker.input:** statuscolumn on resize / re-layout. Fixes [#643](https://github.com/folke/snacks.nvim/issues/643) ([4d8d844](https://github.com/folke/snacks.nvim/commit/4d8d844027a3ea04ac7ecb0ab6cd63e39e96d06f))\n* **picker.input:** strip newllines from pattern (mainly due to pasting in the input box) ([c6a9955](https://github.com/folke/snacks.nvim/commit/c6a9955516b686d1b6bd815e1df0c808baa60bd3))\n* **picker.input:** use `Snacks.util.wo` instead of `vim.wo`. Fixes [#643](https://github.com/folke/snacks.nvim/issues/643) ([d6284d5](https://github.com/folke/snacks.nvim/commit/d6284d51ff748f43c5431c35ffed7f7c02e51069))\n* **picker.list:** disable folds ([5582a84](https://github.com/folke/snacks.nvim/commit/5582a84020a1e11d9001660252cdee6a424ba159))\n* **picker.list:** include `search` filter for highlighting items (live search). See [#474](https://github.com/folke/snacks.nvim/issues/474) ([1693fbb](https://github.com/folke/snacks.nvim/commit/1693fbb0dcf1b82f2212a325379ebb0257d582e5))\n* **picker.list:** newlines in text. Fixes [#607](https://github.com/folke/snacks.nvim/issues/607). Closes [#580](https://github.com/folke/snacks.nvim/issues/580) ([c45a940](https://github.com/folke/snacks.nvim/commit/c45a94044b8e4c9f5deb420a7caa505281e1eed5))\n* **picker.list:** possible issue with window options being set in the wrong window ([f1b6c55](https://github.com/folke/snacks.nvim/commit/f1b6c55027c6e75940fcb40fa8ac5ab717de1647))\n* **picker.list:** scores debug ([9499b94](https://github.com/folke/snacks.nvim/commit/9499b944e79ff305769a59819c44a93911023fc7))\n* **picker.lsp:** added support for single location result ([79d27f1](https://github.com/folke/snacks.nvim/commit/79d27f19dc62c0978d2688c6fac9348f253ef007))\n* **picker.matcher:** initialize matcher with pattern from opts. Fixes [#596](https://github.com/folke/snacks.nvim/issues/596) ([c434eb8](https://github.com/folke/snacks.nvim/commit/c434eb89fe030672f4e518b4de1e506ffaf0e96e))\n* **picker.matcher:** inverse scores ([1816931](https://github.com/folke/snacks.nvim/commit/1816931aadb1fdcd3e08606d773d31f3d51fabcc))\n* **picker.minheap:** clear sorted on minheap clear. Fixes [#492](https://github.com/folke/snacks.nvim/issues/492) ([79bea58](https://github.com/folke/snacks.nvim/commit/79bea58b1ec92909d763c8b5788baf8da6c19a06))\n* **picker.preview:** don't show line numbers for preview commands ([a652214](https://github.com/folke/snacks.nvim/commit/a652214f52694233b5e27d374db0d51d2f7cb43d))\n* **picker.preview:** pattern to detect binary files was incorrect ([bbd1a08](https://github.com/folke/snacks.nvim/commit/bbd1a0885b3e89103a8a59f1f07d296f23c7d2ad))\n* **picker.preview:** scratch buffer filetype. Fixes [#595](https://github.com/folke/snacks.nvim/issues/595) ([ece76b3](https://github.com/folke/snacks.nvim/commit/ece76b333a9ff372959bf5204ab22f58215383c1))\n* **picker.proc:** correct offset for carriage returns. Fixes [#599](https://github.com/folke/snacks.nvim/issues/599) ([a01e0f5](https://github.com/folke/snacks.nvim/commit/a01e0f536815a0cc23443fae5ac10f7fdcb4f313))\n* **picker.qf:** better quickfix item list. Fixes [#562](https://github.com/folke/snacks.nvim/issues/562) ([cb84540](https://github.com/folke/snacks.nvim/commit/cb845408dbc795582ea1567fba2bf4ba64c777ac))\n* **picker.select:** allow main to be current. Fixes [#497](https://github.com/folke/snacks.nvim/issues/497) ([076259d](https://github.com/folke/snacks.nvim/commit/076259d263c927ed1d1c56c94b6e870230e77a3d))\n* **picker.util:** cleanup func for key-value store (frecency) ([bd2da45](https://github.com/folke/snacks.nvim/commit/bd2da45c384ea7ce44bdd15a7b5e32ee3806cf8d))\n* **picker:** add alias for `oldfiles` ([46554a6](https://github.com/folke/snacks.nvim/commit/46554a63425c0594eacaa0e8eaddec5dbf79b48e))\n* **picker:** add keymaps for preview scratch buffers ([dc3f114](https://github.com/folke/snacks.nvim/commit/dc3f114c1f787218e4314d907866874a62253756))\n* **picker:** always stopinsert, even when picker is already closed. Should not be needed, but some plugins misbehave. See  [#579](https://github.com/folke/snacks.nvim/issues/579) ([29becb0](https://github.com/folke/snacks.nvim/commit/29becb0ecbeb20683b3ae71d4c082734b2f518af))\n* **picker:** better buffer edit. Fixes [#593](https://github.com/folke/snacks.nvim/issues/593) ([716492c](https://github.com/folke/snacks.nvim/commit/716492c57870e11e04a5764bcc1e549859f563be))\n* **picker:** better normkey. Fixes [#610](https://github.com/folke/snacks.nvim/issues/610) ([540ecbd](https://github.com/folke/snacks.nvim/commit/540ecbd9a4b4c4d4ed47db83367f9e5d04220c27))\n* **picker:** changed inspect mapping to `&lt;a-d&gt;` since not all terminal differentiate between `<a-i>` and `<tab>` ([8386540](https://github.com/folke/snacks.nvim/commit/8386540c422774059a75fe26ce7cfb6ab3811c73))\n* **picker:** correctly normalize path after fnamemodify ([f351dcf](https://github.com/folke/snacks.nvim/commit/f351dcfcaca069d5f70bcf6edbde244e7358d063))\n* **picker:** deepcopy before config merging. Fixes [#554](https://github.com/folke/snacks.nvim/issues/554) ([7865df0](https://github.com/folke/snacks.nvim/commit/7865df0558fa24cce9ec27c4e002d5e179cab685))\n* **picker:** don't throttle preview if it's the first item we're previewing ([b785167](https://github.com/folke/snacks.nvim/commit/b785167814c5481643b53d241487fc9802a1ab13))\n* **picker:** dont fast path matcher when finder items have scores ([2ba5602](https://github.com/folke/snacks.nvim/commit/2ba5602834830e1a96e4f1a81e0fbb310481ca74))\n* **picker:** format: one too many spaces for default icon in results … ([#594](https://github.com/folke/snacks.nvim/issues/594)) ([d7f727f](https://github.com/folke/snacks.nvim/commit/d7f727f673a67b60882a3d2a96db4d1490566e73))\n* **picker:** picker:items() should return filtered items, not finder items. Closes [#481](https://github.com/folke/snacks.nvim/issues/481) ([d67e093](https://github.com/folke/snacks.nvim/commit/d67e093bbc2a2069c5e6118babf99c9029b34675))\n* **picker:** potential issue with preview winhl being set on the main window ([34208eb](https://github.com/folke/snacks.nvim/commit/34208ebe00a237a232a6050f81fb89f25d473180))\n* **picker:** preview / lsp / diagnostics positions were wrong; Should all be (1-0) indexed. Fixes [#543](https://github.com/folke/snacks.nvim/issues/543) ([40d08bd](https://github.com/folke/snacks.nvim/commit/40d08bd901db740f4b270fb6e9b710312a2846df))\n* **picker:** properly handle `opts.layout` being a string. Fixes [#636](https://github.com/folke/snacks.nvim/issues/636) ([b80c9d2](https://github.com/folke/snacks.nvim/commit/b80c9d275d05778c90df32bd361c8630b28d13fd))\n* **picker:** select_and_prev should use list_up instead of list_down ([#471](https://github.com/folke/snacks.nvim/issues/471)) ([b993be7](https://github.com/folke/snacks.nvim/commit/b993be762ba9aa9c1efd858ae777eef3de28c609))\n* **picker:** set correct cwd for git status picker ([#505](https://github.com/folke/snacks.nvim/issues/505)) ([2cc7cf4](https://github.com/folke/snacks.nvim/commit/2cc7cf42e94041c08f5654935f745424d10a15b1))\n* **picker:** show all files in git status ([#586](https://github.com/folke/snacks.nvim/issues/586)) ([43c312d](https://github.com/folke/snacks.nvim/commit/43c312dfc1433b3bdf24c7a14bc0aac648fa8d33))\n* **scope:** make sure to parse the ts tree. Fixes [#521](https://github.com/folke/snacks.nvim/issues/521) ([4c55f1c](https://github.com/folke/snacks.nvim/commit/4c55f1c2da103e4c2776274cb0f6fab0318ea811))\n* **scratch:** autowrite right buffer. Fixes [#449](https://github.com/folke/snacks.nvim/issues/449). ([#452](https://github.com/folke/snacks.nvim/issues/452)) ([8d2e26c](https://github.com/folke/snacks.nvim/commit/8d2e26cf27330ead517193f7eeb898d189a973c2))\n* **scroll:** don't animate for new changedtick. Fixes [#384](https://github.com/folke/snacks.nvim/issues/384) ([ac8b3cd](https://github.com/folke/snacks.nvim/commit/ac8b3cdfa08c2bba065c95b512887d3bbea91807))\n* **scroll:** don't animate when recording or executing macros ([7dcdcb0](https://github.com/folke/snacks.nvim/commit/7dcdcb0b6ab6ecb2c6efdbaa4769bc03f5837832))\n* **statuscolumn:** return \"\" when no signs and no numbers are needed. Closes [#570](https://github.com/folke/snacks.nvim/issues/570). ([c4980ef](https://github.com/folke/snacks.nvim/commit/c4980ef9b4e678d4057ece6fd2c13637a395f3a0))\n* **util:** normkey ([cd58a14](https://github.com/folke/snacks.nvim/commit/cd58a14e20fdcd810b55e8aee535486a3ad8719f))\n* **win:** clear syntax when setting filetype ([c49f38c](https://github.com/folke/snacks.nvim/commit/c49f38c5a919400689ab11669d45331f210ea91c))\n* **win:** correctly deal with initial text containing newlines. Fixes [#542](https://github.com/folke/snacks.nvim/issues/542) ([825c106](https://github.com/folke/snacks.nvim/commit/825c106f40352dc2979724643e0d36af3a962eb1))\n* **win:** duplicate keymap should take mode into account. Closes [#559](https://github.com/folke/snacks.nvim/issues/559) ([097e68f](https://github.com/folke/snacks.nvim/commit/097e68fc720a8fcdb370e6ccd2a3acad40b98952))\n* **win:** exclude cursor from redraw. Fixes [#613](https://github.com/folke/snacks.nvim/issues/613) ([ad9b382](https://github.com/folke/snacks.nvim/commit/ad9b382f7d0e150d2420cb65d44d6fb81a6b62c8))\n* **win:** fix relative=cursor again ([5b1cd46](https://github.com/folke/snacks.nvim/commit/5b1cd464e8759156d4d69f0398a0f5b34fa0b743))\n* **win:** relative=cursor. Closes [#427](https://github.com/folke/snacks.nvim/issues/427). Closes [#477](https://github.com/folke/snacks.nvim/issues/477) ([743f8b3](https://github.com/folke/snacks.nvim/commit/743f8b3fee780780d8e201960a4a295dc5187e9b))\n* **win:** special handling of `&lt;C-J&gt;`. Closes [#565](https://github.com/folke/snacks.nvim/issues/565). Closes [#592](https://github.com/folke/snacks.nvim/issues/592) ([5ac80f0](https://github.com/folke/snacks.nvim/commit/5ac80f0159239e44448fa9b08d8462e93342192d))\n* **win:** win position with border offsets. Closes [#413](https://github.com/folke/snacks.nvim/issues/413). Fixes [#423](https://github.com/folke/snacks.nvim/issues/423) ([ee08b1f](https://github.com/folke/snacks.nvim/commit/ee08b1f32e06904318f8fa24714557d3abcdd215))\n* **words:** added support for new name of the namespace used for lsp references. Fixes [#555](https://github.com/folke/snacks.nvim/issues/555) ([566f302](https://github.com/folke/snacks.nvim/commit/566f3029035143c525c0df7f5e7bbf9b3e939f54))\n\n\n### Performance Improvements\n\n* **notifier:** skip processing during search. See [#627](https://github.com/folke/snacks.nvim/issues/627) ([cf5f56a](https://github.com/folke/snacks.nvim/commit/cf5f56a1d82f4ece762843334c744e5830f4b4ef))\n* **picker.matcher:** fast path when we already found a perfect match ([6bbf50c](https://github.com/folke/snacks.nvim/commit/6bbf50c5e3a3ab3bfc6a6747f6a2c66cbc9b7548))\n* **picker.matcher:** only use filename_bonus for items that have a file field ([fd854ab](https://github.com/folke/snacks.nvim/commit/fd854ab9efdd13cf9cda192838f967551f332e36))\n* **picker.matcher:** yield every 1ms to prevent ui locking in large repos ([19979c8](https://github.com/folke/snacks.nvim/commit/19979c88f37930f71bd98b96e1afa50ae26a09ae))\n* **picker.util:** cache path calculation ([7117356](https://github.com/folke/snacks.nvim/commit/7117356b49ceedd7185c43a732dfe10c6a60cdbc))\n* **picker:** dont use prompt buffer callbacks ([8293add](https://github.com/folke/snacks.nvim/commit/8293add1e524f48aee1aacd68f72d4204096aed2))\n* **picker:** matcher optims ([5295741](https://github.com/folke/snacks.nvim/commit/529574128783c0fc4a146b207ea532950f48732f))\n\n## [2.12.0](https://github.com/folke/snacks.nvim/compare/v2.11.0...v2.12.0) (2025-01-05)\n\n\n### Features\n\n* **debug:** system & memory metrics useful for debugging ([cba16bd](https://github.com/folke/snacks.nvim/commit/cba16bdb35199c941c8d78b8fb9ddecf568c0b1f))\n* **input:** disable completion engines in input ([37038df](https://github.com/folke/snacks.nvim/commit/37038df00d6b47a65de24266c25683ff5a781a40))\n* **scope:** disable treesitter blocks by default ([8ec6e6a](https://github.com/folke/snacks.nvim/commit/8ec6e6adc5b098674c41005530d1c8af126480ae))\n* **statuscolumn:** allow changing the marks hl group. Fixes [#390](https://github.com/folke/snacks.nvim/issues/390) ([8a6e5b9](https://github.com/folke/snacks.nvim/commit/8a6e5b9685bb8c596a179256b048043a51a64e09))\n* **util:** `Snacks.util.ref` ([7383eda](https://github.com/folke/snacks.nvim/commit/7383edaec842609deac50b114a3567c2983b54f4))\n* **util:** throttle ([737980d](https://github.com/folke/snacks.nvim/commit/737980d987cdb4d3c2b18e0b3b8613fde974a2e9))\n* **win:** `Snacks.win:border_size` ([4cd0647](https://github.com/folke/snacks.nvim/commit/4cd0647eb5bda07431e125374c1419059783a741))\n* **win:** `Snacks.win:redraw` ([0711a82](https://github.com/folke/snacks.nvim/commit/0711a82b7a77c0ab35251e28cf1a7be0b3bde6d4))\n* **win:** `Snacks.win:scroll` ([a1da66e](https://github.com/folke/snacks.nvim/commit/a1da66e3bf2768273f1dfb556b29269fd8ba153d))\n* **win:** allow setting `desc` for window actions ([402494b](https://github.com/folke/snacks.nvim/commit/402494bdee8800c8ac3eeceb8c5e78e00f72f265))\n* **win:** better dimension calculation for windows (use by upcoming layouts) ([cc0b528](https://github.com/folke/snacks.nvim/commit/cc0b52872b99e3af7d80536e8a9cbc28d47e7f19))\n* **win:** top,right,bottom,left borders ([320ecbc](https://github.com/folke/snacks.nvim/commit/320ecbc15c25a240fee2c2970f826259d809ed72))\n\n\n### Bug Fixes\n\n* **dashboard:** hash dashboard terminal section caching key to support long commands ([#381](https://github.com/folke/snacks.nvim/issues/381)) ([d312053](https://github.com/folke/snacks.nvim/commit/d312053f78b4fb55523def179ac502438dd93193))\n* **debug:** make debug.inpect work in fast events ([b70edc2](https://github.com/folke/snacks.nvim/commit/b70edc29dbc8c9718af246a181b05d4d190ad260))\n* **debug:** make sure debug can be required in fast events ([6cbdbb9](https://github.com/folke/snacks.nvim/commit/6cbdbb9afa748e84af4c35d17fc4737b18638a35))\n* **indent:** allow rendering over blank lines. Fixes [#313](https://github.com/folke/snacks.nvim/issues/313) ([766e671](https://github.com/folke/snacks.nvim/commit/766e67145259e30ae7d63dfd6d51b8d8ef0840ae))\n* **indent:** better way to deal with `breakindent`. Fixes [#329](https://github.com/folke/snacks.nvim/issues/329) ([235427a](https://github.com/folke/snacks.nvim/commit/235427abcbf3e2b251a8b75f0e409dfbb6c737d6))\n* **indent:** breakdinent ([972c61c](https://github.com/folke/snacks.nvim/commit/972c61cc1cd254ef3b43ec1dfd51eefbdc441a7d))\n* **indent:** correct calculation of partial indent when leftcol &gt; 0 ([6f3cbf8](https://github.com/folke/snacks.nvim/commit/6f3cbf8ad328d181a694cdded344477e81cd094d))\n* **indent:** do animate check in bufcall ([c62e7a2](https://github.com/folke/snacks.nvim/commit/c62e7a2561351c9fe3a8e7e9fc8602f3b61abf53))\n* **indent:** don't render scopes in closed folds. Fixes [#352](https://github.com/folke/snacks.nvim/issues/352) ([94ec568](https://github.com/folke/snacks.nvim/commit/94ec5686a64218c9477de7761af4fd34dd4a665b))\n* **indent:** off-by-one for indent guide hl group ([551e644](https://github.com/folke/snacks.nvim/commit/551e644ca311d065b3a6882db900846c1e66e636))\n* **indent:** repeat_linbebreak only works on Neovim &gt;= 0.10. Fixes [#353](https://github.com/folke/snacks.nvim/issues/353) ([b93201b](https://github.com/folke/snacks.nvim/commit/b93201bdf36bd62b07daf7d40bc305998f9da52c))\n* **indent:** simplify indent guide logic and never overwrite blanks. Fixes [#334](https://github.com/folke/snacks.nvim/issues/334) ([282be8b](https://github.com/folke/snacks.nvim/commit/282be8bfa8e6f46d6994ff46638d1c155b90753f))\n* **indent:** typo for underline ([66cce2f](https://github.com/folke/snacks.nvim/commit/66cce2f512e11a961a8f187eac802acbf8725d05))\n* **indent:** use space instead of full blank for indent offset. See [#313](https://github.com/folke/snacks.nvim/issues/313) ([58081bc](https://github.com/folke/snacks.nvim/commit/58081bcecb31db8c6f12ad876c70786582a7f6a8))\n* **input:** change buftype to prompt. Fixes [#350](https://github.com/folke/snacks.nvim/issues/350) ([2990bf0](https://github.com/folke/snacks.nvim/commit/2990bf0c7a79f5780a0268a47bae69ef004cec99))\n* **input:** make sure to show input window with a higher zindex of the parent window (if float) ([3123e6e](https://github.com/folke/snacks.nvim/commit/3123e6e9882f178411ea6e9fbf5e9552134b82b0))\n* **input:** refactor for win changes and ensure modified=false. Fixes [#403](https://github.com/folke/snacks.nvim/issues/403). Fixes [#402](https://github.com/folke/snacks.nvim/issues/402) ([8930630](https://github.com/folke/snacks.nvim/commit/89306308f357e12510683758c35d08f368db2b2c))\n* **input:** use correct highlight group for input prompt ([#328](https://github.com/folke/snacks.nvim/issues/328)) ([818da33](https://github.com/folke/snacks.nvim/commit/818da334ac8f655235b5861bb50577921e4e6bd8))\n* **lazygit:** enable boolean values in config ([#377](https://github.com/folke/snacks.nvim/issues/377)) ([ec34684](https://github.com/folke/snacks.nvim/commit/ec346843e0adb51b45e595dd0ef34bf9e64d4627))\n* **notifier:** open history window with correct style ([#307](https://github.com/folke/snacks.nvim/issues/307)) ([d2b5680](https://github.com/folke/snacks.nvim/commit/d2b5680359ee8feb34b095fd574b4f9b3f013629))\n* **notifier:** rename style `notification.history` -&gt; `notification_history` ([fd9ef30](https://github.com/folke/snacks.nvim/commit/fd9ef30206185e3dd4d3294c74e2fd0dee9722d1))\n* **scope:** allow treesitter scopes when treesitter highlighting is disabled. See [#231](https://github.com/folke/snacks.nvim/issues/231) ([58ae580](https://github.com/folke/snacks.nvim/commit/58ae580c2c12275755bb3e2003aebd06d550f2db))\n* **scope:** don't expand to invalid range. Fixes [#339](https://github.com/folke/snacks.nvim/issues/339) ([1244305](https://github.com/folke/snacks.nvim/commit/1244305bedb8e60a946d949c78453263a714a4ad))\n* **scope:** properly caluclate start indent when `cursor=true` for indent scopes. See [#5068](https://github.com/folke/snacks.nvim/issues/5068) ([e63fa7b](https://github.com/folke/snacks.nvim/commit/e63fa7bf05d22f4306c5fff594d48bc01e382238))\n* **scope:** use virtcol for calculating scopes at the cursor ([6a36f32](https://github.com/folke/snacks.nvim/commit/6a36f32eaa7d5d59e681b7b8112a85a58a2d563d))\n* **scroll:** check for invalid window. Fixes [#340](https://github.com/folke/snacks.nvim/issues/340) ([b6032e8](https://github.com/folke/snacks.nvim/commit/b6032e8f1b5cba55b5a2cf138ab4f172c4decfbd))\n* **scroll:** don't animate when leaving cmdline search with incsearch enabled. Fixes [#331](https://github.com/folke/snacks.nvim/issues/331) ([fc0a99b](https://github.com/folke/snacks.nvim/commit/fc0a99b8493c34e6a930b3571ee8491e23831bca))\n* **util:** throttle now autonatically schedules when in fast event ([9840331](https://github.com/folke/snacks.nvim/commit/98403313c749e26e5ae9a8ff51343c97f76ce170))\n* **win:** backdrop having bright cell at top right ([#400](https://github.com/folke/snacks.nvim/issues/400)) ([373d0f9](https://github.com/folke/snacks.nvim/commit/373d0f9b6d6d83cdba641937b6303b0a0a18119f))\n* **win:** don't enter when focusable is `false` ([ca233c7](https://github.com/folke/snacks.nvim/commit/ca233c7448c930658e8c7da9745e8d98884c3852))\n* **win:** force-close any buffer that is not a file ([dd50e53](https://github.com/folke/snacks.nvim/commit/dd50e53a9efea11329e21c4a61ca35ae5122ceca))\n* **win:** unset `winblend` when transparent ([0617e28](https://github.com/folke/snacks.nvim/commit/0617e28f8289002310fed5986acc29fde38e01b5))\n* **words:** only check modes for `is_enabled` when needed ([80dcb88](https://github.com/folke/snacks.nvim/commit/80dcb88ede1a96f79edd3b7ede0bc41d51dd8a2d))\n* **zen:** set zindex to 40, lower than hover (45). Closes [#345](https://github.com/folke/snacks.nvim/issues/345) ([05f4981](https://github.com/folke/snacks.nvim/commit/05f49814f3a2f3ecb83d9e72b7f8f2af40351aad))\n\n## [2.11.0](https://github.com/folke/snacks.nvim/compare/v2.10.0...v2.11.0) (2024-12-15)\n\n\n### Features\n\n* **indent:** properly handle continuation indents. Closes [#286](https://github.com/folke/snacks.nvim/issues/286) ([f2bb7fa](https://github.com/folke/snacks.nvim/commit/f2bb7fa94e4b9b1fa7f84066bbedea8b3d9875e3))\n* **input:** allow configuring position of prompt and icon ([d0cb707](https://github.com/folke/snacks.nvim/commit/d0cb7070e98d6a2ca31d94dd04d7048c9b258f33))\n* **notifier:** notification `history` option ([#297](https://github.com/folke/snacks.nvim/issues/297)) ([8f56e19](https://github.com/folke/snacks.nvim/commit/8f56e19f916f8075e2bfb534d723e3d850e256a4))\n* **scope:** `Scope:inner` for indent based and treesitter scopes ([8a8b1c9](https://github.com/folke/snacks.nvim/commit/8a8b1c976fc2736a3b91697750074fd3b23a24c9))\n* **scope:** added `__tostring` for debugging ([94e0849](https://github.com/folke/snacks.nvim/commit/94e0849c3aae3b818cad2804c256c57318256c72))\n* **scope:** added `opts.cursor` to take cursor column into account for scope detection. (defaults to true). Closes [#282](https://github.com/folke/snacks.nvim/issues/282) ([54bc6ba](https://github.com/folke/snacks.nvim/commit/54bc6bab2dbd07270c8c3fd447e8b72f825c315c))\n* **scope:** text objects now use treesitter scopes by default. See [#231](https://github.com/folke/snacks.nvim/issues/231) ([a953697](https://github.com/folke/snacks.nvim/commit/a9536973a9111c3c7b66fb51bc5f62be27850884))\n* **statuscolumn:** allow left/right to be a function. Closes [#288](https://github.com/folke/snacks.nvim/issues/288) ([cb42b95](https://github.com/folke/snacks.nvim/commit/cb42b952c5d4047f8e805c02c7aa596eb4e45ef2))\n* **util:** on_key handler ([002d5eb](https://github.com/folke/snacks.nvim/commit/002d5eb5c2710a4e7456dd572543369e8424fd64))\n* **win:** win:line() ([17494ad](https://github.com/folke/snacks.nvim/commit/17494ad9bf98e82c6a16f032cb3c9c82e072371a))\n\n\n### Bug Fixes\n\n* **dashboard:** telescope can't be run from a `vim.schedule` for some reason ([dcc5338](https://github.com/folke/snacks.nvim/commit/dcc5338e6f2a825b78791c96829d7e5a29e3ea5d))\n* **indent:** `opts.indent.blank` now defaults to `listchars.space`. Closes [#291](https://github.com/folke/snacks.nvim/issues/291) ([31bc409](https://github.com/folke/snacks.nvim/commit/31bc409342b00d406963de3e1f38f3a2f84cfdcb))\n* **indent:** fixup ([14d71c3](https://github.com/folke/snacks.nvim/commit/14d71c3fb2856634a8697f7c9f01704980e49bd0))\n* **indent:** honor lead listchar ([#303](https://github.com/folke/snacks.nvim/issues/303)) ([7db0cc9](https://github.com/folke/snacks.nvim/commit/7db0cc9281b23c71155422433b6f485675674932))\n* **indent:** honor listchars and list when blank is `nil`. Closes [#296](https://github.com/folke/snacks.nvim/issues/296) ([0e150f5](https://github.com/folke/snacks.nvim/commit/0e150f5510e381753ddd18f29facba14716d5669))\n* **indent:** lower priorities of indent guides ([7f66818](https://github.com/folke/snacks.nvim/commit/7f668185ea810304cef5cb166a51665d4859124b))\n* **input:** check if parent win still exists. Fixes [#287](https://github.com/folke/snacks.nvim/issues/287) ([db768a5](https://github.com/folke/snacks.nvim/commit/db768a5497301aad7fcddae2fe578cb320cc9ca2))\n* **input:** go back to insert mode if input was started from insert mode. Fixes [#287](https://github.com/folke/snacks.nvim/issues/287) ([5d00e6d](https://github.com/folke/snacks.nvim/commit/5d00e6dec5686d7d2a6d97288287892d117d579b))\n* **input:** missing padding if neither title nor icon positioned left ([#292](https://github.com/folke/snacks.nvim/issues/292)) ([97542a7](https://github.com/folke/snacks.nvim/commit/97542a7d9bfc7f242acbaa9851a62c649222fec8))\n* **input:** open input window with `noautocmd=true` set. Fixes [#287](https://github.com/folke/snacks.nvim/issues/287) ([26b7d4c](https://github.com/folke/snacks.nvim/commit/26b7d4cbd9f803d9f759fc00d0dc4caa0141048b))\n* **scope:** add `indent` to `__eq` ([be2779e](https://github.com/folke/snacks.nvim/commit/be2779e942bee0932e9c14ef4ed3e4002be861ce))\n* **scope:** better treesitter scope edge detection ([b7355c1](https://github.com/folke/snacks.nvim/commit/b7355c16fb441e33be993ade74130464b62304cf))\n* **scroll:** check mousescroll before spamming ([3d67bda](https://github.com/folke/snacks.nvim/commit/3d67bda1e29b8e8108dd74d611bf5c8b42883838))\n* **util:** on_key compat with Neovim 0.9 ([effa885](https://github.com/folke/snacks.nvim/commit/effa885120670ca8a1775fc16ab2ec9e8040c288))\n\n## [2.10.0](https://github.com/folke/snacks.nvim/compare/v2.9.0...v2.10.0) (2024-12-13)\n\n\n### Features\n\n* **animate:** add done to animation object ([ec73346](https://github.com/folke/snacks.nvim/commit/ec73346b7d4a25e440538141d5b8c68e42a1047d))\n* **lazygit:** respect existing LG_CONFIG_FILE when setting config paths ([#208](https://github.com/folke/snacks.nvim/issues/208)) ([ef114c0](https://github.com/folke/snacks.nvim/commit/ef114c0efede3339221df5bc5b250aa9f8328b8d))\n* **scroll:** added spamming detection and disable animations when user is spamming keys :) ([c58605f](https://github.com/folke/snacks.nvim/commit/c58605f8b3abf974e984ca5483cbe6ab9d2afc6e))\n* **scroll:** improve smooth scrolling when user is spamming keys ([5532ba0](https://github.com/folke/snacks.nvim/commit/5532ba07be1306eb05c727a27368a8311bae3eeb))\n* **zen:** added on_open / on_close callbacks ([5851de1](https://github.com/folke/snacks.nvim/commit/5851de157a08c96a0ca15580ced2ea53063fd65d))\n* **zen:** make zen/zoom mode work for floating windows. Closes [#5028](https://github.com/folke/snacks.nvim/issues/5028) ([05bb957](https://github.com/folke/snacks.nvim/commit/05bb95739a362f8bec382f164f07c53137244627))\n\n\n### Bug Fixes\n\n* **dashboard:** set cursor to non-hidden actionable items. Fixes [#273](https://github.com/folke/snacks.nvim/issues/273) ([7c7b18f](https://github.com/folke/snacks.nvim/commit/7c7b18fdeeb7e228463b41d805cb47327f3d03f1))\n* **indent:** fix rendering issues when `only_scope` is set for indent. Fixes [#268](https://github.com/folke/snacks.nvim/issues/268) ([370703d](https://github.com/folke/snacks.nvim/commit/370703da81a19db9d8bf41bb518e7b6959e53cea))\n* **indent:** only render adjusted top/bottom. See [#268](https://github.com/folke/snacks.nvim/issues/268) ([54294cb](https://github.com/folke/snacks.nvim/commit/54294cba6a17ec048a63837fa341e6a663f3217d))\n* **notifier:** set `modifiable=false` for notifier history ([12e68a3](https://github.com/folke/snacks.nvim/commit/12e68a33b5a1fd3648a7a558ef027fbb245125f7))\n* **scope:** change from/to selection to make more sense ([e8dd394](https://github.com/folke/snacks.nvim/commit/e8dd394c01699276e8f7214957625222c30c8e9e))\n* **scope:** possible loop? See [#278](https://github.com/folke/snacks.nvim/issues/278) ([ac6a748](https://github.com/folke/snacks.nvim/commit/ac6a74823b29cc1839df82fc839b81400ca80d45))\n* **scratch:** normalize filename ([5200a8b](https://github.com/folke/snacks.nvim/commit/5200a8baa59a96e73786c11192282d2d3e10deeb))\n* **scroll:** don't animate scroll distance 1 ([a986851](https://github.com/folke/snacks.nvim/commit/a986851a74512683c3331fa72a220751026fd611))\n\n## [2.9.0](https://github.com/folke/snacks.nvim/compare/v2.8.0...v2.9.0) (2024-12-12)\n\n\n### Features\n\n* **animate:** allow disabling all animations globally or per buffer ([25c290d](https://github.com/folke/snacks.nvim/commit/25c290d7c093f0c57473ffcccf56780b6d58dd37))\n* **animate:** allow toggling buffer-local / global animations with or without id ([50912dc](https://github.com/folke/snacks.nvim/commit/50912dc2fd926a49e3574d7029aed11fae3fb45b))\n* **dashboard:** add dashboard startuptime icon option ([#214](https://github.com/folke/snacks.nvim/issues/214)) ([63506d5](https://github.com/folke/snacks.nvim/commit/63506d5168d2bb7679026cab80df1adfe3cd98b8))\n* **indent:** animation styles `out`, `up_down`, `up`, `down` ([0a9b013](https://github.com/folke/snacks.nvim/commit/0a9b013ff13f6d1a550af2eac366c73d25e55e0a))\n* **indent:** don't animate indents when new scope overlaps with the previous one, taking white-space into account. See [#264](https://github.com/folke/snacks.nvim/issues/264) ([9b4a859](https://github.com/folke/snacks.nvim/commit/9b4a85905aaa114e04a9371585953e96b3095f93))\n* **indent:** move animate settings top-level, since they impact both scope and chunk ([baf8c18](https://github.com/folke/snacks.nvim/commit/baf8c180d9dda5797b4da538f7af122f4349f554))\n* **toggle:** added zoom toggle ([3367705](https://github.com/folke/snacks.nvim/commit/336770581348c137bc2cb3967cc2af90b2ff51a2))\n* **toggle:** return toggle after map ([4f22016](https://github.com/folke/snacks.nvim/commit/4f22016b4b765f3335ae7682fb5b3b79b414ecbd))\n* **util:** get var either from buffer or global ([4243912](https://github.com/folke/snacks.nvim/commit/42439123c4fbc088fbe0bdd636a6bdc501794491))\n\n\n### Bug Fixes\n\n* **indent:** make sure cursor line is in scope for the `out` style. Fixes [#264](https://github.com/folke/snacks.nvim/issues/264) ([39c009f](https://github.com/folke/snacks.nvim/commit/39c009fe0b45243ccbbd372e659dcbd9409a68df))\n* **indent:** when at edge of two blocks, prefer the one below. See [#231](https://github.com/folke/snacks.nvim/issues/231) ([2457d91](https://github.com/folke/snacks.nvim/commit/2457d913dc92835da12fc071b43bce3a03d31470))\n\n## [2.8.0](https://github.com/folke/snacks.nvim/compare/v2.7.0...v2.8.0) (2024-12-11)\n\n\n### Features\n\n* **animate:** added animate plugin ([971229e](https://github.com/folke/snacks.nvim/commit/971229e8a93dab7dc73fe379110cdb47a7fd1387))\n* **animate:** added animation context to callbacks ([1091280](https://github.com/folke/snacks.nvim/commit/109128087709fe5cba39e6983b8722b60cce8120))\n* **dim:** added dim plugin ([4dda551](https://github.com/folke/snacks.nvim/commit/4dda5516e88a64c2b387727662d4ecd645582c55))\n* **indent:** added indent plugin ([2c4021c](https://github.com/folke/snacks.nvim/commit/2c4021c4663ff4fe5da5b95c3e06a4f6eb416502))\n* **indent:** allow disabling indent guides. See [#230](https://github.com/folke/snacks.nvim/issues/230) ([4a4ad63](https://github.com/folke/snacks.nvim/commit/4a4ad633dc9f864532716af4387b5e035d57768c))\n* **indent:** allow disabling scope highlighting ([99207ee](https://github.com/folke/snacks.nvim/commit/99207ee44d3a2b4d14c915b08407bae708749235))\n* **indent:** optional rendering of scopes as chunks. Closes [#230](https://github.com/folke/snacks.nvim/issues/230) ([109a0d2](https://github.com/folke/snacks.nvim/commit/109a0d207eeee49e13a6640131b44383e31d0b0f))\n* **input:** added `input` snack ([70902ee](https://github.com/folke/snacks.nvim/commit/70902eee9e5aca7791450c6065dd51bed4651f24))\n* **profiler:** on_close can now be a function ([48a5879](https://github.com/folke/snacks.nvim/commit/48a58792a0dd2e3c9249cfa4b1df73a8ea86a290))\n* **scope:** added scope plugin ([63a279c](https://github.com/folke/snacks.nvim/commit/63a279c4e2e84ed02b5a2a6c2f84d68daf8f906a))\n* **scope:** fill the range for treesitter scopes ([38ed01b](https://github.com/folke/snacks.nvim/commit/38ed01b5a229fc0c41b07dde08e9119de9ff1c4e))\n* **scope:** text objects and jumping for scopes. Closes [#231](https://github.com/folke/snacks.nvim/issues/231) ([8faafb3](https://github.com/folke/snacks.nvim/commit/8faafb34831c48f3ff777d6bd0ced1c68c8ab82f))\n* **scroll:** added smooth scrolling plugin ([38a5ccc](https://github.com/folke/snacks.nvim/commit/38a5ccc3a6436ba67fba71f6a2a9693ee1c2f142))\n* **scroll:** allow disabling scroll globally or for some buffers ([04f15c1](https://github.com/folke/snacks.nvim/commit/04f15c1ba29afa6d1b085eb0d85a654c88be8fde))\n* **scroll:** use `on_key` to track mouse scrolling ([26c3e49](https://github.com/folke/snacks.nvim/commit/26c3e4960f37320bcd418ec18f859b0e24d1e7d8))\n* **scroll:** user virtual columns while scrolling ([fefa6fd](https://github.com/folke/snacks.nvim/commit/fefa6fd6920a2f8a6e717ae856c14e32d5d76ddb))\n* **snacks:** zen mode ([c509ea5](https://github.com/folke/snacks.nvim/commit/c509ea52b7b3487e3d904d9f3d55d20ad136facb))\n* **toggle:** add which-key mappings when which-key loads ([c9f494b](https://github.com/folke/snacks.nvim/commit/c9f494bd9a4729722186d2631ca91192ffc19b40))\n* **toggle:** add zen mode toggle ([#243](https://github.com/folke/snacks.nvim/issues/243)) ([9454ba3](https://github.com/folke/snacks.nvim/commit/9454ba35f8c6ad3baeda4132fe1e5c96a5850960))\n* **toggle:** added toggle for smooth scroll ([aeec09c](https://github.com/folke/snacks.nvim/commit/aeec09c5413c87df7ca827bd6b7c3fbf0f4d2909))\n* **toggle:** toggles for new plugins ([bddae83](https://github.com/folke/snacks.nvim/commit/bddae83141d9e18b23a5d7a9ccc52c76ad736ca2))\n* **util:** added Snacks.util.on_module to execute a callback when a module loads (or immediately when already loaded) ([f540b7b](https://github.com/folke/snacks.nvim/commit/f540b7b6cc10223adcbd6d9747155a076ddfa9a4))\n* **util:** set_hl no longer sets default=true when not specified ([d6309c6](https://github.com/folke/snacks.nvim/commit/d6309c62b8e5910407449975b9e333c2699d06d0))\n* **win:** added actions to easily combine actions in keymaps ([46362a5](https://github.com/folke/snacks.nvim/commit/46362a5a9c2583094bd0416dd6dea17996eaecf9))\n* **win:** allow configuring initial text to display in the buffer ([003ea8d](https://github.com/folke/snacks.nvim/commit/003ea8d6edcf6d813bfdc143ffe4fa6cc55c0ea5))\n* **win:** allow customizing backdrop window ([cdb495c](https://github.com/folke/snacks.nvim/commit/cdb495cb8f7b801d9d731cdfa2c6f92fadf1317d))\n* **win:** col/row can be negative calculated on height/end of parent ([bd49d2f](https://github.com/folke/snacks.nvim/commit/bd49d2f32e567cbe42adf0bd8582b7829de6c1dc))\n* **words:** added toggle for words ([bd7cf03](https://github.com/folke/snacks.nvim/commit/bd7cf038234a84b48d1c1f09dffae9e64910ff7e))\n* **zen:** `zz` when entering zen mode ([b5cb90f](https://github.com/folke/snacks.nvim/commit/b5cb90f91dedaa692c4da1dfa216d13e58ad219d))\n* **zen:** added zen plugin ([afb89ea](https://github.com/folke/snacks.nvim/commit/afb89ea159a20e1241656af5aa46f638327d2f5a))\n* **zen:** added zoom indicator ([8459e2a](https://github.com/folke/snacks.nvim/commit/8459e2adc090aaf59865a60836c360744d82ed0a))\n\n\n### Bug Fixes\n\n* **compat:** fixes for Neovim &lt; 0.10 ([33fbb30](https://github.com/folke/snacks.nvim/commit/33fbb309f8c21c8ec30b99fe323a5cc55c84c5bc))\n* **dashboard:** add filetype to terminal sections ([#215](https://github.com/folke/snacks.nvim/issues/215)) ([9c68a54](https://github.com/folke/snacks.nvim/commit/9c68a54af652ff69348848dad62c4cd901da59a0))\n* **dashboard:** don't open with startup option args ([#222](https://github.com/folke/snacks.nvim/issues/222)) ([6b78172](https://github.com/folke/snacks.nvim/commit/6b78172864ef94cd5f2ab184c0f98cf36f5a8e74))\n* **dashboard:** override foldmethod ([47ad2a7](https://github.com/folke/snacks.nvim/commit/47ad2a7bfa49c3eb5c20083de82a39f59fb8f17a))\n* **debug:** schedule wrap print ([3a107af](https://github.com/folke/snacks.nvim/commit/3a107afbf8dffabf6c2754750c51d740707b76af))\n* **dim:** check if win still exist when animating. Closes [#259](https://github.com/folke/snacks.nvim/issues/259) ([69018d0](https://github.com/folke/snacks.nvim/commit/69018d070c9a0db76abc0f34539287e1c181a5d4))\n* **health:** health checks ([72eba84](https://github.com/folke/snacks.nvim/commit/72eba841801928b00a1f1f74e1f976a31534a674))\n* **indent:** always align indents with shiftwidth ([1de6c15](https://github.com/folke/snacks.nvim/commit/1de6c152883b576524201a465e1b8a09622a6041))\n* **indent:** always render underline regardless of leftcol ([4e96e69](https://github.com/folke/snacks.nvim/commit/4e96e692e8a3c6c67f9c9b2971b6fa263461054b))\n* **indent:** always use scope hl to render underlines. Fixes [#234](https://github.com/folke/snacks.nvim/issues/234) ([8723945](https://github.com/folke/snacks.nvim/commit/8723945183aac31671d1fa27481c90dc7c665c02))\n* **indent:** better way of dealing with indents on blank lines. See [#246](https://github.com/folke/snacks.nvim/issues/246) ([c129683](https://github.com/folke/snacks.nvim/commit/c1296836f5c36e75b508aae9a76aa0931058feec))\n* **indent:** expand scopes to inlude end_pos based on the end_pos scope. See [#231](https://github.com/folke/snacks.nvim/issues/231) ([897f801](https://github.com/folke/snacks.nvim/commit/897f8019248009eca191185cf7094e04c9371a85))\n* **indent:** gradually increase scope when identical to visual selection for text objects ([bc7f96b](https://github.com/folke/snacks.nvim/commit/bc7f96bdee77368d4ddae2613823f085266529ab))\n* **indent:** properly deal with empty lines when highlighting scopes. Fixes [#246](https://github.com/folke/snacks.nvim/issues/246). Fixes [#245](https://github.com/folke/snacks.nvim/issues/245) ([d04cf1d](https://github.com/folke/snacks.nvim/commit/d04cf1dc4f332bade99c300283380bf0893f1996))\n* **indent:** set max_size=1 for textobjects and jumps by default. See [#231](https://github.com/folke/snacks.nvim/issues/231) ([5f217bc](https://github.com/folke/snacks.nvim/commit/5f217bca6adc88a5dc6aa55e5bf0580b95025a52))\n* **indent:** set shiftwidth to tabstop when 0 ([782b6ee](https://github.com/folke/snacks.nvim/commit/782b6ee3fca35ede62b8dba866a9ad5c50edfdce))\n* **indent:** underline. See [#234](https://github.com/folke/snacks.nvim/issues/234) ([51f9569](https://github.com/folke/snacks.nvim/commit/51f95693aedcde10e65c8121c6fd1293a3ac3819))\n* **indent:** use correct config options ([5352198](https://github.com/folke/snacks.nvim/commit/5352198b5a59968c871b962fa15f1d7ca4eb7b52))\n* **init:** enabled check ([519a45b](https://github.com/folke/snacks.nvim/commit/519a45bfe5df7fdf5aea0323e978e20eb52e15bc))\n* **init:** set input disabled by default. Fixes [#227](https://github.com/folke/snacks.nvim/issues/227) ([e9d0993](https://github.com/folke/snacks.nvim/commit/e9d099322fca1bb7b35e781230e4a7478ded86bf))\n* **input:** health check. Fixes [#239](https://github.com/folke/snacks.nvim/issues/239) ([acf743f](https://github.com/folke/snacks.nvim/commit/acf743fcfc4e0e42e1c9fe5c06f677849fa38e8b))\n* **input:** set current win before executing callback. Fixes [#257](https://github.com/folke/snacks.nvim/issues/257) ([c17c1b2](https://github.com/folke/snacks.nvim/commit/c17c1b2f6c99f0ed4f3ab8f00bf32bb39f9d0186))\n* **input:** set current win in `vim.schedule` so that it works properly from `expr` keymaps. Fixes [#257](https://github.com/folke/snacks.nvim/issues/257) ([8c2410c](https://github.com/folke/snacks.nvim/commit/8c2410c2de0a86c095177a831993f8eea78a63b6))\n* **input:** update window position in the context of the parent window to make sure position=cursor works as expected. Fixes [#254](https://github.com/folke/snacks.nvim/issues/254) ([6c27ff2](https://github.com/folke/snacks.nvim/commit/6c27ff2a365c961831c7620365dd87fa8d8ad633))\n* **input:** various minor visual fixes ([#252](https://github.com/folke/snacks.nvim/issues/252)) ([e01668c](https://github.com/folke/snacks.nvim/commit/e01668c36771c0c1424a2ce3ab26a09cbb43d472))\n* **notifier:** toggle show history. Fixes [#197](https://github.com/folke/snacks.nvim/issues/197) ([8b58b55](https://github.com/folke/snacks.nvim/commit/8b58b55e40221ca5124f156f47e46185310fbe1c))\n* **scope:** better edge detection for treesitter scopes ([6b02a09](https://github.com/folke/snacks.nvim/commit/6b02a09e5e81e4e38a42e0fcc2d7f0350404c228))\n* **scope:** return `nil` when buffer is empty for indent scope ([4aa378a](https://github.com/folke/snacks.nvim/commit/4aa378a35e8f3d3771410344525cc4bc9ac50e8a))\n* **scope:** take edges into account for min_size ([e2e6c86](https://github.com/folke/snacks.nvim/commit/e2e6c86d214029bfeae5d50929aee72f7059b7b7))\n* **scope:** typo for textobject ([0324125](https://github.com/folke/snacks.nvim/commit/0324125ca1e5a5e6810d28ea81bb2c7c0af1dc16))\n* **scroll:** better toggle ([3dcaad8](https://github.com/folke/snacks.nvim/commit/3dcaad8d0aacc1a736f75fee5719adbf80cbbfa2))\n* **scroll:** disable scroll by default for terminals ([7b5a78a](https://github.com/folke/snacks.nvim/commit/7b5a78a5c76cdf2c73abbeef47ac14bb8ccbee72))\n* **scroll:** don't animate invalid windows ([41ca13d](https://github.com/folke/snacks.nvim/commit/41ca13d119b328872ed1da9c8f458c5c24962d31))\n* **scroll:** don't bother setting cursor when scrolloff is larger than half of viewport. Fixes [#240](https://github.com/folke/snacks.nvim/issues/240) ([0ca9ca7](https://github.com/folke/snacks.nvim/commit/0ca9ca79926b3bf473172eee7072fa2838509fba))\n* **scroll:** move scrollbind check to M.check ([7211ec0](https://github.com/folke/snacks.nvim/commit/7211ec08ce01da754544f66e965effb13fd22fd3))\n* **scroll:** only animate the current window when scrollbind is active ([c9880ce](https://github.com/folke/snacks.nvim/commit/c9880ce872ca000d17ae8d62b10e913045f54735))\n* **scroll:** set cursor to correct position when target is reached. Fixes [#236](https://github.com/folke/snacks.nvim/issues/236) ([4209929](https://github.com/folke/snacks.nvim/commit/4209929e6d4f67d3d216d1ab5f52dc387c8e27c2))\n* **scroll:** use actual scrolling to perform the scroll to properly deal with folds etc. Fixes [#236](https://github.com/folke/snacks.nvim/issues/236) ([280a09e](https://github.com/folke/snacks.nvim/commit/280a09e4eef08157eface8398295eb7fa3f9a08d))\n* **win:** ensure win is set when relative=win ([5d472b8](https://github.com/folke/snacks.nvim/commit/5d472b833b7f925033fea164de0ab9e389e31bef))\n* **words:** incorrect enabled check. Fixes [#247](https://github.com/folke/snacks.nvim/issues/247) ([9c8f3d5](https://github.com/folke/snacks.nvim/commit/9c8f3d531874ebd20eebe259f5c30cea575a1bba))\n* **zen:** properly close existing zen window on toggle ([14da56e](https://github.com/folke/snacks.nvim/commit/14da56ee9791143ef2503816fb93f8bd2bf0b58d))\n* **zen:** return after closing. Fixes [#259](https://github.com/folke/snacks.nvim/issues/259) ([b13eaf6](https://github.com/folke/snacks.nvim/commit/b13eaf6bd9089d8832c79f4088e72affc449c8ee))\n* **zen:** when Normal is transparent, show an opaque transparent backdrop. Fixes [#235](https://github.com/folke/snacks.nvim/issues/235) ([d93de7a](https://github.com/folke/snacks.nvim/commit/d93de7af6916d2c734c9420624b8703236f386ff))\n\n\n### Performance Improvements\n\n* **animate:** check for animation easing updates ouside the main loop and schedule an update when needed ([03c0774](https://github.com/folke/snacks.nvim/commit/03c0774e8555a38267624309d4f00a30e351c4be))\n* **input:** lazy-load `vim.ui.input` ([614df63](https://github.com/folke/snacks.nvim/commit/614df63acfb5ce9b1ac174ea4f09e545a086af4d))\n* **util:** redraw helpers ([9fb88c6](https://github.com/folke/snacks.nvim/commit/9fb88c67b60cbd9d4a56f9aadcb9285929118518))\n\n## [2.7.0](https://github.com/folke/snacks.nvim/compare/v2.6.0...v2.7.0) (2024-12-07)\n\n\n### Features\n\n* **bigfile:** disable matchparen, set foldmethod=manual, set conceallevel=0 ([891648a](https://github.com/folke/snacks.nvim/commit/891648a483b6f5410ec9c8b74890d5a00b50fa4c))\n* **dashbard:** explude files from stdpath data/cache/state in recent files and projects ([b99bc64](https://github.com/folke/snacks.nvim/commit/b99bc64ef910cd075e4ab9cf0914e99e1a1d61c1))\n* **dashboard:** allow items to be hidden, but still create the keymaps etc ([7a47eb7](https://github.com/folke/snacks.nvim/commit/7a47eb76df2fd36bfcf3ed5c4da871542e1386be))\n* **dashboard:** allow terminal sections to have actions. Closes [#189](https://github.com/folke/snacks.nvim/issues/189) ([78f44f7](https://github.com/folke/snacks.nvim/commit/78f44f720b1b0609930581965f4f92649efae95b))\n* **dashboard:** hide title if section has no items. Fixes [#184](https://github.com/folke/snacks.nvim/issues/184) ([d370be6](https://github.com/folke/snacks.nvim/commit/d370be6d6966a298725901abad4bec90264859af))\n* **dashboard:** make buffer not listed ([#191](https://github.com/folke/snacks.nvim/issues/191)) ([42d6277](https://github.com/folke/snacks.nvim/commit/42d62775d82b7af4dbe001b04be6a8a6e461e8ec))\n* **dashboard:** when a section has a title, use that for action and key. If not put it in the section. Fixes [#189](https://github.com/folke/snacks.nvim/issues/189) ([045a17d](https://github.com/folke/snacks.nvim/commit/045a17da069aae6221b2b3eae8610c2aa5ca03ea))\n* **debug:** added `Snacks.debug.run()` to execute the buffer or selection with inlined print and errors ([e1fe4f5](https://github.com/folke/snacks.nvim/commit/e1fe4f5afed5c679fd2eed3486f17e8c0994b982))\n* **gitbrowse:** added `line_count`. See [#186](https://github.com/folke/snacks.nvim/issues/186) ([f03727c](https://github.com/folke/snacks.nvim/commit/f03727c77f739503fd297dd12a826c2aca3490f9))\n* **gitbrowse:** opts.notify ([a856952](https://github.com/folke/snacks.nvim/commit/a856952ab24757f4eaf4ae2e1728d097f1866681))\n* **gitbrowse:** url pattern can now also be a function ([0a48c2e](https://github.com/folke/snacks.nvim/commit/0a48c2e726e6ca90370260a05618f45345dbb66a))\n* **notifier:** reverse notif history by default for `show_history` ([5a50738](https://github.com/folke/snacks.nvim/commit/5a50738b8e952519658570f31ff5f24e06882f18))\n* **scratch:** `opts.ft` can now be a function and defaults to the ft of the current buffer or markdown ([652303e](https://github.com/folke/snacks.nvim/commit/652303e6de5fa746709979425f4bed763e3fcbfa))\n* **scratch:** change keymap to execute buffer/selection to `&lt;cr&gt;` ([7db0ed4](https://github.com/folke/snacks.nvim/commit/7db0ed4239a2f67c0ca288aaac21bc6aa65212a7))\n* **scratch:** use `Snacks.debug.run()` to execute buffer/selection ([32c46b4](https://github.com/folke/snacks.nvim/commit/32c46b4e2f61c026e41b3fa128d01b0e89da106c))\n* **snacks:** added `Snacks.profiler` ([8088799](https://github.com/folke/snacks.nvim/commit/808879951f960399844c89efef9aec1724f83402))\n* **snacks:** added new `scratch` snack ([1cec695](https://github.com/folke/snacks.nvim/commit/1cec695fefb6e42ee644cfaf282612c213009aed))\n* **toggle:** toggles for the profiler ([999ae07](https://github.com/folke/snacks.nvim/commit/999ae07808858df08d30eb099a8dbce401527008))\n* **util:** encode/decode a string to be used as a filename ([e6f6397](https://github.com/folke/snacks.nvim/commit/e6f63970de2225ad44ed08af7ffd8a0f37d8fc58))\n* **util:** simple function to get an icon ([7c29848](https://github.com/folke/snacks.nvim/commit/7c29848e89861b40e751cc15c557cf1e574acf66))\n* **win:** added `opts.fixbuf` to configure fixed window buffers ([1f74d1c](https://github.com/folke/snacks.nvim/commit/1f74d1ce77d2015e2802027c93b9e0bcd548e4d1))\n* **win:** backdrop can now be made opaque ([681b9c9](https://github.com/folke/snacks.nvim/commit/681b9c9d650e7b01a5e54567656f646fbd3b8d46))\n* **win:** width/height can now be a function ([964d7ae](https://github.com/folke/snacks.nvim/commit/964d7ae99af1f45949175e1494562a796e2ef99b))\n\n\n### Bug Fixes\n\n* **dashboard:** calculate proper offset when item has no text ([6e3b954](https://github.com/folke/snacks.nvim/commit/6e3b9546de4871a696652cfcee6768c39e7b8ee9))\n* **dashboard:** prevent possible duplicate recent files. Fixes [#171](https://github.com/folke/snacks.nvim/issues/171) ([93b254d](https://github.com/folke/snacks.nvim/commit/93b254d65845aa44ad1e01000c26e1faf5efb9a6))\n* **dashboard:** take hidden items into account when calculating padding. Fixes [#194](https://github.com/folke/snacks.nvim/issues/194) ([736ce44](https://github.com/folke/snacks.nvim/commit/736ce447e8815eb231271abf7d49d8fa7d96e225))\n* **dashboard:** take indent into account when calculating terminal width ([cda695e](https://github.com/folke/snacks.nvim/commit/cda695e53ffb34c7569dc3536134c9e432b2a1c1))\n* **dashboard:** truncate file names when too long. Fixes [#183](https://github.com/folke/snacks.nvim/issues/183) ([4bdf7da](https://github.com/folke/snacks.nvim/commit/4bdf7daece384ab8a5472e6effd5fd6167a5ce6a))\n* **debug:** better way of getting visual selection. See [#190](https://github.com/folke/snacks.nvim/issues/190) ([af41cb0](https://github.com/folke/snacks.nvim/commit/af41cb088d3ff8da9dede9fbdd5c81860bc37e64))\n* **gitbrowse:** opts.notify ([2436557](https://github.com/folke/snacks.nvim/commit/243655796e4adddf58ce581f1b86a283130ecf41))\n* **gitbrowse:** removed debug ([f894952](https://github.com/folke/snacks.nvim/commit/f8949523ed3f27f976e5346051a6658957d9492a))\n* **gitbrowse:** use line_start and line_end directly in patterns. Closes [#186](https://github.com/folke/snacks.nvim/issues/186) ([adf0433](https://github.com/folke/snacks.nvim/commit/adf0433e8fca3fbc7287ac7b05f01f10ac354283))\n* **profiler:** startup opts ([85f5132](https://github.com/folke/snacks.nvim/commit/85f51320b2662830a6435563668a04ab21686178))\n* **scratch:** always set filetype on the buffer. Fixes [#179](https://github.com/folke/snacks.nvim/issues/179) ([6db50cf](https://github.com/folke/snacks.nvim/commit/6db50cfe2d1b333512e8175d072a2f82e796433d))\n* **scratch:** floating window title/footer hl groups ([6c25ab1](https://github.com/folke/snacks.nvim/commit/6c25ab1108d12ef3642e97e3757710e69782cbd1))\n* **scratch:** make sure win.opts.keys exists. See [#190](https://github.com/folke/snacks.nvim/issues/190) ([50bd510](https://github.com/folke/snacks.nvim/commit/50bd5103ba0a15294b1a931fda14900e7fd10161))\n* **scratch:** sort keys. Fixes [#193](https://github.com/folke/snacks.nvim/issues/193) ([0df7a08](https://github.com/folke/snacks.nvim/commit/0df7a08b01b037e434efe7cd25e7d4608a282a92))\n* **scratch:** weirdness with visual selection and inclusive/exclusive. See [#190](https://github.com/folke/snacks.nvim/issues/190) ([f955f08](https://github.com/folke/snacks.nvim/commit/f955f082e09683d02df3fd0f8b621b938f38e6aa))\n* **statuscolumn:** add virtnum and relnum to cache key. See [#198](https://github.com/folke/snacks.nvim/issues/198) ([3647ca7](https://github.com/folke/snacks.nvim/commit/3647ca7d8a47362a01b539f5b6efc2d5339b5e8e))\n* **statuscolumn:** don't show signs on virtual ligns. See [#198](https://github.com/folke/snacks.nvim/issues/198) ([f5fb59c](https://github.com/folke/snacks.nvim/commit/f5fb59cc4c62e25745bbefd2ef55665a67196245))\n* **util:** better support for nvim-web-devicons ([ddaa2aa](https://github.com/folke/snacks.nvim/commit/ddaa2aaba59bbd05c03992bfb295c98ccd3b3e50))\n* **util:** make sure to always return an icon ([ca7188c](https://github.com/folke/snacks.nvim/commit/ca7188c531350fe313c211ad60a59d642749f93e))\n* **win:** allow resolving nil window option values. Fixes [#179](https://github.com/folke/snacks.nvim/issues/179) ([0043fa9](https://github.com/folke/snacks.nvim/commit/0043fa9ee142b63ab653507f2e6f45395e8a23d5))\n* **win:** don't force close modified buffers ([d517b11](https://github.com/folke/snacks.nvim/commit/d517b11cabf94bf833d020c7a0781122d0f48c06))\n* **win:** update opts.wo for padding instead of vim.wo directly ([446f502](https://github.com/folke/snacks.nvim/commit/446f50208fe823787ce60a8b216a622a4b6b63dd))\n* **win:** update window local options when the buffer changes ([630d96c](https://github.com/folke/snacks.nvim/commit/630d96cf1f0403352580f2d119fc3b3ba29e33a4))\n\n\n### Performance Improvements\n\n* **dashboard:** properly cleanup autocmds ([8e6d977](https://github.com/folke/snacks.nvim/commit/8e6d977ec985a1b3f12a53741df82881a7835f9a))\n* **statuscolumn:** optimize caching ([d972bc0](https://github.com/folke/snacks.nvim/commit/d972bc0a471fbf3067a115924a4add852d15f5f0))\n\n## [2.6.0](https://github.com/folke/snacks.nvim/compare/v2.5.0...v2.6.0) (2024-11-29)\n\n\n### Features\n\n* **config:** allow overriding resolved options for a plugin. See [#164](https://github.com/folke/snacks.nvim/issues/164) ([d730a13](https://github.com/folke/snacks.nvim/commit/d730a13b5519fa901114cbdd0b2d484067068cce))\n* **config:** make it easier to use examples in your config ([6e3cb7e](https://github.com/folke/snacks.nvim/commit/6e3cb7e53c0a1b314203d392dc1b7df8207a31a6))\n* **dashboard:** allow passing win=0, buf=0 to use for the dashboard instead of creating a new window ([417e07c](https://github.com/folke/snacks.nvim/commit/417e07c0d22173a0a50ed8207e913ba25b96088e))\n* **dashboard:** always render cache even when expired. Then refresh when needed. ([59f8f0d](https://github.com/folke/snacks.nvim/commit/59f8f0db99e7a2d4f6a181f02f3fe77355c016c8))\n* **gitbrowse:** add Bitbucket URL patterns ([#163](https://github.com/folke/snacks.nvim/issues/163)) ([53441c9](https://github.com/folke/snacks.nvim/commit/53441c97030dbc15b4a22d56e33054749a13750f))\n* **gitbrowse:** open commit when word is valid hash ([#161](https://github.com/folke/snacks.nvim/issues/161)) ([59c8eb3](https://github.com/folke/snacks.nvim/commit/59c8eb36ae6933dd54d87811fec22decfe4f303c))\n* **health:** check that snacks.nvim plugin spec is correctly setup ([2c7b4b7](https://github.com/folke/snacks.nvim/commit/2c7b4b7971c8b488cfc9949f402f6c0307e24fce))\n* **notifier:** added history opts.reverse ([bebd7e7](https://github.com/folke/snacks.nvim/commit/bebd7e70cdd336dcc582227c2f3bd6ea0cef60d9))\n* **win:** go back to the previous window, when closing a snacks window ([51996df](https://github.com/folke/snacks.nvim/commit/51996dfeac5f0936aa1196e90b28760eb028ac1a))\n\n\n### Bug Fixes\n\n* **config:** check correct var for single config result. Fixes [#167](https://github.com/folke/snacks.nvim/issues/167) ([45fd0ef](https://github.com/folke/snacks.nvim/commit/45fd0efe41a453f1fa54a0892d352b931d6f88bb))\n* **dashboard:** fixed mini.sessions.read. Fixes [#144](https://github.com/folke/snacks.nvim/issues/144) ([4e04b70](https://github.com/folke/snacks.nvim/commit/4e04b70ea3f6f91ae47e0fc7671e53e801171290))\n* **dashboard:** terminal commands get 5 seconds to complete to trigger caching ([f83a7b0](https://github.com/folke/snacks.nvim/commit/f83a7b0ffb13adfae55a464f4d99fe3d4b578fe6))\n* **git:** make git.get_root work for work-trees and cache git root checks. Closes [#136](https://github.com/folke/snacks.nvim/issues/136). Fixes [#115](https://github.com/folke/snacks.nvim/issues/115) ([9462273](https://github.com/folke/snacks.nvim/commit/9462273bf7c0e627da0f412c02daee907947078d))\n* **init:** use rawget when loading modules to prevent possible recursive loading with invalid module fields ([d0794dc](https://github.com/folke/snacks.nvim/commit/d0794dcf8e988cf70c8db705a6e65867ba3b6e30))\n* **notifier:** always show notifs directly when blocking ([0c7f7c5](https://github.com/folke/snacks.nvim/commit/0c7f7c5970d204d62488a4e351f1f1514a2a42e5))\n* **notifier:** gracefully handle E565 errors ([0bbc9e7](https://github.com/folke/snacks.nvim/commit/0bbc9e7ae65820bc5ee356e1321656a7106d409a))\n* **statuscolumn:** bad copy/paste!! Fixes [#152](https://github.com/folke/snacks.nvim/issues/152) ([7564a30](https://github.com/folke/snacks.nvim/commit/7564a30cad803c01f8ecc15683a280d2f0e9bdb7))\n* **statuscolumn:** never error (in case of E565 for example). Fixes [#150](https://github.com/folke/snacks.nvim/issues/150) ([cf84008](https://github.com/folke/snacks.nvim/commit/cf840087c5adf1c076b61fdd044ac960b31e4e1e))\n* **win:** handle E565 errors on close ([0b02044](https://github.com/folke/snacks.nvim/commit/0b020449ad8496c6bfd34e10bc69f807b52970f8))\n\n\n### Performance Improvements\n\n* **statuscolumn:** some small optims ([985be4a](https://github.com/folke/snacks.nvim/commit/985be4a759f6fe83e569679da431eeb7d2db5286))\n\n## [2.5.0](https://github.com/folke/snacks.nvim/compare/v2.4.0...v2.5.0) (2024-11-22)\n\n\n### Features\n\n* **dashboard:** added Snacks.dashboard.update(). Closes [#121](https://github.com/folke/snacks.nvim/issues/121) ([c770ebe](https://github.com/folke/snacks.nvim/commit/c770ebeaf7b19abad8a447ef55b48cec71e7db54))\n* **debug:** profile title ([0177079](https://github.com/folke/snacks.nvim/commit/017707955f465335900c4fd483c32df018fd3427))\n* **notifier:** show indicator when notif has more lines. Closes [#112](https://github.com/folke/snacks.nvim/issues/112) ([cf72c06](https://github.com/folke/snacks.nvim/commit/cf72c06ee61f3102bf828ee7e8dde20316310374))\n* **terminal:** added Snacks.terminal.get(). Closes [#122](https://github.com/folke/snacks.nvim/issues/122) ([7f63d4f](https://github.com/folke/snacks.nvim/commit/7f63d4fefb7ba22f6e98986f7adeb04f9f9369b1))\n* **util:** get hl color ([b0da066](https://github.com/folke/snacks.nvim/commit/b0da066536493b6ed977744e4ee91fac01fcc2a8))\n* **util:** set_hl managed ([9642695](https://github.com/folke/snacks.nvim/commit/96426953a029b12d02ad45849e086c1ee14e065b))\n\n\n### Bug Fixes\n\n* **dashboard:** `vim.pesc` for auto keys. Fixes [#134](https://github.com/folke/snacks.nvim/issues/134) ([aebffe5](https://github.com/folke/snacks.nvim/commit/aebffe535b09237b28d2c61bb3febab12bc95ae8))\n* **dashboard:** align should always set width even if no alignment is needed. Fixes [#137](https://github.com/folke/snacks.nvim/issues/137) ([54d521c](https://github.com/folke/snacks.nvim/commit/54d521cd0fde5e3ccf36716f23371707d0267768))\n* **dashboard:** better git check for advanced example. See [#126](https://github.com/folke/snacks.nvim/issues/126) ([b4a293a](https://github.com/folke/snacks.nvim/commit/b4a293aac747fbde7aafa72242a2d26dc17e325d))\n* **dashboard:** open fullscreen on relaunch ([853240b](https://github.com/folke/snacks.nvim/commit/853240bb207ed7a2366c6c63ffc38f3b26fd484f))\n* **dashboard:** randomseed needs argument on stable ([c359164](https://github.com/folke/snacks.nvim/commit/c359164872e82646e11c652fb0fbe723e58bfdd8))\n* **debug:** include `main` in caller ([33d31af](https://github.com/folke/snacks.nvim/commit/33d31af1501ec154dba6008064d17ab72ec37d00))\n* **git:** get_root should work for non file buffers ([723d8ea](https://github.com/folke/snacks.nvim/commit/723d8eac849749e9015d9e9598f99974684ca3bb))\n* **notifier:** hide existing nofif if higher prio notif arrives and no free space for lower notif ([7a061de](https://github.com/folke/snacks.nvim/commit/7a061de75f758db23cf2c2ee0822a76356b54035))\n* **quickfile:** don't load when bigfile detected. Fixes [#116](https://github.com/folke/snacks.nvim/issues/116) ([978424c](https://github.com/folke/snacks.nvim/commit/978424ce280ec85e78e9660b200aee9aa12e9ef2))\n* **sessions:** change persisted.nvim load session command ([#118](https://github.com/folke/snacks.nvim/issues/118)) ([26bec4b](https://github.com/folke/snacks.nvim/commit/26bec4b51d617cd275218e9935fdff5390c18a87))\n* **terminal:** hide on `q` instead of close ([30a0721](https://github.com/folke/snacks.nvim/commit/30a0721d56993a7125a247a07116f1a07e0efda4))\n\n## [2.4.0](https://github.com/folke/snacks.nvim/compare/v2.3.0...v2.4.0) (2024-11-19)\n\n\n### Features\n\n* **dashboard:** hide tabline and statusline when loading during startup ([75dc74c](https://github.com/folke/snacks.nvim/commit/75dc74c5dc933b81cde85e8bc368a384343af69f))\n* **dashboard:** when an item is wider than pane width and only one pane, then center it. See [#108](https://github.com/folke/snacks.nvim/issues/108) ([c15953e](https://github.com/folke/snacks.nvim/commit/c15953ee885cf8afb40dcd478569d7de2edae939))\n* **gitbrowse:** open also visual selection range ([#89](https://github.com/folke/snacks.nvim/issues/89)) ([c29c0d4](https://github.com/folke/snacks.nvim/commit/c29c0d48500cb976c9210bb2d42909ad203cd4aa))\n* **win:** detect alien buffers opening in managed windows and open them somewhere else. Fixes [#110](https://github.com/folke/snacks.nvim/issues/110) ([9c0d2e2](https://github.com/folke/snacks.nvim/commit/9c0d2e2e93615e70627e5c09c7bbb04e93eab2c6))\n\n\n### Bug Fixes\n\n* **dashboard:** always hide cursor ([68fcc25](https://github.com/folke/snacks.nvim/commit/68fcc258023404a0a0341a7cc93db47cd17f85f4))\n* **dashboard:** check session managers in order ([1acea8b](https://github.com/folke/snacks.nvim/commit/1acea8b94005620dad70dfde6a6344c130a57c59))\n* **dashboard:** fix race condition when sending data while closing ([4188446](https://github.com/folke/snacks.nvim/commit/4188446f86b5c6abae090eb6abca65d5d9bb8003))\n* **dashboard:** minimum one pane even when it doesn't fit the screen. Fixes [#104](https://github.com/folke/snacks.nvim/issues/104) ([be8feef](https://github.com/folke/snacks.nvim/commit/be8feef4ab584f50aaa96b69d50b3f86a35aacff))\n* **dashboard:** only check for piped stdin when in TUI. Ignore GUIs ([3311d75](https://github.com/folke/snacks.nvim/commit/3311d75f893191772a1b9525b18b94d8c3a8943a))\n* **dashboard:** remove weird preset.keys function override. Just copy defaults if you want to change them ([0b9e09c](https://github.com/folke/snacks.nvim/commit/0b9e09cbd97c178ebe5db78fd373448448c5511b))\n\n## [2.3.0](https://github.com/folke/snacks.nvim/compare/v2.2.0...v2.3.0) (2024-11-18)\n\n\n### Features\n\n* added dashboard health checks ([deb00d0](https://github.com/folke/snacks.nvim/commit/deb00d0ddc57d77f5f6c3e5510ba7c2f07e593eb))\n* **dashboard:** added support for mini.sessions ([c8e209e](https://github.com/folke/snacks.nvim/commit/c8e209e6be9e8d8cdee19842a99ae7b89ac4248d))\n* **dashboard:** allow opts.preset.keys to be a function with default keymaps as arg ([b7775ec](https://github.com/folke/snacks.nvim/commit/b7775ec879e14362d8e4082b7ed97a752bbb654a))\n* **dashboard:** automatically detect streaming commands and don't cache those. tty-clock, cmatrix galore. Fixes [#100](https://github.com/folke/snacks.nvim/issues/100) ([082beb5](https://github.com/folke/snacks.nvim/commit/082beb508ccf3584aebfee845c1271d9f8b8abb6))\n* **notifier:** timeout=0 keeps the notif visible till manually hidden. Closes [#102](https://github.com/folke/snacks.nvim/issues/102) ([0cf22a8](https://github.com/folke/snacks.nvim/commit/0cf22a8d87f28759c083969d67b55498e568a1b7))\n\n\n### Bug Fixes\n\n* **dashboard:** check uis for headless and stdin_tty. Fixes [#97](https://github.com/folke/snacks.nvim/issues/97). Fixes [#98](https://github.com/folke/snacks.nvim/issues/98) ([4ff08f1](https://github.com/folke/snacks.nvim/commit/4ff08f1c4d7b7b5d3e1da3b2cc9d6c341cd4dc1a))\n* **dashboard:** debug output ([c0129da](https://github.com/folke/snacks.nvim/commit/c0129da4f839fd4306627b087cb722ea54c50c18))\n* **dashboard:** disable `vim.wo.colorcolumn` ([#101](https://github.com/folke/snacks.nvim/issues/101)) ([43b4abb](https://github.com/folke/snacks.nvim/commit/43b4abb9f11a07d7130b461f9bd96b3e4e3c5b94))\n* **dashboard:** notify on errors. Fixes [#99](https://github.com/folke/snacks.nvim/issues/99) ([2ae4108](https://github.com/folke/snacks.nvim/commit/2ae410889cbe6f59fb52f40cb86b25d6f7e874e2))\n* **debug:** MYVIMRC is not always set ([735f4d8](https://github.com/folke/snacks.nvim/commit/735f4d8c9de6fcff31bce671495569f345818ea0))\n* **notifier:** also handle timeout = false / timeout = true. See [#102](https://github.com/folke/snacks.nvim/issues/102) ([99f1f49](https://github.com/folke/snacks.nvim/commit/99f1f49104d413dabee9ee45bc22366aee97056e))\n\n## [2.2.0](https://github.com/folke/snacks.nvim/compare/v2.1.0...v2.2.0) (2024-11-18)\n\n\n### Features\n\n* **dashboard:** added new `dashboard` snack ([#77](https://github.com/folke/snacks.nvim/issues/77)) ([d540fa6](https://github.com/folke/snacks.nvim/commit/d540fa607c415b55f5a0d773f561c19cd6287de4))\n* **debug:** Snacks.debug.trace and Snacks.debug.stats for hierarchical traces (like lazy profile) ([b593598](https://github.com/folke/snacks.nvim/commit/b593598859b1bb3946671fc78ee1896d32460552))\n* **notifier:** global keep when in cmdline ([73b1e20](https://github.com/folke/snacks.nvim/commit/73b1e20d38d4d238316ed391faf3d7ad4c3e71be))\n\n## [2.1.0](https://github.com/folke/snacks.nvim/compare/v2.0.0...v2.1.0) (2024-11-16)\n\n\n### Features\n\n* **notifier:** allow specifying a minimal level to show notifications. See [#82](https://github.com/folke/snacks.nvim/issues/82) ([ec9cfb3](https://github.com/folke/snacks.nvim/commit/ec9cfb36b1ea2b4bf21b5812791c8bfee3bcf322))\n* **terminal:** when terminal terminates too quickly, don't close the window and show an error message. See [#80](https://github.com/folke/snacks.nvim/issues/80) ([313954e](https://github.com/folke/snacks.nvim/commit/313954efdfb064a85df731b29fa9b86bc711044a))\n\n\n### Bug Fixes\n\n* **docs:** typo in README.md ([#78](https://github.com/folke/snacks.nvim/issues/78)) ([dc0f404](https://github.com/folke/snacks.nvim/commit/dc0f4041dcc8da860bdf84c3bf27d41a6a4debf3))\n* **example:** rename file. Closes [#76](https://github.com/folke/snacks.nvim/issues/76) ([00c7a67](https://github.com/folke/snacks.nvim/commit/00c7a674004665999fbea310a322f1e105e1cfb5))\n* **notifier:** no gap in border without title/icon ([#85](https://github.com/folke/snacks.nvim/issues/85)) ([bc80bdc](https://github.com/folke/snacks.nvim/commit/bc80bdcc62efd236617dbbd183ce5882aded2145))\n* **win:** delay when closing windows ([#81](https://github.com/folke/snacks.nvim/issues/81)) ([d3dc8e7](https://github.com/folke/snacks.nvim/commit/d3dc8e7c27a663e4b30579e4e1ca3313052d0874))\n\n## [2.0.0](https://github.com/folke/snacks.nvim/compare/v1.2.0...v2.0.0) (2024-11-14)\n\n\n### ⚠ BREAKING CHANGES\n\n* **config:** plugins are no longer enabled by default. Pass any options, or set `enabled = true`.\n\n### Features\n\n* **config:** plugins are no longer enabled by default. Pass any options, or set `enabled = true`. ([797708b](https://github.com/folke/snacks.nvim/commit/797708b0384ddfd66118651c48c3b399e376cb77))\n* **health:** added health checks to plugins ([1c4c748](https://github.com/folke/snacks.nvim/commit/1c4c74828fcca382f54817f4446649b201d56557))\n* **terminal:** added `Snacks.terminal.colorize()` to replace ansi codes by colors ([519b684](https://github.com/folke/snacks.nvim/commit/519b6841c42c575aec2ffc6c79c4e0a1a13e74bd))\n\n\n### Bug Fixes\n\n* **lazygit:** not needed to use deprecated fallback for set_hl ([14f076e](https://github.com/folke/snacks.nvim/commit/14f076e039aa876ba086449a45053d847bddb3db))\n* **notifier:** disable `colorcolumn` by default ([#66](https://github.com/folke/snacks.nvim/issues/66)) ([7627b81](https://github.com/folke/snacks.nvim/commit/7627b81d9f3453bd2e979d48d3eff2787e6029e9))\n* **statuscolumn:** ensure Snacks exists when loading before plugin loaded ([97e0e1e](https://github.com/folke/snacks.nvim/commit/97e0e1ec7f1088ee026efdaa789d102461ad49d4))\n* **terminal:** properly deal with args in `vim.o.shell`. Fixes [#69](https://github.com/folke/snacks.nvim/issues/69) ([2ccb70f](https://github.com/folke/snacks.nvim/commit/2ccb70fd3a42d188c95db233bfb7469259d56fb6))\n* **win:** take border into account for window position ([#64](https://github.com/folke/snacks.nvim/issues/64)) ([f0e47fd](https://github.com/folke/snacks.nvim/commit/f0e47fd5dc3ccc71cdd8866e2ce82749d3797fbb))\n\n## [1.2.0](https://github.com/folke/snacks.nvim/compare/v1.1.0...v1.2.0) (2024-11-11)\n\n\n### Features\n\n* **bufdelete:** added `wipe` option. Closes [#38](https://github.com/folke/snacks.nvim/issues/38) ([5914cb1](https://github.com/folke/snacks.nvim/commit/5914cb101070956a73462dcb1c81c8462e9e77d7))\n* **lazygit:** allow overriding extra lazygit config options ([d2f4f19](https://github.com/folke/snacks.nvim/commit/d2f4f1937e6fa97a48d5839d49f1f3012067bf45))\n* **notifier:** added `refresh` option configurable ([df8c9d7](https://github.com/folke/snacks.nvim/commit/df8c9d7724ade9f3c63277f08b237ac3b32b6cfe))\n* **notifier:** added backward compatibility for nvim-notify's replace option ([9b9777e](https://github.com/folke/snacks.nvim/commit/9b9777ec3bba97b3ddb37bd824a9ef9a46955582))\n* **words:** add `fold_open` and `set_jump_point` config options ([#31](https://github.com/folke/snacks.nvim/issues/31)) ([5dc749b](https://github.com/folke/snacks.nvim/commit/5dc749b045e62e30a156ca8522416a6d1ca9a959))\n\n\n### Bug Fixes\n\n* added compatibility with Neovim &gt;= 0.9.4 ([4f99818](https://github.com/folke/snacks.nvim/commit/4f99818b0ab98510ab8987a0427afc515fb5f76b))\n* **bufdelete:** opts.wipe. See [#38](https://github.com/folke/snacks.nvim/issues/38) ([0efbb93](https://github.com/folke/snacks.nvim/commit/0efbb93e0a4405b955d574746eb57ef6d48ae386))\n* **notifier:** take title/footer into account to determine notification width. Fixes [#54](https://github.com/folke/snacks.nvim/issues/54) ([09a6f17](https://github.com/folke/snacks.nvim/commit/09a6f17eccbb551797f522403033f48e63a25f74))\n* **notifier:** update layout on vim resize ([7f9f691](https://github.com/folke/snacks.nvim/commit/7f9f691a12d0665146b25a44323f21e18aa46c24))\n* **terminal:** `gf` properly opens file ([#45](https://github.com/folke/snacks.nvim/issues/45)) ([340cc27](https://github.com/folke/snacks.nvim/commit/340cc2756e9d7ef0ae9a6f55cdfbfdca7a9defa7))\n* **terminal:** pass a list of strings to termopen to prevent splitting. Fixes [#59](https://github.com/folke/snacks.nvim/issues/59) ([458a84b](https://github.com/folke/snacks.nvim/commit/458a84bd1db856c21f234a504ec384191a9899cf))\n\n\n### Performance Improvements\n\n* **notifier:** only force redraw for new windows and for updated while search is not active. Fixes [#52](https://github.com/folke/snacks.nvim/issues/52) ([da86b1d](https://github.com/folke/snacks.nvim/commit/da86b1deff9a0b1bb66f344241fb07577b6463b8))\n* **win:** don't try highlighting snacks internal filetypes ([eb8ab37](https://github.com/folke/snacks.nvim/commit/eb8ab37f6ac421eeda2570257d2279bd12700667))\n* **win:** prevent treesitter and syntax attaching to scratch buffers ([cc80f6d](https://github.com/folke/snacks.nvim/commit/cc80f6dc1b7a286cb06c6321bfeb1046f7a59418))\n\n## [1.1.0](https://github.com/folke/snacks.nvim/compare/v1.0.0...v1.1.0) (2024-11-08)\n\n\n### Features\n\n* **bufdelete:** optional filter and shortcuts to delete `all` and `other` buffers. Closes [#11](https://github.com/folke/snacks.nvim/issues/11) ([71a2346](https://github.com/folke/snacks.nvim/commit/71a234608ffeebfa8a04c652834342fa9ce508c3))\n* **debug:** simple log function to quickly log something to a debug.log file ([fc2a8e7](https://github.com/folke/snacks.nvim/commit/fc2a8e74686c7c347ed0aaa5eb607874ecdca288))\n* **docs:** docs for highlight groups ([#13](https://github.com/folke/snacks.nvim/issues/13)) ([964cd6a](https://github.com/folke/snacks.nvim/commit/964cd6aa76f3608c7e379b8b1a483ae19f57e279))\n* **gitbrowse:** choose to open repo, branch or file. Closes [#10](https://github.com/folke/snacks.nvim/issues/10). Closes [#17](https://github.com/folke/snacks.nvim/issues/17) ([92da87c](https://github.com/folke/snacks.nvim/commit/92da87c910a9b1421e8baae2e67020565526fba8))\n* **notifier:** added history to notifier. Closes [#14](https://github.com/folke/snacks.nvim/issues/14) ([65d8c8f](https://github.com/folke/snacks.nvim/commit/65d8c8f00b6589b44410301b790d97c268f86f85))\n* **notifier:** added option to show notifs top-down or bottom-up. Closes [#9](https://github.com/folke/snacks.nvim/issues/9) ([080e0d4](https://github.com/folke/snacks.nvim/commit/080e0d403924e1e62d3b88412a41f3ab22594049))\n* **notifier:** allow overriding hl groups per notification ([8bcb2bc](https://github.com/folke/snacks.nvim/commit/8bcb2bc805a1785208f96ad7ad96690eee50c925))\n* **notifier:** allow setting dynamic options ([36e9f45](https://github.com/folke/snacks.nvim/commit/36e9f45302bc9c200c76349ecd79a319a5944d8c))\n* **win:** added default hl groups for windows ([8c0f10b](https://github.com/folke/snacks.nvim/commit/8c0f10b9dade154d355e31aa3f9c8c0ba212205e))\n* **win:** allow setting `ft` just for highlighting without actually changing the `filetype` ([cad236f](https://github.com/folke/snacks.nvim/commit/cad236f9bbe46fbb53127014731d8507a3bc80af))\n* **win:** disable winblend when colorscheme is transparent. Fixes [#26](https://github.com/folke/snacks.nvim/issues/26) ([12077bc](https://github.com/folke/snacks.nvim/commit/12077bcf65554b585b1f094c69746df402433132))\n* **win:** equalize splits ([e982aab](https://github.com/folke/snacks.nvim/commit/e982aabefdf0b1d00ddd850152921e577cd980cc))\n* **win:** util methods to handle buffer text ([d3efb92](https://github.com/folke/snacks.nvim/commit/d3efb92aa546eb160782e24e305f74a559eec212))\n* **win:** win:focus() ([476fb56](https://github.com/folke/snacks.nvim/commit/476fb56bfd8e32a2805f46fadafbc4eee7878597))\n* **words:** `jump` optionally shows notification with reference count ([#23](https://github.com/folke/snacks.nvim/issues/23)) ([6a3f865](https://github.com/folke/snacks.nvim/commit/6a3f865357005c934e2b5ad2cfadaa038775e9e0))\n* **words:** configurable mode to show references. Defaults to n, i, c. Closes [#18](https://github.com/folke/snacks.nvim/issues/18) ([d079fbf](https://github.com/folke/snacks.nvim/commit/d079fbfe354ebfec18c4e1bfd7fee695703e3692))\n\n\n### Bug Fixes\n\n* **config:** deepcopy config where needed ([6c76f91](https://github.com/folke/snacks.nvim/commit/6c76f913981663ec0dba39686018cbc2ff3220b8))\n* **config:** fix reading config during setup. Fixes [#2](https://github.com/folke/snacks.nvim/issues/2) ([0d91a4e](https://github.com/folke/snacks.nvim/commit/0d91a4e364866e407901020b59121883cbfb1cf1))\n* **notifier:** re-apply winhl since level might have changed with a replace ([b8cc93e](https://github.com/folke/snacks.nvim/commit/b8cc93e273fd481f2b3b7785f64e301d70fd8e45))\n* **notifier:** set default conceallevel=2 ([662795c](https://github.com/folke/snacks.nvim/commit/662795c2855b7bfd5e6ec254e469284dacdabb3f))\n* **notifier:** try to keep layout when replacing notifs ([9bdb24e](https://github.com/folke/snacks.nvim/commit/9bdb24e735458ea4fd3974939c33ea78cbba0212))\n* **terminal:** dont overwrite user opts ([0b08d28](https://github.com/folke/snacks.nvim/commit/0b08d280b605b2e460c1fd92bc87152e66f14430))\n* **terminal:** user options ([334895c](https://github.com/folke/snacks.nvim/commit/334895c5bb2ed04f65800abaeb91ccb0487b0f1f))\n* **win:** better winfixheight and winfixwidth for splits ([8be14c6](https://github.com/folke/snacks.nvim/commit/8be14c68a7825fff90ca071f0650657ba88da423))\n* **win:** disable sidescroloff in minimal style ([107d10b](https://github.com/folke/snacks.nvim/commit/107d10b52e54828606a645517b55802dd807e8ad))\n* **win:** dont center float when `relative=\"cursor\"` ([4991e34](https://github.com/folke/snacks.nvim/commit/4991e347dcc6ff6c14443afe9b4d849a67b67944))\n* **win:** properly resolve user styles as last ([cc5ee19](https://github.com/folke/snacks.nvim/commit/cc5ee192caf79446d58cbc09487268aa1f86f405))\n* **win:** set border to none for backdrop windows ([#19](https://github.com/folke/snacks.nvim/issues/19)) ([f5602e6](https://github.com/folke/snacks.nvim/commit/f5602e60c325f0c60eb6f2869a7222beb88a773c))\n* **win:** simpler way to add buffer padding ([f59237f](https://github.com/folke/snacks.nvim/commit/f59237f1dcdceb646bf2552b69b7e2040f80f603))\n* **win:** update win/buf opts when needed ([5fd9c42](https://github.com/folke/snacks.nvim/commit/5fd9c426e850c02489943d7177d9e7fddec5e589))\n* **words:** disable notify_jump by default ([9576081](https://github.com/folke/snacks.nvim/commit/9576081e871a801f60367e7180543fa41c384755))\n\n\n### Performance Improvements\n\n* **notifier:** index queue by id ([5df4394](https://github.com/folke/snacks.nvim/commit/5df4394c60958635bf4651d8d7e25f53f48a3965))\n* **notifier:** optimize layout code ([8512896](https://github.com/folke/snacks.nvim/commit/8512896228b3e37e3d02c68fa739749c9f0b9838))\n* **notifier:** skip processing queue when free space is smaller than min height ([08190a5](https://github.com/folke/snacks.nvim/commit/08190a545857ef09cb6ada4337fe7ec67d3602a9))\n* **win:** skip events when setting buf/win options. Trigger FileType on BufEnter only if needed ([61496a3](https://github.com/folke/snacks.nvim/commit/61496a3ef00bd67afb7affcb4933905910a6283c))\n\n## 1.0.0 (2024-11-06)\n\n\n### Features\n\n* added debug ([6cb43f6](https://github.com/folke/snacks.nvim/commit/6cb43f603360c6fc702b5d7c928dfde22d886e2f))\n* added git ([f0a9991](https://github.com/folke/snacks.nvim/commit/f0a999134738c54dccb78ae462774eb228614221))\n* added gitbrowse ([a638d8b](https://github.com/folke/snacks.nvim/commit/a638d8bafef85ac6046cfc02e415a8893e0391b9))\n* added lazygit ([fc32619](https://github.com/folke/snacks.nvim/commit/fc32619734e4d3c024b8fc2db941c8ac19d2dd6c))\n* added notifier ([44011dd](https://github.com/folke/snacks.nvim/commit/44011ddf0da07d0fa89734d21bb770f01a630077))\n* added notify ([f4e0130](https://github.com/folke/snacks.nvim/commit/f4e0130ec3cb0299a3a85c589250c114d46f53c2))\n* added toggle ([28c3029](https://github.com/folke/snacks.nvim/commit/28c30296991ac5549b49b7ecfb49f108f70d76ba))\n* better buffer/window vars for terminal and float ([1abce78](https://github.com/folke/snacks.nvim/commit/1abce78a8b826943d5055464636cd9fad074b4bb))\n* bigfile ([8d62b28](https://github.com/folke/snacks.nvim/commit/8d62b285d5026e3d7c064d435c424bab40d1910a))\n* **bigfile:** show message when bigfile was detected ([fdc0d3d](https://github.com/folke/snacks.nvim/commit/fdc0d3d1f80a6be64e85a5a25dc34693edadd73f))\n* bufdelete ([cc5353f](https://github.com/folke/snacks.nvim/commit/cc5353f6b3f3f3869e2110b2d3d1a95418653213))\n* config & setup ([c98c4c0](https://github.com/folke/snacks.nvim/commit/c98c4c030711a59e6791d8e5cab7550e33ac2d2d))\n* **config:** get config for snack with defaults and custom opts ([b3d08be](https://github.com/folke/snacks.nvim/commit/b3d08beb8c60fddc6bfbf96ac9f45c4db49e64af))\n* **debug:** added simple profile function ([e1f736d](https://github.com/folke/snacks.nvim/commit/e1f736d71fb9020a09019a49d645d4fe6d9f30db))\n* **docs:** better handling of overloads ([038b283](https://github.com/folke/snacks.nvim/commit/038b28319c3a4eba7220a679a5759c06e69b8493))\n* ensure Snacks global is available when not using setup ([f0458ba](https://github.com/folke/snacks.nvim/commit/f0458bafb059da9885de4fbab1ae5cb6ce2cd0bb))\n* float ([d106107](https://github.com/folke/snacks.nvim/commit/d106107cdccc7ecb9931e011a89df6011eed44c4))\n* **float:** added support for splits ([977a3d3](https://github.com/folke/snacks.nvim/commit/977a3d345b6da2b819d9bc4870d3d8a7e026728e))\n* **float:** better key mappings ([a171a81](https://github.com/folke/snacks.nvim/commit/a171a815b3acd72dd779781df5586a7cd6ddd649))\n* initial commit ([63a24f6](https://github.com/folke/snacks.nvim/commit/63a24f6eb047530234297460a9b7ccd6af0b9858))\n* **notifier:** add 1 cell left/right padding and make wrapping work properly ([efc9699](https://github.com/folke/snacks.nvim/commit/efc96996e5a98b619e87581e9527c871177dee52))\n* **notifier:** added global keep config option ([f32d82d](https://github.com/folke/snacks.nvim/commit/f32d82d1b705512eb56c901d4d7de68eedc827b1))\n* **notifier:** added minimal style ([b29a6d5](https://github.com/folke/snacks.nvim/commit/b29a6d5972943cb8fcfdfb94610d850f0ba050b3))\n* **notifier:** allow closing notifs with `q` ([97acbbb](https://github.com/folke/snacks.nvim/commit/97acbbb654d13a0d38792fd6383973a2ca01a2bf))\n* **notifier:** allow config of default filetype ([8a96888](https://github.com/folke/snacks.nvim/commit/8a968884098be83acb42f31a573e62b63420268e))\n* **notifier:** enable wrapping by default ([d02aa2f](https://github.com/folke/snacks.nvim/commit/d02aa2f7cb49273330fd778818124ddb39838372))\n* **notifier:** keep notif open when it's the current window ([1e95800](https://github.com/folke/snacks.nvim/commit/1e9580039b706cfc1f526fea2e46b6857473420b))\n* quickfile ([d0ce645](https://github.com/folke/snacks.nvim/commit/d0ce6454f95fe056c65324a0f59a250532a658f3))\n* rename ([fa33688](https://github.com/folke/snacks.nvim/commit/fa336883019110b8f525081665bf55c19df5f0aa))\n* statuscolumn ([99b1700](https://github.com/folke/snacks.nvim/commit/99b170001592fe054368a220599da546de64894e))\n* terminal ([e6cc7c9](https://github.com/folke/snacks.nvim/commit/e6cc7c998afa63eaf126d169f4702953f548d39f))\n* **terminal:** allow to override the default terminal implementation (like toggleterm) ([11c9ee8](https://github.com/folke/snacks.nvim/commit/11c9ee83aa133f899dad966224df0e2d7de236f2))\n* **terminal:** better defaults and winbar ([7ceeb47](https://github.com/folke/snacks.nvim/commit/7ceeb47e545619dff6dd8853f0a368afae7d3ec8))\n* **terminal:** better double esc to go to normal mode ([a4af729](https://github.com/folke/snacks.nvim/commit/a4af729b2489714b066ca03f008bf5fe42c93343))\n* **win:** better api to deal with sizes ([ac1a50c](https://github.com/folke/snacks.nvim/commit/ac1a50c810c5f67909921592afcebffa566ee3d3))\n* **win:** custom views ([12d6f86](https://github.com/folke/snacks.nvim/commit/12d6f863f73cbd3580295adc5bc546c9d10e9e7f))\n* words ([73445af](https://github.com/folke/snacks.nvim/commit/73445af400457722508395d18d2c974965c53fe2))\n\n\n### Bug Fixes\n\n* **config:** don't change defaults in merge ([6e825f5](https://github.com/folke/snacks.nvim/commit/6e825f509ed0e41dbafe5bca0236157772344554))\n* **config:** merging of possible nil values ([f5bbb44](https://github.com/folke/snacks.nvim/commit/f5bbb446ed012361bbd54362558d1f32476206c7))\n* **debug:** exclude vimrc from callers ([8845a6a](https://github.com/folke/snacks.nvim/commit/8845a6a912a528f63216ffe4b991b025c8955447))\n* **float:** don't use backdrop for splits ([5eb64c5](https://github.com/folke/snacks.nvim/commit/5eb64c52aeb7271a3116751f1ec61b00536cdc08))\n* **float:** only set default filetype if no ft is set ([66b2525](https://github.com/folke/snacks.nvim/commit/66b252535c7a78f0cd73755fad22b07b814309f1))\n* **float:** proper closing of backdrop ([a528e77](https://github.com/folke/snacks.nvim/commit/a528e77397daea422ff84dde64124d4a7e352bc2))\n* **notifier:** modifiable ([fd57c24](https://github.com/folke/snacks.nvim/commit/fd57c243015e6f2c863bb6c89e1417e77f3e0ea4))\n* **notifier:** modifiable = false ([9ef9e69](https://github.com/folke/snacks.nvim/commit/9ef9e69620fc51d368fcee830a59bc9279594d43))\n* **notifier:** show notifier errors with nvim_err_writeln ([e8061bc](https://github.com/folke/snacks.nvim/commit/e8061bcda095e3e9a3110ce697cdf7155e178d6e))\n* **notifier:** sorting ([d9a1f23](https://github.com/folke/snacks.nvim/commit/d9a1f23e216230fcb2060e74056778d57d1d7676))\n* simplify setup ([787b53e](https://github.com/folke/snacks.nvim/commit/787b53e7635f322bf42a42279f647340daf77770))\n* **win:** backdrop ([71dd912](https://github.com/folke/snacks.nvim/commit/71dd912763918fd6b7fd07dd66ccd16afd4fea78))\n* **win:** better implementation of window styles (previously views) ([6681097](https://github.com/folke/snacks.nvim/commit/66810971b9bd08e212faae467d884758bf142ffe))\n* **win:** dont error when augroup is already deleted ([8c43597](https://github.com/folke/snacks.nvim/commit/8c43597f10dc2916200a8c857026f3f15fd3ae65))\n* **win:** dont update win opt noautocmd ([a06e3ed](https://github.com/folke/snacks.nvim/commit/a06e3ed8fcd08aaf43fc2454bb1b0f935053aca7))\n* **win:** no need to set EndOfBuffer winhl ([7a7f221](https://github.com/folke/snacks.nvim/commit/7a7f221020e024da831c2a3d72f8a9a0c330d711))\n* **win:** use syntax as fallback for treesitter ([f3b69a6](https://github.com/folke/snacks.nvim/commit/f3b69a617a57597571fcf4263463f989fa3b663d))\n\n\n### Performance Improvements\n\n* **win:** set options with eventignore and handle ft manually ([80d9a89](https://github.com/folke/snacks.nvim/commit/80d9a894f9d6b2087e0dfee6d777e7b78490ba93))\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# 🍿 `snacks.nvim`\n\nA collection of small QoL plugins for Neovim.\n\n## ✨ Features\n\n<!-- toc:start -->\n\n| Snack | Description | Setup |\n| ----- | ----------- | :---: |\n| [animate](https://github.com/folke/snacks.nvim/blob/main/docs/animate.md) | Efficient animations including over 45 easing functions _(library)_ |  |\n| [bigfile](https://github.com/folke/snacks.nvim/blob/main/docs/bigfile.md) | Deal with big files | ‼️ |\n| [bufdelete](https://github.com/folke/snacks.nvim/blob/main/docs/bufdelete.md) | Delete buffers without disrupting window layout |  |\n| [dashboard](https://github.com/folke/snacks.nvim/blob/main/docs/dashboard.md) |  Beautiful declarative dashboards | ‼️ |\n| [debug](https://github.com/folke/snacks.nvim/blob/main/docs/debug.md) | Pretty inspect & backtraces for debugging |  |\n| [dim](https://github.com/folke/snacks.nvim/blob/main/docs/dim.md) | Focus on the active scope by dimming the rest |  |\n| [explorer](https://github.com/folke/snacks.nvim/blob/main/docs/explorer.md) | A file explorer (picker in disguise) | ‼️ |\n| [gh](https://github.com/folke/snacks.nvim/blob/main/docs/gh.md) | GitHub CLI integration |  |\n| [git](https://github.com/folke/snacks.nvim/blob/main/docs/git.md) | Git utilities |  |\n| [gitbrowse](https://github.com/folke/snacks.nvim/blob/main/docs/gitbrowse.md) | Open the current file, branch, commit, or repo in a browser (e.g. GitHub, GitLab, Bitbucket) |  |\n| [image](https://github.com/folke/snacks.nvim/blob/main/docs/image.md) | Image viewer using Kitty Graphics Protocol, supported by `kitty`, `wezterm` and `ghostty` | ‼️ |\n| [indent](https://github.com/folke/snacks.nvim/blob/main/docs/indent.md) | Indent guides and scopes |  |\n| [input](https://github.com/folke/snacks.nvim/blob/main/docs/input.md) | Better `vim.ui.input` | ‼️ |\n| [keymap](https://github.com/folke/snacks.nvim/blob/main/docs/keymap.md) | Better `vim.keymap` with support for filetypes and LSP clients |  |\n| [layout](https://github.com/folke/snacks.nvim/blob/main/docs/layout.md) | Window layouts |  |\n| [lazygit](https://github.com/folke/snacks.nvim/blob/main/docs/lazygit.md) | Open LazyGit in a float, auto-configure colorscheme and integration with Neovim |  |\n| [notifier](https://github.com/folke/snacks.nvim/blob/main/docs/notifier.md) | Pretty `vim.notify` | ‼️ |\n| [notify](https://github.com/folke/snacks.nvim/blob/main/docs/notify.md) | Utility functions to work with Neovim's `vim.notify` |  |\n| [picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) | Picker for selecting items | ‼️ |\n| [profiler](https://github.com/folke/snacks.nvim/blob/main/docs/profiler.md) | Neovim lua profiler |  |\n| [quickfile](https://github.com/folke/snacks.nvim/blob/main/docs/quickfile.md) | When doing `nvim somefile.txt`, it will render the file as quickly as possible, before loading your plugins. | ‼️ |\n| [rename](https://github.com/folke/snacks.nvim/blob/main/docs/rename.md) | LSP-integrated file renaming with support for plugins like [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim) and [mini.files](https://github.com/nvim-mini/mini.files). |  |\n| [scope](https://github.com/folke/snacks.nvim/blob/main/docs/scope.md) | Scope detection, text objects and jumping based on treesitter or indent | ‼️ |\n| [scratch](https://github.com/folke/snacks.nvim/blob/main/docs/scratch.md) | Scratch buffers with a persistent file |  |\n| [scroll](https://github.com/folke/snacks.nvim/blob/main/docs/scroll.md) | Smooth scrolling | ‼️ |\n| [statuscolumn](https://github.com/folke/snacks.nvim/blob/main/docs/statuscolumn.md) | Pretty status column | ‼️ |\n| [terminal](https://github.com/folke/snacks.nvim/blob/main/docs/terminal.md) | Create and toggle floating/split terminals |  |\n| [toggle](https://github.com/folke/snacks.nvim/blob/main/docs/toggle.md) | Toggle keymaps integrated with which-key icons / colors |  |\n| [util](https://github.com/folke/snacks.nvim/blob/main/docs/util.md) | Utility functions for Snacks _(library)_ |  |\n| [win](https://github.com/folke/snacks.nvim/blob/main/docs/win.md) | Create and manage floating windows or splits |  |\n| [words](https://github.com/folke/snacks.nvim/blob/main/docs/words.md) | Auto-show LSP references and quickly navigate between them | ‼️ |\n| [zen](https://github.com/folke/snacks.nvim/blob/main/docs/zen.md) | Zen mode • distraction-free coding |  |\n\n<!-- toc:end -->\n\n## ⚡️ Requirements\n\n- **Neovim** >= 0.9.4\n- for proper icons support:\n  - [mini.icons](https://github.com/nvim-mini/mini.icons) _(optional)_\n  - [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) _(optional)_\n  - a [Nerd Font](https://www.nerdfonts.com/) **_(optional)_**\n\n## 📦 Installation\n\nInstall the plugin with your package manager:\n\n### [lazy.nvim](https://github.com/folke/lazy.nvim)\n\n> [!important]\n> A couple of plugins **require** `snacks.nvim` to be set-up early.\n> Setup creates some autocmds and does not load any plugins.\n> Check the [code](https://github.com/folke/snacks.nvim/blob/main/lua/snacks/init.lua) to see what it does.\n\n> [!caution]\n> You need to explicitly pass options for a plugin or set `enabled = true` to enable it.\n\n> [!tip]\n> It's a good idea to run `:checkhealth snacks` to see if everything is set up correctly.\n\n```lua\n{\n  \"folke/snacks.nvim\",\n  priority = 1000,\n  lazy = false,\n  ---@type snacks.Config\n  opts = {\n    -- your configuration comes here\n    -- or leave it empty to use the default settings\n    -- refer to the configuration section below\n    bigfile = { enabled = true },\n    dashboard = { enabled = true },\n    explorer = { enabled = true },\n    indent = { enabled = true },\n    input = { enabled = true },\n    picker = { enabled = true },\n    notifier = { enabled = true },\n    quickfile = { enabled = true },\n    scope = { enabled = true },\n    scroll = { enabled = true },\n    statuscolumn = { enabled = true },\n    words = { enabled = true },\n  },\n}\n```\n\nFor an in-depth setup of `snacks.nvim` with `lazy.nvim`, check the [example](https://github.com/folke/snacks.nvim?tab=readme-ov-file#-usage) below.\n\n## ⚙️ Configuration\n\nPlease refer to the readme of each plugin for their specific configuration.\n\n<details><summary>Default Options</summary>\n\n<!-- config:start -->\n\n```lua\n---@class snacks.Config\n---@field animate? snacks.animate.Config\n---@field bigfile? snacks.bigfile.Config\n---@field dashboard? snacks.dashboard.Config\n---@field dim? snacks.dim.Config\n---@field explorer? snacks.explorer.Config\n---@field gh? snacks.gh.Config\n---@field gitbrowse? snacks.gitbrowse.Config\n---@field image? snacks.image.Config\n---@field indent? snacks.indent.Config\n---@field input? snacks.input.Config\n---@field layout? snacks.layout.Config\n---@field lazygit? snacks.lazygit.Config\n---@field notifier? snacks.notifier.Config\n---@field picker? snacks.picker.Config\n---@field profiler? snacks.profiler.Config\n---@field quickfile? snacks.quickfile.Config\n---@field scope? snacks.scope.Config\n---@field scratch? snacks.scratch.Config\n---@field scroll? snacks.scroll.Config\n---@field statuscolumn? snacks.statuscolumn.Config\n---@field terminal? snacks.terminal.Config\n---@field toggle? snacks.toggle.Config\n---@field win? snacks.win.Config\n---@field words? snacks.words.Config\n---@field zen? snacks.zen.Config\n---@field styles? table<string, snacks.win.Config>\n---@field image? snacks.image.Config|{}\n{\n  image = {\n    -- define these here, so that we don't need to load the image module\n    formats = {\n      \"png\",\n      \"jpg\",\n      \"jpeg\",\n      \"gif\",\n      \"bmp\",\n      \"webp\",\n      \"tiff\",\n      \"heic\",\n      \"avif\",\n      \"mp4\",\n      \"mov\",\n      \"avi\",\n      \"mkv\",\n      \"webm\",\n      \"pdf\",\n      \"icns\",\n    },\n  },\n}\n```\n\n<!-- config:end -->\n\n</details>\n\nSome plugins have examples in their documentation. You can include them in your\nconfig like this:\n\n```lua\n{\n  dashboard = { example = \"github\" }\n}\n```\n\nIf you want to customize options for a plugin after they have been resolved, you\ncan use the `config` function:\n\n```lua\n{\n  gitbrowse = {\n    config = function(opts, defaults)\n      table.insert(opts.remote_patterns, { \"my\", \"custom pattern\" })\n    end\n  },\n}\n```\n\n## 🚀 Usage\n\nSee the example below for how to configure `snacks.nvim`.\n\n<!-- example:start -->\n\n```lua\n{\n  \"folke/snacks.nvim\",\n  priority = 1000,\n  lazy = false,\n  ---@type snacks.Config\n  opts = {\n    bigfile = { enabled = true },\n    dashboard = { enabled = true },\n    explorer = { enabled = true },\n    indent = { enabled = true },\n    input = { enabled = true },\n    notifier = {\n      enabled = true,\n      timeout = 3000,\n    },\n    picker = { enabled = true },\n    quickfile = { enabled = true },\n    scope = { enabled = true },\n    scroll = { enabled = true },\n    statuscolumn = { enabled = true },\n    words = { enabled = true },\n    styles = {\n      notification = {\n        -- wo = { wrap = true } -- Wrap notifications\n      }\n    }\n  },\n  keys = {\n    -- Top Pickers & Explorer\n    { \"<leader><space>\", function() Snacks.picker.smart() end, desc = \"Smart Find Files\" },\n    { \"<leader>,\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n    { \"<leader>/\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n    { \"<leader>:\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n    { \"<leader>n\", function() Snacks.picker.notifications() end, desc = \"Notification History\" },\n    { \"<leader>e\", function() Snacks.explorer() end, desc = \"File Explorer\" },\n    -- find\n    { \"<leader>fb\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n    { \"<leader>fc\", function() Snacks.picker.files({ cwd = vim.fn.stdpath(\"config\") }) end, desc = \"Find Config File\" },\n    { \"<leader>ff\", function() Snacks.picker.files() end, desc = \"Find Files\" },\n    { \"<leader>fg\", function() Snacks.picker.git_files() end, desc = \"Find Git Files\" },\n    { \"<leader>fp\", function() Snacks.picker.projects() end, desc = \"Projects\" },\n    { \"<leader>fr\", function() Snacks.picker.recent() end, desc = \"Recent\" },\n    -- git\n    { \"<leader>gb\", function() Snacks.picker.git_branches() end, desc = \"Git Branches\" },\n    { \"<leader>gl\", function() Snacks.picker.git_log() end, desc = \"Git Log\" },\n    { \"<leader>gL\", function() Snacks.picker.git_log_line() end, desc = \"Git Log Line\" },\n    { \"<leader>gs\", function() Snacks.picker.git_status() end, desc = \"Git Status\" },\n    { \"<leader>gS\", function() Snacks.picker.git_stash() end, desc = \"Git Stash\" },\n    { \"<leader>gd\", function() Snacks.picker.git_diff() end, desc = \"Git Diff (Hunks)\" },\n    { \"<leader>gf\", function() Snacks.picker.git_log_file() end, desc = \"Git Log File\" },\n    -- gh\n    { \"<leader>gi\", function() Snacks.picker.gh_issue() end, desc = \"GitHub Issues (open)\" },\n    { \"<leader>gI\", function() Snacks.picker.gh_issue({ state = \"all\" }) end, desc = \"GitHub Issues (all)\" },\n    { \"<leader>gp\", function() Snacks.picker.gh_pr() end, desc = \"GitHub Pull Requests (open)\" },\n    { \"<leader>gP\", function() Snacks.picker.gh_pr({ state = \"all\" }) end, desc = \"GitHub Pull Requests (all)\" },\n    -- Grep\n    { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n    { \"<leader>sB\", function() Snacks.picker.grep_buffers() end, desc = \"Grep Open Buffers\" },\n    { \"<leader>sg\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n    { \"<leader>sw\", function() Snacks.picker.grep_word() end, desc = \"Visual selection or word\", mode = { \"n\", \"x\" } },\n    -- search\n    { '<leader>s\"', function() Snacks.picker.registers() end, desc = \"Registers\" },\n    { '<leader>s/', function() Snacks.picker.search_history() end, desc = \"Search History\" },\n    { \"<leader>sa\", function() Snacks.picker.autocmds() end, desc = \"Autocmds\" },\n    { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n    { \"<leader>sc\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n    { \"<leader>sC\", function() Snacks.picker.commands() end, desc = \"Commands\" },\n    { \"<leader>sd\", function() Snacks.picker.diagnostics() end, desc = \"Diagnostics\" },\n    { \"<leader>sD\", function() Snacks.picker.diagnostics_buffer() end, desc = \"Buffer Diagnostics\" },\n    { \"<leader>sh\", function() Snacks.picker.help() end, desc = \"Help Pages\" },\n    { \"<leader>sH\", function() Snacks.picker.highlights() end, desc = \"Highlights\" },\n    { \"<leader>si\", function() Snacks.picker.icons() end, desc = \"Icons\" },\n    { \"<leader>sj\", function() Snacks.picker.jumps() end, desc = \"Jumps\" },\n    { \"<leader>sk\", function() Snacks.picker.keymaps() end, desc = \"Keymaps\" },\n    { \"<leader>sl\", function() Snacks.picker.loclist() end, desc = \"Location List\" },\n    { \"<leader>sm\", function() Snacks.picker.marks() end, desc = \"Marks\" },\n    { \"<leader>sM\", function() Snacks.picker.man() end, desc = \"Man Pages\" },\n    { \"<leader>sp\", function() Snacks.picker.lazy() end, desc = \"Search for Plugin Spec\" },\n    { \"<leader>sq\", function() Snacks.picker.qflist() end, desc = \"Quickfix List\" },\n    { \"<leader>sR\", function() Snacks.picker.resume() end, desc = \"Resume\" },\n    { \"<leader>su\", function() Snacks.picker.undo() end, desc = \"Undo History\" },\n    { \"<leader>uC\", function() Snacks.picker.colorschemes() end, desc = \"Colorschemes\" },\n    -- LSP\n    { \"gd\", function() Snacks.picker.lsp_definitions() end, desc = \"Goto Definition\" },\n    { \"gD\", function() Snacks.picker.lsp_declarations() end, desc = \"Goto Declaration\" },\n    { \"gr\", function() Snacks.picker.lsp_references() end, nowait = true, desc = \"References\" },\n    { \"gI\", function() Snacks.picker.lsp_implementations() end, desc = \"Goto Implementation\" },\n    { \"gy\", function() Snacks.picker.lsp_type_definitions() end, desc = \"Goto T[y]pe Definition\" },\n    { \"gai\", function() Snacks.picker.lsp_incoming_calls() end, desc = \"C[a]lls Incoming\" },\n    { \"gao\", function() Snacks.picker.lsp_outgoing_calls() end, desc = \"C[a]lls Outgoing\" },\n    { \"<leader>ss\", function() Snacks.picker.lsp_symbols() end, desc = \"LSP Symbols\" },\n    { \"<leader>sS\", function() Snacks.picker.lsp_workspace_symbols() end, desc = \"LSP Workspace Symbols\" },\n    -- Other\n    { \"<leader>z\",  function() Snacks.zen() end, desc = \"Toggle Zen Mode\" },\n    { \"<leader>Z\",  function() Snacks.zen.zoom() end, desc = \"Toggle Zoom\" },\n    { \"<leader>.\",  function() Snacks.scratch() end, desc = \"Toggle Scratch Buffer\" },\n    { \"<leader>S\",  function() Snacks.scratch.select() end, desc = \"Select Scratch Buffer\" },\n    { \"<leader>n\",  function() Snacks.notifier.show_history() end, desc = \"Notification History\" },\n    { \"<leader>bd\", function() Snacks.bufdelete() end, desc = \"Delete Buffer\" },\n    { \"<leader>cR\", function() Snacks.rename.rename_file() end, desc = \"Rename File\" },\n    { \"<leader>gB\", function() Snacks.gitbrowse() end, desc = \"Git Browse\", mode = { \"n\", \"v\" } },\n    { \"<leader>gg\", function() Snacks.lazygit() end, desc = \"Lazygit\" },\n    { \"<leader>un\", function() Snacks.notifier.hide() end, desc = \"Dismiss All Notifications\" },\n    { \"<c-/>\",      function() Snacks.terminal() end, desc = \"Toggle Terminal\" },\n    { \"<c-_>\",      function() Snacks.terminal() end, desc = \"which_key_ignore\" },\n    { \"]]\",         function() Snacks.words.jump(vim.v.count1) end, desc = \"Next Reference\", mode = { \"n\", \"t\" } },\n    { \"[[\",         function() Snacks.words.jump(-vim.v.count1) end, desc = \"Prev Reference\", mode = { \"n\", \"t\" } },\n    {\n      \"<leader>N\",\n      desc = \"Neovim News\",\n      function()\n        Snacks.win({\n          file = vim.api.nvim_get_runtime_file(\"doc/news.txt\", false)[1],\n          width = 0.6,\n          height = 0.6,\n          wo = {\n            spell = false,\n            wrap = false,\n            signcolumn = \"yes\",\n            statuscolumn = \" \",\n            conceallevel = 3,\n          },\n        })\n      end,\n    }\n  },\n  init = function()\n    vim.api.nvim_create_autocmd(\"User\", {\n      pattern = \"VeryLazy\",\n      callback = function()\n        -- Setup some globals for debugging (lazy-loaded)\n        _G.dd = function(...)\n          Snacks.debug.inspect(...)\n        end\n        _G.bt = function()\n          Snacks.debug.backtrace()\n        end\n\n        -- Override print to use snacks for `:=` command\n        if vim.fn.has(\"nvim-0.11\") == 1 then\n          vim._print = function(_, ...)\n            dd(...)\n          end\n        else\n          vim.print = _G.dd \n        end\n\n        -- Create some toggle mappings\n        Snacks.toggle.option(\"spell\", { name = \"Spelling\" }):map(\"<leader>us\")\n        Snacks.toggle.option(\"wrap\", { name = \"Wrap\" }):map(\"<leader>uw\")\n        Snacks.toggle.option(\"relativenumber\", { name = \"Relative Number\" }):map(\"<leader>uL\")\n        Snacks.toggle.diagnostics():map(\"<leader>ud\")\n        Snacks.toggle.line_number():map(\"<leader>ul\")\n        Snacks.toggle.option(\"conceallevel\", { off = 0, on = vim.o.conceallevel > 0 and vim.o.conceallevel or 2 }):map(\"<leader>uc\")\n        Snacks.toggle.treesitter():map(\"<leader>uT\")\n        Snacks.toggle.option(\"background\", { off = \"light\", on = \"dark\", name = \"Dark Background\" }):map(\"<leader>ub\")\n        Snacks.toggle.inlay_hints():map(\"<leader>uh\")\n        Snacks.toggle.indent():map(\"<leader>ug\")\n        Snacks.toggle.dim():map(\"<leader>uD\")\n      end,\n    })\n  end,\n}\n```\n\n<!-- example:end -->\n\n## 🌈 Highlight Groups\n\nSnacks defines **a lot** of highlight groups and it's impossible to document them all.\n\nInstead, you can use the picker to see all the highlight groups.\n\n```lua\nSnacks.picker.highlights({pattern = \"hl_group:^Snacks\"})\n```\n"
  },
  {
    "path": "doc/snacks.nvim-animate.txt",
    "content": "*snacks-animate*                             snacks animate docs\n\n==============================================================================\nTable of Contents                      *snacks.nvim-animate-table-of-contents*\n\n1. Setup                                           |snacks.nvim-animate-setup|\n2. Config                                         |snacks.nvim-animate-config|\n3. Types                                           |snacks.nvim-animate-types|\n4. Module                                         |snacks.nvim-animate-module|\n  - Snacks.animate()             |snacks.nvim-animate-module-snacks.animate()|\n  - Snacks.animate.add()     |snacks.nvim-animate-module-snacks.animate.add()|\n  - Snacks.animate.del()     |snacks.nvim-animate-module-snacks.animate.del()|\n  - Snacks.animate.enabled()|snacks.nvim-animate-module-snacks.animate.enabled()|\nEfficient animation library including over 45 easing functions:\n\n- Emmanuel Oga’s easing functions <https://github.com/EmmanuelOga/easing>\n- Easing functions overview <https://github.com/kikito/tween.lua?tab=readme-ov-file#easing-functions>\n\nThere’s at any given time at most one timer running, that takes care of all\nactive animations, controlled by the `fps` setting.\n\nYou can at any time disable all animations with:\n\n- `vim.g.snacks_animate = false` globally\n- `vim.b.snacks_animate = false` locally for the buffer\n\nDoing this, will disable `scroll`, `indent`, `dim` and all other animations.\n\n\n==============================================================================\n1. Setup                                           *snacks.nvim-animate-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        animate = {\n          -- your animate configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                         *snacks.nvim-animate-config*\n\n>lua\n    ---@class snacks.animate.Config\n    ---@field easing? snacks.animate.easing|snacks.animate.easing.Fn\n    {\n      ---@type snacks.animate.Duration|number\n      duration = 20, -- ms per step\n      easing = \"linear\",\n      fps = 120, -- frames per second. Global setting for all animations\n    }\n<\n\n\n==============================================================================\n3. Types                                           *snacks.nvim-animate-types*\n\nAll easing functions take these parameters:\n\n- `t` _(time)_: should go from 0 to duration\n- `b` _(begin)_: starting value of the property\n- `c` _(change)_: ending value of the property - starting value\n- `d` _(duration)_: total duration of the animation\n\nSome functions allow additional modifiers, like the elastic functions which\nalso can receive an amplitud and a period parameters (defaults are included)\n\n>lua\n    ---@alias snacks.animate.easing.Fn fun(t: number, b: number, c: number, d: number): number\n<\n\nDuration can be specified as the total duration or the duration per step. When\nboth are specified, the minimum of both is used.\n\n>lua\n    ---@class snacks.animate.Duration\n    ---@field step? number duration per step in ms\n    ---@field total? number total duration in ms\n<\n\n>lua\n    ---@class snacks.animate.Opts: snacks.animate.Config\n    ---@field buf? number optional buffer to check if animations should be enabled\n    ---@field int? boolean interpolate the value to an integer\n    ---@field id? number|string unique identifier for the animation\n<\n\n>lua\n    ---@class snacks.animate.ctx\n    ---@field anim snacks.animate.Animation\n    ---@field prev number\n    ---@field done boolean\n<\n\n>lua\n    ---@alias snacks.animate.cb fun(value:number, ctx: snacks.animate.ctx)\n<\n\n\n==============================================================================\n4. Module                                         *snacks.nvim-animate-module*\n\n\n`Snacks.animate()`                                          *Snacks.animate()*\n\n>lua\n    ---@type fun(from: number, to: number, cb: snacks.animate.cb, opts?: snacks.animate.Opts): snacks.animate.Animation\n    Snacks.animate()\n<\n\n\n`Snacks.animate.add()`                                  *Snacks.animate.add()*\n\nAdd an animation\n\n>lua\n    ---@param from number\n    ---@param to number\n    ---@param cb snacks.animate.cb\n    ---@param opts? snacks.animate.Opts\n    Snacks.animate.add(from, to, cb, opts)\n<\n\n\n`Snacks.animate.del()`                                  *Snacks.animate.del()*\n\nDelete an animation\n\n>lua\n    ---@param id number|string\n    Snacks.animate.del(id)\n<\n\n\n`Snacks.animate.enabled()`                          *Snacks.animate.enabled()*\n\nCheck if animations are enabled. Will return false if `snacks_animate` is set\nto false or if the buffer local variable `snacks_animate` is set to false.\n\n>lua\n    ---@param opts? {buf?: number, name?: string}\n    Snacks.animate.enabled(opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-bigfile.txt",
    "content": "*snacks-bigfile*                             snacks bigfile docs\n\n==============================================================================\nTable of Contents                      *snacks.nvim-bigfile-table-of-contents*\n\n1. Setup                                           |snacks.nvim-bigfile-setup|\n2. Config                                         |snacks.nvim-bigfile-config|\n`bigfile` adds a new filetype `bigfile` to Neovim that triggers when the file\nis larger than the configured size. This automatically prevents things like LSP\nand Treesitter attaching to the buffer.\n\nUse the `setup` config function to further make changes to a `bigfile` buffer.\nThe context provides the actual filetype.\n\nThe default implementation enables `syntax` for the buffer and disables\nmini.animate <https://github.com/nvim-mini/mini.animate> (if used)\n\n\n==============================================================================\n1. Setup                                           *snacks.nvim-bigfile-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        bigfile = {\n          -- your bigfile configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                         *snacks.nvim-bigfile-config*\n\n>lua\n    ---@class snacks.bigfile.Config\n    ---@field enabled? boolean\n    {\n      notify = true, -- show notification when big file detected\n      size = 1.5 * 1024 * 1024, -- 1.5MB\n      line_length = 1000, -- average line length (useful for minified files)\n      -- Enable or disable features when big file detected\n      ---@param ctx {buf: number, ft:string}\n      setup = function(ctx)\n        if vim.fn.exists(\":NoMatchParen\") ~= 0 then\n          vim.cmd([[NoMatchParen]])\n        end\n        Snacks.util.wo(0, { foldmethod = \"manual\", statuscolumn = \"\", conceallevel = 0 })\n        vim.b.completion = false\n        vim.b.minianimate_disable = true\n        vim.b.minihipatterns_disable = true\n        vim.schedule(function()\n          if vim.api.nvim_buf_is_valid(ctx.buf) then\n            vim.bo[ctx.buf].syntax = ctx.ft\n          end\n        end)\n      end,\n    }\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-bufdelete.txt",
    "content": "*snacks-bufdelete*                         snacks bufdelete docs\n\n==============================================================================\nTable of Contents                    *snacks.nvim-bufdelete-table-of-contents*\n\n1. Types                                         |snacks.nvim-bufdelete-types|\n2. Module                                       |snacks.nvim-bufdelete-module|\n  - Snacks.bufdelete()       |snacks.nvim-bufdelete-module-snacks.bufdelete()|\n  - Snacks.bufdelete.all()|snacks.nvim-bufdelete-module-snacks.bufdelete.all()|\n  - Snacks.bufdelete.delete()|snacks.nvim-bufdelete-module-snacks.bufdelete.delete()|\n  - Snacks.bufdelete.other()|snacks.nvim-bufdelete-module-snacks.bufdelete.other()|\nDelete buffers without disrupting window layout.\n\nIf the buffer you want to close has changes, a prompt will be shown to save or\ndiscard.\n\n\n==============================================================================\n1. Types                                         *snacks.nvim-bufdelete-types*\n\n>lua\n    ---@class snacks.bufdelete.Opts\n    ---@field buf? number Buffer to delete. Defaults to the current buffer\n    ---@field file? string Delete buffer by file name. If provided, `buf` is ignored\n    ---@field force? boolean Delete the buffer even if it is modified\n    ---@field filter? fun(buf: number): boolean Filter buffers to delete\n    ---@field wipe? boolean Wipe the buffer instead of deleting it (see `:h :bwipeout`)\n<\n\n\n==============================================================================\n2. Module                                       *snacks.nvim-bufdelete-module*\n\n\n`Snacks.bufdelete()`                                      *Snacks.bufdelete()*\n\n>lua\n    ---@type fun(buf?: number|snacks.bufdelete.Opts)\n    Snacks.bufdelete()\n<\n\n\n`Snacks.bufdelete.all()`                              *Snacks.bufdelete.all()*\n\nDelete all buffers\n\n>lua\n    ---@param opts? snacks.bufdelete.Opts\n    Snacks.bufdelete.all(opts)\n<\n\n\n`Snacks.bufdelete.delete()`                           *Snacks.bufdelete.delete()*\n\nDelete a buffer: - either the current buffer if `buf` is not provided - or the\nbuffer `buf` if it is a number - or every buffer for which `buf` returns true\nif it is a function\n\n>lua\n    ---@param opts? number|snacks.bufdelete.Opts\n    Snacks.bufdelete.delete(opts)\n<\n\n\n`Snacks.bufdelete.other()`                           *Snacks.bufdelete.other()*\n\nDelete all buffers except the current one\n\n>lua\n    ---@param opts? snacks.bufdelete.Opts\n    Snacks.bufdelete.other(opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-dashboard.txt",
    "content": "*snacks-dashboard*                         snacks dashboard docs\n\n==============================================================================\nTable of Contents                    *snacks.nvim-dashboard-table-of-contents*\n\n1. Features                                   |snacks.nvim-dashboard-features|\n2. Usage                                         |snacks.nvim-dashboard-usage|\n  - Section actions              |snacks.nvim-dashboard-usage-section-actions|\n  - Item text                          |snacks.nvim-dashboard-usage-item-text|\n3. Setup                                         |snacks.nvim-dashboard-setup|\n4. Config                                       |snacks.nvim-dashboard-config|\n5. Examples                                   |snacks.nvim-dashboard-examples|\n  - advanced                         |snacks.nvim-dashboard-examples-advanced|\n  - chafa                               |snacks.nvim-dashboard-examples-chafa|\n  - compact_files               |snacks.nvim-dashboard-examples-compact_files|\n  - doom                                 |snacks.nvim-dashboard-examples-doom|\n  - files                               |snacks.nvim-dashboard-examples-files|\n  - github                             |snacks.nvim-dashboard-examples-github|\n  - pokemon                           |snacks.nvim-dashboard-examples-pokemon|\n  - startify                         |snacks.nvim-dashboard-examples-startify|\n6. Styles                                       |snacks.nvim-dashboard-styles|\n  - dashboard                         |snacks.nvim-dashboard-styles-dashboard|\n7. Types                                         |snacks.nvim-dashboard-types|\n8. Module                                       |snacks.nvim-dashboard-module|\n  - Snacks.dashboard()       |snacks.nvim-dashboard-module-snacks.dashboard()|\n  - Snacks.dashboard.have_plugin()|snacks.nvim-dashboard-module-snacks.dashboard.have_plugin()|\n  - Snacks.dashboard.health()|snacks.nvim-dashboard-module-snacks.dashboard.health()|\n  - Snacks.dashboard.icon()|snacks.nvim-dashboard-module-snacks.dashboard.icon()|\n  - Snacks.dashboard.oldfiles()|snacks.nvim-dashboard-module-snacks.dashboard.oldfiles()|\n  - Snacks.dashboard.open()|snacks.nvim-dashboard-module-snacks.dashboard.open()|\n  - Snacks.dashboard.pick()|snacks.nvim-dashboard-module-snacks.dashboard.pick()|\n  - Snacks.dashboard.sections.header()|snacks.nvim-dashboard-module-snacks.dashboard.sections.header()|\n  - Snacks.dashboard.sections.keys()|snacks.nvim-dashboard-module-snacks.dashboard.sections.keys()|\n  - Snacks.dashboard.sections.projects()|snacks.nvim-dashboard-module-snacks.dashboard.sections.projects()|\n  - Snacks.dashboard.sections.recent_files()|snacks.nvim-dashboard-module-snacks.dashboard.sections.recent_files()|\n  - Snacks.dashboard.sections.session()|snacks.nvim-dashboard-module-snacks.dashboard.sections.session()|\n  - Snacks.dashboard.sections.startup()|snacks.nvim-dashboard-module-snacks.dashboard.sections.startup()|\n  - Snacks.dashboard.sections.terminal()|snacks.nvim-dashboard-module-snacks.dashboard.sections.terminal()|\n  - Snacks.dashboard.setup()|snacks.nvim-dashboard-module-snacks.dashboard.setup()|\n  - Snacks.dashboard.update()|snacks.nvim-dashboard-module-snacks.dashboard.update()|\n9. Links                                         |snacks.nvim-dashboard-links|\n\n==============================================================================\n1. Features                                   *snacks.nvim-dashboard-features*\n\n- declarative configuration\n- flexible layouts\n- multiple vertical panes\n- built-in sections:\n    - **header**: show a header\n    - **keys**: show keymaps\n    - **projects**: show recent projects\n    - **recent_files**: show recent files\n    - **session**: session support\n    - **startup**: startup time (lazy.nvim)\n    - **terminal**: colored terminal output\n- super fast `terminal` sections with automatic caching\n\n\n==============================================================================\n2. Usage                                         *snacks.nvim-dashboard-usage*\n\nThe dashboard comes with a set of default sections, that can be customized with\n`opts.preset` or fully replaced with `opts.sections`.\n\nThe default preset comes with support for:\n\n- pickers:\n    - fzf-lua <https://github.com/ibhagwan/fzf-lua>\n    - telescope.nvim <https://github.com/nvim-telescope/telescope.nvim>\n    - mini.pick <https://github.com/nvim-mini/mini.pick>\n- session managers: (only works with lazy.nvim <https://github.com/folke/lazy.nvim>)\n    - persistence.nvim <https://github.com/folke/persistence.nvim>\n    - persisted.nvim <https://github.com/olimorris/persisted.nvim>\n    - neovim-session-manager <https://github.com/Shatur/neovim-session-manager>\n    - posession.nvim <https://github.com/jedrzejboczar/possession.nvim>\n    - mini.sessions <https://github.com/nvim-mini/mini.sessions>\n\n\nSECTION ACTIONS                  *snacks.nvim-dashboard-usage-section-actions*\n\nA section can have an `action` property that will be executed as:\n\n- a command if it starts with `:`\n- a keymap if it’s a string not starting with `:`\n- a function if it’s a function\n\n>lua\n    -- command\n    {\n      action = \":Telescope find_files\",\n      key = \"f\",\n    },\n<\n\n>lua\n    -- keymap\n    {\n      action = \"<leader>ff\",\n      key = \"f\",\n    },\n<\n\n>lua\n    -- function\n    {\n      action = function()\n        require(\"telescope.builtin\").find_files()\n      end,\n      key = \"h\",\n    },\n<\n\n\nITEM TEXT                              *snacks.nvim-dashboard-usage-item-text*\n\nEvery item should have a `text` property with an array of\n`snacks.dashboard.Text` objects. If the `text` property is not provided, the\n`snacks.dashboard.Config.formats` will be used to generate the text.\n\nIn the example below, both sections are equivalent.\n\n>lua\n    {\n      text = {\n        { \"  \", hl = \"SnacksDashboardIcon\" },\n        { \"Find File\", hl = \"SnacksDashboardDesc\", width = 50 },\n        { \"[f]\", hl = \"SnacksDashboardKey\" },\n      },\n      action = \":Telescope find_files\",\n      key = \"f\",\n    },\n<\n\n>lua\n    {\n      action = \":Telescope find_files\",\n      key = \"f\",\n      desc = \"Find File\",\n      icon = \" \",\n    },\n<\n\n\n==============================================================================\n3. Setup                                         *snacks.nvim-dashboard-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        dashboard = {\n          -- your dashboard configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n4. Config                                       *snacks.nvim-dashboard-config*\n\n>lua\n    ---@class snacks.dashboard.Config\n    ---@field enabled? boolean\n    ---@field sections snacks.dashboard.Section\n    ---@field formats table<string, snacks.dashboard.Text|fun(item:snacks.dashboard.Item, ctx:snacks.dashboard.Format.ctx):snacks.dashboard.Text>\n    {\n      width = 60,\n      row = nil, -- dashboard position. nil for center\n      col = nil, -- dashboard position. nil for center\n      pane_gap = 4, -- empty columns between vertical panes\n      autokeys = \"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\", -- autokey sequence\n      -- These settings are used by some built-in sections\n      preset = {\n        -- Defaults to a picker that supports `fzf-lua`, `telescope.nvim` and `mini.pick`\n        ---@type fun(cmd:string, opts:table)|nil\n        pick = nil,\n        -- Used by the `keys` section to show keymaps.\n        -- Set your custom keymaps here.\n        -- When using a function, the `items` argument are the default keymaps.\n        ---@type snacks.dashboard.Item[]\n        keys = {\n          { icon = \" \", key = \"f\", desc = \"Find File\", action = \":lua Snacks.dashboard.pick('files')\" },\n          { icon = \" \", key = \"n\", desc = \"New File\", action = \":ene | startinsert\" },\n          { icon = \" \", key = \"g\", desc = \"Find Text\", action = \":lua Snacks.dashboard.pick('live_grep')\" },\n          { icon = \" \", key = \"r\", desc = \"Recent Files\", action = \":lua Snacks.dashboard.pick('oldfiles')\" },\n          { icon = \" \", key = \"c\", desc = \"Config\", action = \":lua Snacks.dashboard.pick('files', {cwd = vim.fn.stdpath('config')})\" },\n          { icon = \" \", key = \"s\", desc = \"Restore Session\", section = \"session\" },\n          { icon = \"󰒲 \", key = \"L\", desc = \"Lazy\", action = \":Lazy\", enabled = package.loaded.lazy ~= nil },\n          { icon = \" \", key = \"q\", desc = \"Quit\", action = \":qa\" },\n        },\n        -- Used by the `header` section\n        header = [[\n    ███╗   ██╗███████╗ ██████╗ ██╗   ██╗██╗███╗   ███╗\n    ████╗  ██║██╔════╝██╔═══██╗██║   ██║██║████╗ ████║\n    ██╔██╗ ██║█████╗  ██║   ██║██║   ██║██║██╔████╔██║\n    ██║╚██╗██║██╔══╝  ██║   ██║╚██╗ ██╔╝██║██║╚██╔╝██║\n    ██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║\n    ╚═╝  ╚═══╝╚══════╝ ╚═════╝   ╚═══╝  ╚═╝╚═╝     ╚═╝]],\n      },\n      -- item field formatters\n      formats = {\n        icon = function(item)\n          if item.file and item.icon == \"file\" or item.icon == \"directory\" then\n            return Snacks.dashboard.icon(item.file, item.icon)\n          end\n          return { item.icon, width = 2, hl = \"icon\" }\n        end,\n        footer = { \"%s\", align = \"center\" },\n        header = { \"%s\", align = \"center\" },\n        file = function(item, ctx)\n          local fname = vim.fn.fnamemodify(item.file, \":~\")\n          fname = ctx.width and #fname > ctx.width and vim.fn.pathshorten(fname) or fname\n          if #fname > ctx.width then\n            local dir = vim.fn.fnamemodify(fname, \":h\")\n            local file = vim.fn.fnamemodify(fname, \":t\")\n            if dir and file then\n              file = file:sub(-(ctx.width - #dir - 2))\n              fname = dir .. \"/…\" .. file\n            end\n          end\n          local dir, file = fname:match(\"^(.*)/(.+)$\")\n          return dir and { { dir .. \"/\", hl = \"dir\" }, { file, hl = \"file\" } } or { { fname, hl = \"file\" } }\n        end,\n      },\n      sections = {\n        { section = \"header\" },\n        { section = \"keys\", gap = 1, padding = 1 },\n        { section = \"startup\" },\n      },\n    }\n<\n\n\n==============================================================================\n5. Examples                                   *snacks.nvim-dashboard-examples*\n\n\nADVANCED                             *snacks.nvim-dashboard-examples-advanced*\n\nA more advanced example using multiple panes\n\n>lua\n    {\n      sections = {\n        { section = \"header\" },\n        {\n          pane = 2,\n          section = \"terminal\",\n          cmd = \"colorscript -e square\",\n          height = 5,\n          padding = 1,\n        },\n        { section = \"keys\", gap = 1, padding = 1 },\n        { pane = 2, icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = 1 },\n        { pane = 2, icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 1 },\n        {\n          pane = 2,\n          icon = \" \",\n          title = \"Git Status\",\n          section = \"terminal\",\n          enabled = function()\n            return Snacks.git.get_root() ~= nil\n          end,\n          cmd = \"git status --short --branch --renames\",\n          height = 5,\n          padding = 1,\n          ttl = 5 * 60,\n          indent = 3,\n        },\n        { section = \"startup\" },\n      },\n    }\n<\n\n\nCHAFA                                   *snacks.nvim-dashboard-examples-chafa*\n\nAn example using the `chafa` command to display an image\n\n>lua\n    {\n      sections = {\n        {\n          section = \"terminal\",\n          cmd = \"chafa ~/.config/wall.png --format symbols --symbols vhalf --size 60x17 --stretch; sleep .1\",\n          height = 17,\n          padding = 1,\n        },\n        {\n          pane = 2,\n          { section = \"keys\", gap = 1, padding = 1 },\n          { section = \"startup\" },\n        },\n      },\n    }\n<\n\n\nCOMPACT_FILES                   *snacks.nvim-dashboard-examples-compact_files*\n\nA more compact version of the `files` example\n\n>lua\n    {\n      sections = {\n        { section = \"header\" },\n        { icon = \" \", title = \"Keymaps\", section = \"keys\", indent = 2, padding = 1 },\n        { icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = 1 },\n        { icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 1 },\n        { section = \"startup\" },\n      },\n    }\n<\n\n\nDOOM                                     *snacks.nvim-dashboard-examples-doom*\n\nSimilar to the Emacs Doom dashboard\n\n>lua\n    {\n      sections = {\n        { section = \"header\" },\n        { section = \"keys\", gap = 1, padding = 1 },\n        { section = \"startup\" },\n      },\n    }\n<\n\n\nFILES                                   *snacks.nvim-dashboard-examples-files*\n\nA simple example with a header, keys, recent files, and projects\n\n>lua\n    {\n      sections = {\n        { section = \"header\" },\n        { section = \"keys\", gap = 1 },\n        { icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = { 2, 2 } },\n        { icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 2 },\n        { section = \"startup\" },\n      },\n    }\n<\n\n\nGITHUB                                 *snacks.nvim-dashboard-examples-github*\n\nAdvanced example using the GitHub CLI.\n\n>lua\n    {\n      sections = {\n        { section = \"header\" },\n        {\n          pane = 2,\n          section = \"terminal\",\n          cmd = \"colorscript -e square\",\n          height = 5,\n          padding = 1,\n        },\n        { section = \"keys\", gap = 1, padding = 1 },\n        {\n          pane = 2,\n          icon = \" \",\n          desc = \"Browse Repo\",\n          padding = 1,\n          key = \"b\",\n          action = function()\n            Snacks.gitbrowse()\n          end,\n        },\n        function()\n          local in_git = Snacks.git.get_root() ~= nil\n          local cmds = {\n            {\n              title = \"Notifications\",\n              cmd = \"gh notify -s -a -n5\",\n              action = function()\n                vim.ui.open(\"https://github.com/notifications\")\n              end,\n              key = \"n\",\n              icon = \" \",\n              height = 5,\n              enabled = true,\n            },\n            {\n              title = \"Open Issues\",\n              cmd = \"gh issue list -L 3\",\n              key = \"i\",\n              action = function()\n                vim.fn.jobstart(\"gh issue list --web\", { detach = true })\n              end,\n              icon = \" \",\n              height = 7,\n            },\n            {\n              icon = \" \",\n              title = \"Open PRs\",\n              cmd = \"gh pr list -L 3\",\n              key = \"P\",\n              action = function()\n                vim.fn.jobstart(\"gh pr list --web\", { detach = true })\n              end,\n              height = 7,\n            },\n            {\n              icon = \" \",\n              title = \"Git Status\",\n              cmd = \"git --no-pager diff --stat -B -M -C\",\n              height = 10,\n            },\n          }\n          return vim.tbl_map(function(cmd)\n            return vim.tbl_extend(\"force\", {\n              pane = 2,\n              section = \"terminal\",\n              enabled = in_git,\n              padding = 1,\n              ttl = 5 * 60,\n              indent = 3,\n            }, cmd)\n          end, cmds)\n        end,\n        { section = \"startup\" },\n      },\n    }\n<\n\n\nPOKEMON                               *snacks.nvim-dashboard-examples-pokemon*\n\nPokemons, because why not?\n\n>lua\n    {\n      sections = {\n        { section = \"header\" },\n        { section = \"keys\", gap = 1, padding = 1 },\n        { section = \"startup\" },\n        {\n          section = \"terminal\",\n          cmd = \"pokemon-colorscripts -r --no-title; sleep .1\",\n          random = 10,\n          pane = 2,\n          indent = 4,\n          height = 30,\n        },\n      },\n    }\n<\n\n\nSTARTIFY                             *snacks.nvim-dashboard-examples-startify*\n\nSimilar to the Vim Startify dashboard\n\n>lua\n    {\n      formats = {\n        key = function(item)\n          return { { \"[\", hl = \"special\" }, { item.key, hl = \"key\" }, { \"]\", hl = \"special\" } }\n        end,\n      },\n      sections = {\n        { section = \"terminal\", cmd = \"fortune -s | cowsay\", hl = \"header\", padding = 1, indent = 8 },\n        { title = \"MRU\", padding = 1 },\n        { section = \"recent_files\", limit = 8, padding = 1 },\n        { title = \"MRU \", file = vim.fn.fnamemodify(\".\", \":~\"), padding = 1 },\n        { section = \"recent_files\", cwd = true, limit = 8, padding = 1 },\n        { title = \"Sessions\", padding = 1 },\n        { section = \"projects\", padding = 1 },\n        { title = \"Bookmarks\", padding = 1 },\n        { section = \"keys\" },\n      },\n    }\n<\n\n\n==============================================================================\n6. Styles                                       *snacks.nvim-dashboard-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nDASHBOARD                             *snacks.nvim-dashboard-styles-dashboard*\n\nThe default style for the dashboard. When opening the dashboard during startup,\nonly the `bo` and `wo` options are used. The other options are used with `:lua\nSnacks.dashboard()`\n\n>lua\n    {\n      zindex = 10,\n      height = 0,\n      width = 0,\n      bo = {\n        bufhidden = \"wipe\",\n        buftype = \"nofile\",\n        buflisted = false,\n        filetype = \"snacks_dashboard\",\n        swapfile = false,\n        undofile = false,\n      },\n      wo = {\n        colorcolumn = \"\",\n        cursorcolumn = false,\n        cursorline = false,\n        foldmethod = \"manual\",\n        list = false,\n        number = false,\n        relativenumber = false,\n        sidescrolloff = 0,\n        signcolumn = \"no\",\n        spell = false,\n        statuscolumn = \"\",\n        statusline = \"\",\n        winbar = \"\",\n        winhighlight = \"Normal:SnacksDashboardNormal,NormalFloat:SnacksDashboardNormal\",\n        wrap = false,\n      },\n    }\n<\n\n\n==============================================================================\n7. Types                                         *snacks.nvim-dashboard-types*\n\n>lua\n    ---@class snacks.dashboard.Item\n    ---@field indent? number\n    ---@field align? \"left\" | \"center\" | \"right\"\n    ---@field gap? number the number of empty lines between child items\n    ---@field padding? number | {[1]:number, [2]:number} bottom or {bottom, top} padding\n    --- The action to run when the section is selected or the key is pressed.\n    --- * if it's a string starting with `:`, it will be run as a command\n    --- * if it's a string, it will be executed as a keymap\n    --- * if it's a function, it will be called\n    ---@field action? snacks.dashboard.Action\n    ---@field enabled? boolean|fun(opts:snacks.dashboard.Opts):boolean if false, the section will be disabled\n    ---@field section? string the name of a section to include. See `Snacks.dashboard.sections`\n    ---@field [string] any section options\n    ---@field key? string shortcut key\n    ---@field hidden? boolean when `true`, the item will not be shown, but the key will still be assigned\n    ---@field autokey? boolean automatically assign a numerical key\n    ---@field label? string\n    ---@field desc? string\n    ---@field file? string\n    ---@field footer? string\n    ---@field header? string\n    ---@field icon? string\n    ---@field title? string\n    ---@field text? string|snacks.dashboard.Text[]\n<\n\n>lua\n    ---@alias snacks.dashboard.Format.ctx {width?:number}\n    ---@alias snacks.dashboard.Action string|fun(self:snacks.dashboard.Class)\n    ---@alias snacks.dashboard.Gen fun(self:snacks.dashboard.Class):snacks.dashboard.Section?\n    ---@alias snacks.dashboard.Section snacks.dashboard.Item|snacks.dashboard.Gen|snacks.dashboard.Section[]\n<\n\n>lua\n    ---@class snacks.dashboard.Text\n    ---@field [1] string the text\n    ---@field hl? string the highlight group\n    ---@field width? number the width used for alignment\n    ---@field align? \"left\" | \"center\" | \"right\"\n<\n\n>lua\n    ---@class snacks.dashboard.Opts: snacks.dashboard.Config\n    ---@field buf? number the buffer to use. If not provided, a new buffer will be created\n    ---@field win? number the window to use. If not provided, a new floating window will be created\n<\n\n\n==============================================================================\n8. Module                                       *snacks.nvim-dashboard-module*\n\n\n`Snacks.dashboard()`                                      *Snacks.dashboard()*\n\n>lua\n    ---@type fun(opts?: snacks.dashboard.Opts): snacks.dashboard.Class\n    Snacks.dashboard()\n<\n\n\n`Snacks.dashboard.have_plugin()`                           *Snacks.dashboard.have_plugin()*\n\nChecks if the plugin is installed. Only works with lazy.nvim\n<https://github.com/folke/lazy.nvim>\n\n>lua\n    ---@param name string\n    Snacks.dashboard.have_plugin(name)\n<\n\n\n`Snacks.dashboard.health()`                           *Snacks.dashboard.health()*\n\n>lua\n    Snacks.dashboard.health()\n<\n\n\n`Snacks.dashboard.icon()`                            *Snacks.dashboard.icon()*\n\nGet an icon\n\n>lua\n    ---@param name string\n    ---@param cat? string\n    ---@return snacks.dashboard.Text\n    Snacks.dashboard.icon(name, cat)\n<\n\n\n`Snacks.dashboard.oldfiles()`                           *Snacks.dashboard.oldfiles()*\n\n>lua\n    ---@param opts? {filter?: table<string, boolean>}\n    ---@return fun():string?\n    Snacks.dashboard.oldfiles(opts)\n<\n\n\n`Snacks.dashboard.open()`                            *Snacks.dashboard.open()*\n\n>lua\n    ---@param opts? snacks.dashboard.Opts\n    ---@return snacks.dashboard.Class\n    Snacks.dashboard.open(opts)\n<\n\n\n`Snacks.dashboard.pick()`                            *Snacks.dashboard.pick()*\n\nUsed by the default preset to pick something\n\n>lua\n    ---@param cmd? string\n    Snacks.dashboard.pick(cmd, opts)\n<\n\n\n`Snacks.dashboard.sections.header()`                           *Snacks.dashboard.sections.header()*\n\n>lua\n    ---@return snacks.dashboard.Gen\n    Snacks.dashboard.sections.header()\n<\n\n\n`Snacks.dashboard.sections.keys()`                           *Snacks.dashboard.sections.keys()*\n\n>lua\n    ---@return snacks.dashboard.Gen\n    Snacks.dashboard.sections.keys()\n<\n\n\n`Snacks.dashboard.sections.projects()`                           *Snacks.dashboard.sections.projects()*\n\nGet the most recent projects based on git roots of recent files. The default\naction will change the directory to the project root, try to restore the\nsession and open the picker if the session is not restored. You can customize\nthe behavior by providing a custom action. Use `opts.dirs` to provide a list of\ndirectories to use instead of the git roots.\n\n>lua\n    ---@param opts? {limit?:number, dirs?:(string[]|fun():string[]), pick?:boolean, session?:boolean, action?:fun(dir), filter?:fun(dir:string):boolean?}\n    Snacks.dashboard.sections.projects(opts)\n<\n\n\n`Snacks.dashboard.sections.recent_files()`                           *Snacks.dashboard.sections.recent_files()*\n\nGet the most recent files, optionally filtered by the current working directory\nor a custom directory.\n\n>lua\n    ---@param opts? {limit?:number, cwd?:string|boolean, filter?:fun(file:string):boolean?}\n    ---@return snacks.dashboard.Gen\n    Snacks.dashboard.sections.recent_files(opts)\n<\n\n\n`Snacks.dashboard.sections.session()`                           *Snacks.dashboard.sections.session()*\n\nAdds a section to restore the session if any of the supported plugins are\ninstalled.\n\n>lua\n    ---@param item? snacks.dashboard.Item\n    ---@return snacks.dashboard.Item?\n    Snacks.dashboard.sections.session(item)\n<\n\n\n`Snacks.dashboard.sections.startup()`                           *Snacks.dashboard.sections.startup()*\n\nAdd the startup section\n\n>lua\n    ---@param opts? {icon?:string}\n    ---@return snacks.dashboard.Section?\n    Snacks.dashboard.sections.startup(opts)\n<\n\n\n`Snacks.dashboard.sections.terminal()`                           *Snacks.dashboard.sections.terminal()*\n\n>lua\n    ---@param opts {cmd:string|string[], ttl?:number, height?:number, width?:number, random?:number}|snacks.dashboard.Item\n    ---@return snacks.dashboard.Gen\n    Snacks.dashboard.sections.terminal(opts)\n<\n\n\n`Snacks.dashboard.setup()`                           *Snacks.dashboard.setup()*\n\nCheck if the dashboard should be opened\n\n>lua\n    Snacks.dashboard.setup()\n<\n\n\n`Snacks.dashboard.update()`                           *Snacks.dashboard.update()*\n\nUpdate the dashboard\n\n>lua\n    Snacks.dashboard.update()\n<\n\n==============================================================================\n9. Links                                         *snacks.nvim-dashboard-links*\n\n1. *image*: https://github.com/user-attachments/assets/bbf4d2cd-6fc5-4122-a462-0ca59ba89545\n2. *image*: https://github.com/user-attachments/assets/e498ef8f-83ce-4917-a720-8cb31d98ecec\n3. *image*: https://github.com/user-attachments/assets/772e84fe-b220-4841-bbe9-6e28780dc30a\n4. *image*: https://github.com/user-attachments/assets/823f702d-e5d0-449a-afd2-684e1fb97622\n5. *image*: https://github.com/user-attachments/assets/e98997b6-07d3-4162-bc06-2768b78fe353\n6. *image*: https://github.com/user-attachments/assets/747d7386-ef05-487f-9550-3e5ef94869fc\n7. *image*: https://github.com/user-attachments/assets/2fb17ecc-8bc0-48d3-a023-aa8dfc70247e\n8. *image*: https://github.com/user-attachments/assets/561eff8c-ddf0-4de9-8485-e6be18a19c0b\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-debug.txt",
    "content": "*snacks-debug*                                 snacks debug docs\n\n==============================================================================\nTable of Contents                        *snacks.nvim-debug-table-of-contents*\n\n1. Types                                             |snacks.nvim-debug-types|\n2. Module                                           |snacks.nvim-debug-module|\n  - Snacks.debug()                   |snacks.nvim-debug-module-snacks.debug()|\n  - Snacks.debug.backtrace()|snacks.nvim-debug-module-snacks.debug.backtrace()|\n  - Snacks.debug.cmd()           |snacks.nvim-debug-module-snacks.debug.cmd()|\n  - Snacks.debug.inspect()   |snacks.nvim-debug-module-snacks.debug.inspect()|\n  - Snacks.debug.log()           |snacks.nvim-debug-module-snacks.debug.log()|\n  - Snacks.debug.metrics()   |snacks.nvim-debug-module-snacks.debug.metrics()|\n  - Snacks.debug.profile()   |snacks.nvim-debug-module-snacks.debug.profile()|\n  - Snacks.debug.run()           |snacks.nvim-debug-module-snacks.debug.run()|\n  - Snacks.debug.size()         |snacks.nvim-debug-module-snacks.debug.size()|\n  - Snacks.debug.stats()       |snacks.nvim-debug-module-snacks.debug.stats()|\n  - Snacks.debug.trace()       |snacks.nvim-debug-module-snacks.debug.trace()|\n  - Snacks.debug.tracemod() |snacks.nvim-debug-module-snacks.debug.tracemod()|\n3. Links                                             |snacks.nvim-debug-links|\nUtility functions you can use in your code.\n\nPersonally, I have the code below at the top of my `init.lua`:\n\n>lua\n    _G.dd = function(...)\n      Snacks.debug.inspect(...)\n    end\n    _G.bt = function()\n      Snacks.debug.backtrace()\n    end\n    if vim.fn.has(\"nvim-0.11\") == 1 then\n      vim._print = function(_, ...)\n        dd(...)\n      end\n    else\n      vim.print = dd\n    end\n<\n\nWhat this does:\n\n- Add a global `dd(...)` you can use anywhere to quickly show a\n    notification with a pretty printed dump of the object(s)\n    with lua treesitter highlighting\n- Add a global `bt()` to show a notification with a pretty\n    backtrace.\n- Override Neovim’s `vim.print`, which is also used by `:= {something = 123}`\n\n\n==============================================================================\n1. Types                                             *snacks.nvim-debug-types*\n\n>lua\n    ---@class snacks.debug.cmd\n    ---@field cmd string|string[]\n    ---@field level? snacks.notifier.level|vim.log.levels\n    ---@field title? string\n    ---@field args? string[]\n    ---@field cwd? string\n    ---@field group? boolean\n    ---@field notify? boolean\n    ---@field footer? string\n    ---@field header? string\n    ---@field props? table<string, string|boolean|number|nil>\n<\n\n>lua\n    ---@alias snacks.debug.Trace {name: string, time: number, [number]:snacks.debug.Trace}\n    ---@alias snacks.debug.Stat {name:string, time:number, count?:number, depth?:number}\n<\n\n\n==============================================================================\n2. Module                                           *snacks.nvim-debug-module*\n\n\n`Snacks.debug()`                                              *Snacks.debug()*\n\n>lua\n    ---@type fun(...)\n    Snacks.debug()\n<\n\n\n`Snacks.debug.backtrace()`                          *Snacks.debug.backtrace()*\n\nShow a notification with a pretty backtrace\n\n>lua\n    ---@param msg? string|string[]\n    ---@param opts? snacks.notify.Opts\n    Snacks.debug.backtrace(msg, opts)\n<\n\n\n`Snacks.debug.cmd()`                                      *Snacks.debug.cmd()*\n\n>lua\n    ---@param opts snacks.debug.cmd\n    Snacks.debug.cmd(opts)\n<\n\n\n`Snacks.debug.inspect()`                              *Snacks.debug.inspect()*\n\nShow a notification with a pretty printed dump of the object(s) with lua\ntreesitter highlighting and the location of the caller\n\n>lua\n    Snacks.debug.inspect(...)\n<\n\n\n`Snacks.debug.log()`                                      *Snacks.debug.log()*\n\nLog a message to the file `./debug.log`. - a timestamp will be added to every\nmessage. - accepts multiple arguments and pretty prints them. - if the argument\nis not a string, it will be printed using `vim.inspect`. - if the message is\nsmaller than 120 characters, it will be printed on a single line.\n\n>lua\n    Snacks.debug.log(\"Hello\", { foo = \"bar\" }, 42)\n    -- 2024-11-08 08:56:52 Hello { foo = \"bar\" } 42\n<\n\n>lua\n    Snacks.debug.log(...)\n<\n\n\n`Snacks.debug.metrics()`                              *Snacks.debug.metrics()*\n\n>lua\n    Snacks.debug.metrics()\n<\n\n\n`Snacks.debug.profile()`                              *Snacks.debug.profile()*\n\nVery simple function to profile a lua function. **flush**: set to `true` to use\n`jit.flush` in every iteration. **count**: defaults to 100\n\n>lua\n    ---@param fn fun()\n    ---@param opts? {count?: number, flush?: boolean, title?: string}\n    Snacks.debug.profile(fn, opts)\n<\n\n\n`Snacks.debug.run()`                                      *Snacks.debug.run()*\n\nRun the current buffer or a range of lines. Shows the output of `print` inlined\nwith the code. Any error will be shown as a diagnostic.\n\n>lua\n    ---@param opts? {name?:string, buf?:number, print?:boolean}\n    Snacks.debug.run(opts)\n<\n\n\n`Snacks.debug.size()`                                    *Snacks.debug.size()*\n\n>lua\n    Snacks.debug.size(bytes)\n<\n\n\n`Snacks.debug.stats()`                                  *Snacks.debug.stats()*\n\n>lua\n    ---@param opts? {min?: number, show?:boolean}\n    ---@return {summary:table<string, snacks.debug.Stat>, trace:snacks.debug.Stat[], traces:snacks.debug.Trace[]}\n    Snacks.debug.stats(opts)\n<\n\n\n`Snacks.debug.trace()`                                  *Snacks.debug.trace()*\n\n>lua\n    ---@param name string?\n    Snacks.debug.trace(name)\n<\n\n\n`Snacks.debug.tracemod()`                            *Snacks.debug.tracemod()*\n\n>lua\n    ---@param modname string\n    ---@param mod? table\n    ---@param suffix? string\n    Snacks.debug.tracemod(modname, mod, suffix)\n<\n\n==============================================================================\n3. Links                                             *snacks.nvim-debug-links*\n\n1. *image*: https://github.com/user-attachments/assets/0517aed7-fbd0-42ee-8058-c213410d80a7\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-dim.txt",
    "content": "*snacks-dim*                                     snacks dim docs\n\n==============================================================================\nTable of Contents                          *snacks.nvim-dim-table-of-contents*\n\n1. Setup                                               |snacks.nvim-dim-setup|\n2. Config                                             |snacks.nvim-dim-config|\n3. Module                                             |snacks.nvim-dim-module|\n  - Snacks.dim()                         |snacks.nvim-dim-module-snacks.dim()|\n  - Snacks.dim.disable()         |snacks.nvim-dim-module-snacks.dim.disable()|\n  - Snacks.dim.enable()           |snacks.nvim-dim-module-snacks.dim.enable()|\n4. Links                                               |snacks.nvim-dim-links|\nFocus on the active scope by dimming the rest.\n\nSimilar plugins:\n\n- twilight.nvim <https://github.com/folke/twilight.nvim>\n- limelight.vim <https://github.com/junegunn/limelight.vim>\n- goyo.vim <https://github.com/junegunn/goyo.vim>\n\n\n==============================================================================\n1. Setup                                               *snacks.nvim-dim-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        dim = {\n          -- your dim configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                             *snacks.nvim-dim-config*\n\n>lua\n    ---@class snacks.dim.Config\n    {\n      ---@type snacks.scope.Config\n      scope = {\n        min_size = 5,\n        max_size = 20,\n        siblings = true,\n      },\n      -- animate scopes. Enabled by default for Neovim >= 0.10\n      -- Works on older versions but has to trigger redraws during animation.\n      ---@type snacks.animate.Config|{enabled?: boolean}\n      animate = {\n        enabled = vim.fn.has(\"nvim-0.10\") == 1,\n        easing = \"outQuad\",\n        duration = {\n          step = 20, -- ms per step\n          total = 300, -- maximum duration\n        },\n      },\n      -- what buffers to dim\n      filter = function(buf)\n        return vim.g.snacks_dim ~= false and vim.b[buf].snacks_dim ~= false and vim.bo[buf].buftype == \"\"\n      end,\n    }\n<\n\n\n==============================================================================\n3. Module                                             *snacks.nvim-dim-module*\n\n\n`Snacks.dim()`                                                  *Snacks.dim()*\n\n>lua\n    ---@type fun(opts: snacks.dim.Config)\n    Snacks.dim()\n<\n\n\n`Snacks.dim.disable()`                                  *Snacks.dim.disable()*\n\nDisable dimming\n\n>lua\n    Snacks.dim.disable()\n<\n\n\n`Snacks.dim.enable()`                                    *Snacks.dim.enable()*\n\n>lua\n    ---@param opts? snacks.dim.Config\n    Snacks.dim.enable(opts)\n<\n\n==============================================================================\n4. Links                                               *snacks.nvim-dim-links*\n\n1. *image*: https://github.com/user-attachments/assets/c0c5ffda-aaeb-4578-8a18-abee2e443a93\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-explorer.txt",
    "content": "*snacks-explorer*                           snacks explorer docs\n\n==============================================================================\nTable of Contents                     *snacks.nvim-explorer-table-of-contents*\n\n1. Usage                                          |snacks.nvim-explorer-usage|\n  - File Operations               |snacks.nvim-explorer-usage-file-operations|\n  - Navigation                         |snacks.nvim-explorer-usage-navigation|\n  - Quick Actions                   |snacks.nvim-explorer-usage-quick-actions|\n  - Git Integration               |snacks.nvim-explorer-usage-git-integration|\n  - Diagnostics                       |snacks.nvim-explorer-usage-diagnostics|\n  - Visual Mode                       |snacks.nvim-explorer-usage-visual-mode|\n2. Setup                                          |snacks.nvim-explorer-setup|\n3. Config                                        |snacks.nvim-explorer-config|\n4. Module                                        |snacks.nvim-explorer-module|\n  - Snacks.explorer()          |snacks.nvim-explorer-module-snacks.explorer()|\n  - Snacks.explorer.health()|snacks.nvim-explorer-module-snacks.explorer.health()|\n  - Snacks.explorer.open()|snacks.nvim-explorer-module-snacks.explorer.open()|\n  - Snacks.explorer.reveal()|snacks.nvim-explorer-module-snacks.explorer.reveal()|\n5. Links                                          |snacks.nvim-explorer-links|\nA file explorer for snacks. This is actually a picker\n<https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#explorer> in\ndisguise.\n\nThis module provide a shortcut to open the explorer picker and a setup function\nto replace netrw with the explorer.\n\nWhen the explorer and `replace_netrw` is enabled, the explorer will be opened:\n\n- when you start `nvim` with a directory\n- when you open a directory in vim\n\nConfiguring the explorer picker is done with the picker options\n<https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#explorer>.\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        explorer = {\n          -- your explorer configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        },\n        picker = {\n          sources = {\n            explorer = {\n              -- your explorer picker configuration comes here\n              -- or leave it empty to use the default settings\n            }\n          }\n        }\n      }\n    }\n<\n\n\n==============================================================================\n1. Usage                                          *snacks.nvim-explorer-usage*\n\n\nFILE OPERATIONS                   *snacks.nvim-explorer-usage-file-operations*\n\nThe explorer provides powerful file operations with an intuitive\nselection-based workflow.\n\n\nMOVING AND COPYING FILES ~\n\nThe most efficient way to move or copy multiple files:\n\n1. **Select files** with `<Tab>` (works on multiple files)\n2. **Navigate** to the target directory\n3. **Execute** the operation:- Press `m` to **move** selected files to the current directory\n- Press `c` to **copy** selected files to the current directory\n\n\n\n>\n    Example workflow:\n    1. Navigate to source files\n    2. Press <Tab> on file1.txt\n    3. Press <Tab> on file2.txt (both now selected)\n    4. Navigate to target directory\n    5. Press 'm' → files are moved!\n<\n\n**Single file operations:**\n\n- `m` on a single file (no selection) → renames the file\n- `c` on a single file (no selection) → prompts for new name to copy to\n- `r` → rename current file\n- `d` → delete current/selected files\n\n\nCOPY/PASTE WITH REGISTERS ~\n\nAlternative workflow using yank and paste:\n\n1. **Select files** with `<Tab>` or visual mode\n2. Press `y` to **yank** file paths to register\n3. Navigate to target directory\n4. Press `p` to **paste** (copies files from register)\n\nThis works across different explorer instances and even after\nclosing/reopening!\n\n\nOTHER FILE OPERATIONS ~\n\n- `a` → **Add** new file or directory (directories end with `/`)\n- `d` → **Delete** files (uses system trash if available, see `:checkhealth snacks`)\n- `o` → **Open** file with system application\n- `u` → **Update/refresh** the file tree\n\n\nNAVIGATION                             *snacks.nvim-explorer-usage-navigation*\n\n- `<CR>` or `l` → Open file or toggle directory\n- `h` → Close directory\n- `<BS>` → Go up one directory\n- `.` → Focus on current directory (set as cwd)\n- `H` → Toggle hidden files\n- `I` → Toggle ignored files (from gitignore)\n- `Z` → Close all directories\n\n\nQUICK ACTIONS                       *snacks.nvim-explorer-usage-quick-actions*\n\n- `<leader>/` → Grep in current directory\n- `<c-t>` → Open terminal in current directory\n- `<c-c>` → Change tab directory to current directory\n- `P` → Toggle preview\n\n\nGIT INTEGRATION                   *snacks.nvim-explorer-usage-git-integration*\n\nWhen `git_status = true` (default), files show git status indicators:\n\n- `]g` / `[g` → Jump to next/previous git change\n- Directories show aggregate status of contained files\n\n\nDIAGNOSTICS                           *snacks.nvim-explorer-usage-diagnostics*\n\nWhen `diagnostics = true` (default), files show diagnostic indicators:\n\n- `]d` / `[d` → Jump to next/previous diagnostic\n- `]e` / `[e` → Jump to next/previous error\n- `]w` / `[w` → Jump to next/previous warning\n\n\nVISUAL MODE                           *snacks.nvim-explorer-usage-visual-mode*\n\nYou can use visual mode (`v` or `V`) to select multiple files, then:\n\n- `y` → Yank selected file paths\n- Any other operation works on visual selection\n\n\n==============================================================================\n2. Setup                                          *snacks.nvim-explorer-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        explorer = {\n          -- your explorer configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n3. Config                                        *snacks.nvim-explorer-config*\n\nThese are just the general explorer settings. To configure the explorer picker,\nsee `snacks.picker.explorer.Config`\n\n>lua\n    ---@class snacks.explorer.Config\n    {\n      replace_netrw = true, -- Replace netrw with the snacks explorer\n      trash = true, -- Use the system trash when deleting files\n    }\n<\n\n\n==============================================================================\n4. Module                                        *snacks.nvim-explorer-module*\n\n\n`Snacks.explorer()`                                        *Snacks.explorer()*\n\n>lua\n    ---@type fun(opts?: snacks.picker.explorer.Config): snacks.Picker\n    Snacks.explorer()\n<\n\n\n`Snacks.explorer.health()`                          *Snacks.explorer.health()*\n\n>lua\n    Snacks.explorer.health()\n<\n\n\n`Snacks.explorer.open()`                              *Snacks.explorer.open()*\n\nShortcut to open the explorer picker\n\n>lua\n    ---@param opts? snacks.picker.explorer.Config|{}\n    Snacks.explorer.open(opts)\n<\n\n\n`Snacks.explorer.reveal()`                          *Snacks.explorer.reveal()*\n\nReveals the given file/buffer or the current buffer in the explorer\n\n>lua\n    ---@param opts? {file?:string, buf?:number}\n    Snacks.explorer.reveal(opts)\n<\n\n==============================================================================\n5. Links                                          *snacks.nvim-explorer-links*\n\n1. *image*: https://github.com/user-attachments/assets/e09d25f8-8559-441c-a0f7-576d2aa57097\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-gh.txt",
    "content": "*snacks-gh*                                       snacks gh docs\n\n==============================================================================\nTable of Contents                           *snacks.nvim-gh-table-of-contents*\n\n1. Features                                          |snacks.nvim-gh-features|\n2. Requirements                                  |snacks.nvim-gh-requirements|\n3. Recommended Setup                        |snacks.nvim-gh-recommended-setup|\n4. Usage                                                |snacks.nvim-gh-usage|\n  - Available Actions                 |snacks.nvim-gh-usage-available-actions|\n  - GitHub Buffers                       |snacks.nvim-gh-usage-github-buffers|\n5. Setup                                                |snacks.nvim-gh-setup|\n6. Config                                              |snacks.nvim-gh-config|\n7. Types                                                |snacks.nvim-gh-types|\n8. Module                                              |snacks.nvim-gh-module|\n  - Snacks.gh.issue()                |snacks.nvim-gh-module-snacks.gh.issue()|\n  - Snacks.gh.pr()                      |snacks.nvim-gh-module-snacks.gh.pr()|\nA modern GitHub CLI integration for Neovim that brings GitHub issues and pull\nrequests directly into your editor.\n\n\n\n\n==============================================================================\n1. Features                                          *snacks.nvim-gh-features*\n\n- Browse and search **GitHub issues** and **pull requests** with fuzzy finding\n- View full issue/PR details including **comments**, **reactions**, and **status checks**\n- Perform GitHub actions directly from Neovim:\n    - Comment on issues and PRs\n    - Close, reopen, edit, and merge PRs\n    - Add reactions and labels\n    - Review PRs (approve, request changes, comment)\n    - Checkout PR branches locally\n    - View PR diffs with syntax highlighting\n- Customizable **keymaps** for common GitHub operations\n- Beautiful **syntax highlighting** using Treesitter\n- Open issues/PRs in your web browser\n- Yank URLs to clipboard\n- Built on top of the powerful Snacks picker <https://github.com/folke/snacks.nvim/blob/main/docs/picker.md>\n\n\n==============================================================================\n2. Requirements                                  *snacks.nvim-gh-requirements*\n\n- GitHub CLI (`gh`) <https://cli.github.com/> - must be installed and authenticated\n- Snacks picker <https://github.com/folke/snacks.nvim/blob/main/docs/picker.md> enabled\n\n\n==============================================================================\n3. Recommended Setup                        *snacks.nvim-gh-recommended-setup*\n\n>lua\n    {\n      \"folke/snacks.nvim\",\n      opts = {\n        gh = {\n          -- your gh configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        },\n        picker = {\n          sources = {\n            gh_issue = {\n              -- your gh_issue picker configuration comes here\n              -- or leave it empty to use the default settings\n            },\n            gh_pr = {\n              -- your gh_pr picker configuration comes here\n              -- or leave it empty to use the default settings\n            }\n          }\n        },\n      },\n      keys = {\n        { \"<leader>gi\", function() Snacks.picker.gh_issue() end, desc = \"GitHub Issues (open)\" },\n        { \"<leader>gI\", function() Snacks.picker.gh_issue({ state = \"all\" }) end, desc = \"GitHub Issues (all)\" },\n        { \"<leader>gp\", function() Snacks.picker.gh_pr() end, desc = \"GitHub Pull Requests (open)\" },\n        { \"<leader>gP\", function() Snacks.picker.gh_pr({ state = \"all\" }) end, desc = \"GitHub Pull Requests (all)\" },\n      },\n    }\n<\n\n\n==============================================================================\n4. Usage                                                *snacks.nvim-gh-usage*\n\n>lua\n    -- Browse open issues\n    Snacks.picker.gh_issue()\n    \n    -- Browse all issues (including closed)\n    Snacks.picker.gh_issue({ state = \"all\" })\n    \n    -- Browse open pull requests\n    Snacks.picker.gh_pr()\n    \n    -- Browse all pull requests\n    Snacks.picker.gh_pr({ state = \"all\" })\n    \n    -- View PR diff\n    Snacks.picker.gh_diff({ pr = 123 })\n    \n    -- Open issue/PR in buffer\n    Snacks.gh.open({ type = \"issue\", number = 123, repo = \"owner/repo\" })\n<\n\n\nAVAILABLE ACTIONS                     *snacks.nvim-gh-usage-available-actions*\n\nWhen viewing an issue or PR in the picker, press `<cr>` to show available\nactions:\n\n\n\n`Snacks.gh` makes extensive use of `Snacks.scratch` for editing comments and\ndescriptions.\n\n\n\n**Common Actions:**\n\n- **Open in buffer** - View full details with comments\n- **Open in browser** - Open in GitHub web UI\n- **Add comment** - Add a new comment\n- **Add reaction** - React with emoji\n- **Add/Remove labels** - Manage labels\n- **Close/Reopen** - Change issue/PR state\n- **Edit** - Edit title and body\n- **Yank URL** - Copy URL to clipboard\n\n**Pull Request/Issue Specific:**\n\n- **View diff** - Show changed files with syntax highlighting\n- **Checkout** - Checkout PR branch locally\n- **Merge** - Merge, squash, or rebase and merge\n- **Review** - Approve, request changes, or comment\n- **Mark as draft/ready** - Change draft status\n- and more…\n\n\n\n\nGITHUB BUFFERS                           *snacks.nvim-gh-usage-github-buffers*\n\nWhen you open an issue or PR in a buffer, you get a beautiful rendered view\nwith:\n\n- **Metadata** - Status, author, dates, labels, reactions, and assignees\n- **Description** - Full issue/PR body with markdown rendering\n- **Comments** - All comments with author info and timestamps\n- **Status Checks** - PR status checks and CI results (for PRs)\n- **Syntax Highlighting** - Full Treesitter support for markdown\n- **Folding** - Foldable sections for comments and metadata\n\n**Default Keymaps in GitHub Buffers:**\n\n  Key    Action          Description\n  ------ --------------- ------------------------------\n  <cr>   Select Action   Show available actions menu\n  i      Edit            Edit issue/PR title and body\n  a      Add Comment     Add a new comment\n  c      Close           Close the issue/PR\n  o      Reopen          Reopen a closed issue/PR\nSee the |snacks.nvim-gh-config-section| to customize these keymaps.\n\n\n==============================================================================\n5. Setup                                                *snacks.nvim-gh-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        gh = {\n          -- your gh configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n6. Config                                              *snacks.nvim-gh-config*\n\n>lua\n    ---@class snacks.gh.Config\n    {\n      --- Keymaps for GitHub buffers\n      ---@type table<string, snacks.gh.Keymap|false>?\n      keys = {\n        select  = { \"<cr>\", \"gh_actions\", desc = \"Select Action\" },\n        edit    = { \"i\"   , \"gh_edit\"   , desc = \"Edit\" },\n        comment = { \"a\"   , \"gh_comment\", desc = \"Add Comment\" },\n        close   = { \"c\"   , \"gh_close\"  , desc = \"Close\" },\n        reopen  = { \"o\"   , \"gh_reopen\" , desc = \"Reopen\" },\n      },\n      ---@type vim.wo|{}\n      wo = {\n        breakindent = true,\n        wrap = true,\n        showbreak = \"\",\n        linebreak = true,\n        number = false,\n        relativenumber = false,\n        foldexpr = \"v:lua.vim.treesitter.foldexpr()\",\n        foldmethod = \"expr\",\n        concealcursor = \"n\",\n        conceallevel = 2,\n        list = false,\n        winhighlight = Snacks.util.winhl({\n          Normal = \"SnacksGhNormal\",\n          NormalFloat = \"SnacksGhNormalFloat\",\n          FloatBorder = \"SnacksGhBorder\",\n          FloatTitle = \"SnacksGhTitle\",\n          FloatFooter = \"SnacksGhFooter\",\n        }),\n      },\n      ---@type vim.bo|{}\n      bo = {},\n      diff = {\n        min = 4, -- minimum number of lines changed to show diff\n        wrap = 80, -- wrap diff lines at this length\n      },\n      scratch = {\n        height = 15, -- height of scratch window\n      },\n      icons = {\n        logo = \" \",\n        user= \" \",\n        checkmark = \" \",\n        crossmark = \" \",\n        block = \"■\",\n        file = \" \",\n        checks = {\n          pending = \" \",\n          success = \" \",\n          failure = \"\",\n          skipped = \" \",\n        },\n        issue = {\n          open      = \" \",\n          completed = \" \",\n          other     = \" \"\n        },\n        pr = {\n          open   = \" \",\n          closed = \" \",\n          merged = \" \",\n          draft  = \" \",\n          other  = \" \",\n        },\n        review = {\n          approved           = \" \",\n          changes_requested  = \" \",\n          commented          = \" \",\n          dismissed          = \" \",\n          pending            = \" \",\n        },\n        merge_status = {\n          clean    = \" \",\n          dirty    = \" \",\n          blocked  = \" \",\n          unstable = \" \"\n        },\n        reactions = {\n          thumbs_up   = \"👍\",\n          thumbs_down = \"👎\",\n          eyes        = \"👀\",\n          confused    = \"😕\",\n          heart       = \"❤️\",\n          hooray      = \"🎉\",\n          laugh       = \"😄\",\n          rocket      = \"🚀\",\n        },\n      },\n    }\n<\n\n\n==============================================================================\n7. Types                                                *snacks.nvim-gh-types*\n\n>lua\n    ---@alias snacks.gh.Keymap.fn fun(item:snacks.picker.gh.Item, buf:snacks.gh.Buf)\n    ---@class snacks.gh.Keymap: vim.keymap.set.Opts\n    ---@field [1] string lhs\n    ---@field [2] string|snacks.gh.Keymap.fn rhs\n    ---@field mode? string|string[] defaults to `n`\n<\n\n\n==============================================================================\n8. Module                                              *snacks.nvim-gh-module*\n\n>lua\n    ---@class snacks.gh\n    ---@field api snacks.gh.api\n    ---@field item snacks.picker.gh.Item\n    Snacks.gh = {}\n<\n\n\n`Snacks.gh.issue()`                                        *Snacks.gh.issue()*\n\n>lua\n    ---@param opts? snacks.picker.gh.issue.Config\n    Snacks.gh.issue(opts)\n<\n\n\n`Snacks.gh.pr()`                                              *Snacks.gh.pr()*\n\n>lua\n    ---@param opts? snacks.picker.gh.pr.Config\n    Snacks.gh.pr(opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-git.txt",
    "content": "*snacks-git*                                     snacks git docs\n\n==============================================================================\nTable of Contents                          *snacks.nvim-git-table-of-contents*\n\n1. Styles                                             |snacks.nvim-git-styles|\n  - blame_line                             |snacks.nvim-git-styles-blame_line|\n2. Module                                             |snacks.nvim-git-module|\n  - Snacks.git.blame_line()   |snacks.nvim-git-module-snacks.git.blame_line()|\n  - Snacks.git.get_root()       |snacks.nvim-git-module-snacks.git.get_root()|\n\n==============================================================================\n1. Styles                                             *snacks.nvim-git-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nBLAME_LINE                                 *snacks.nvim-git-styles-blame_line*\n\n>lua\n    {\n      width = 0.6,\n      height = 0.6,\n      border = true,\n      title = \" Git Blame \",\n      title_pos = \"center\",\n      ft = \"git\",\n    }\n<\n\n\n==============================================================================\n2. Module                                             *snacks.nvim-git-module*\n\n\n`Snacks.git.blame_line()`                            *Snacks.git.blame_line()*\n\nShow git log for the current line.\n\n>lua\n    ---@param opts? snacks.terminal.Opts | {count?: number}\n    Snacks.git.blame_line(opts)\n<\n\n\n`Snacks.git.get_root()`                                *Snacks.git.get_root()*\n\nGets the git root for a buffer or path. Defaults to the current buffer.\n\n>lua\n    ---@param path? number|string buffer or path\n    ---@return string?\n    Snacks.git.get_root(path)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-gitbrowse.txt",
    "content": "*snacks-gitbrowse*                         snacks gitbrowse docs\n\n==============================================================================\nTable of Contents                    *snacks.nvim-gitbrowse-table-of-contents*\n\n1. Setup                                         |snacks.nvim-gitbrowse-setup|\n2. Config                                       |snacks.nvim-gitbrowse-config|\n3. Types                                         |snacks.nvim-gitbrowse-types|\n4. Module                                       |snacks.nvim-gitbrowse-module|\n  - Snacks.gitbrowse()       |snacks.nvim-gitbrowse-module-snacks.gitbrowse()|\n  - Snacks.gitbrowse.get_url()|snacks.nvim-gitbrowse-module-snacks.gitbrowse.get_url()|\n  - Snacks.gitbrowse.open()|snacks.nvim-gitbrowse-module-snacks.gitbrowse.open()|\nOpen the repo of the active file in the browser (e.g., GitHub)\n\n\n==============================================================================\n1. Setup                                         *snacks.nvim-gitbrowse-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        gitbrowse = {\n          -- your gitbrowse configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                       *snacks.nvim-gitbrowse-config*\n\n>lua\n    ---@class snacks.gitbrowse.Config\n    ---@field url_patterns? table<string, table<string, string|fun(fields:snacks.gitbrowse.Fields):string>>\n    {\n      notify = true, -- show notification on open\n      -- Handler to open the url in a browser\n      ---@param url string\n      open = function(url)\n        if vim.fn.has(\"nvim-0.10\") == 0 then\n          require(\"lazy.util\").open(url, { system = true })\n          return\n        end\n        vim.ui.open(url)\n      end,\n      ---@type \"repo\" | \"branch\" | \"file\" | \"commit\" | \"permalink\"\n      what = \"commit\", -- what to open. not all remotes support all types\n      commit = nil, ---@type string?\n      branch = nil, ---@type string?\n      line_start = nil, ---@type number?\n      line_end = nil, ---@type number?\n      -- patterns to transform remotes to an actual URL\n      remote_patterns = {\n        { \"^(https?://.*)%.git$\"              , \"%1\" },\n        { \"^git@(.+):(.+)%.git$\"              , \"https://%1/%2\" },\n        { \"^git@(.+):(.+)$\"                   , \"https://%1/%2\" },\n        { \"^git@(.+)/(.+)$\"                   , \"https://%1/%2\" },\n        { \"^org%-%d+@(.+):(.+)%.git$\"         , \"https://%1/%2\" },\n        { \"^ssh://git@(.*)$\"                  , \"https://%1\" },\n        { \"^ssh://([^:/]+)(:%d+)/(.*)$\"       , \"https://%1/%3\" },\n        { \"^ssh://([^/]+)/(.*)$\"              , \"https://%1/%2\" },\n        { \"ssh%.dev%.azure%.com/v3/(.*)/(.*)$\", \"dev.azure.com/%1/_git/%2\" },\n        { \"^https://%w*@(.*)\"                 , \"https://%1\" },\n        { \"^git@(.*)\"                         , \"https://%1\" },\n        { \":%d+\"                              , \"\" },\n        { \"%.git$\"                            , \"\" },\n      },\n      url_patterns = {\n        [\"github%.com\"] = {\n          branch = \"/tree/{branch}\",\n          file = \"/blob/{branch}/{file}#L{line_start}-L{line_end}\",\n          permalink = \"/blob/{commit}/{file}#L{line_start}-L{line_end}\",\n          commit = \"/commit/{commit}\",\n        },\n        [\"gitlab%.com\"] = {\n          branch = \"/-/tree/{branch}\",\n          file = \"/-/blob/{branch}/{file}#L{line_start}-{line_end}\",\n          permalink = \"/-/blob/{commit}/{file}#L{line_start}-{line_end}\",\n          commit = \"/-/commit/{commit}\",\n        },\n        [\"bitbucket%.org\"] = {\n          branch = \"/src/{branch}\",\n          file = \"/src/{branch}/{file}#lines-{line_start}-L{line_end}\",\n          permalink = \"/src/{commit}/{file}#lines-{line_start}-L{line_end}\",\n          commit = \"/commits/{commit}\",\n        },\n        [\"git.sr.ht\"] = {\n          branch = \"/tree/{branch}\",\n          file = \"/tree/{branch}/item/{file}\",\n          permalink = \"/tree/{commit}/item/{file}#L{line_start}\",\n          commit = \"/commit/{commit}\",\n        },\n      },\n    }\n<\n\n\n==============================================================================\n3. Types                                         *snacks.nvim-gitbrowse-types*\n\n>lua\n    ---@class snacks.gitbrowse.Fields\n    ---@field branch? string\n    ---@field file? string\n    ---@field line_start? number\n    ---@field line_end? number\n    ---@field commit? string\n    ---@field line_count? number\n<\n\n\n==============================================================================\n4. Module                                       *snacks.nvim-gitbrowse-module*\n\n\n`Snacks.gitbrowse()`                                      *Snacks.gitbrowse()*\n\n>lua\n    ---@type fun(opts?: snacks.gitbrowse.Config)\n    Snacks.gitbrowse()\n<\n\n\n`Snacks.gitbrowse.get_url()`                           *Snacks.gitbrowse.get_url()*\n\n>lua\n    ---@param repo string\n    ---@param fields snacks.gitbrowse.Fields\n    ---@param opts? snacks.gitbrowse.Config\n    Snacks.gitbrowse.get_url(repo, fields, opts)\n<\n\n\n`Snacks.gitbrowse.open()`                            *Snacks.gitbrowse.open()*\n\n>lua\n    ---@param opts? snacks.gitbrowse.Config\n    Snacks.gitbrowse.open(opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-health.txt",
    "content": "*snacks-health*                               snacks health docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-health-table-of-contents*\n\n1. Types                                            |snacks.nvim-health-types|\n2. Module                                          |snacks.nvim-health-module|\n  - Snacks.health.check()    |snacks.nvim-health-module-snacks.health.check()|\n  - Snacks.health.has_lang()|snacks.nvim-health-module-snacks.health.has_lang()|\n  - Snacks.health.have_tool()|snacks.nvim-health-module-snacks.health.have_tool()|\n\n==============================================================================\n1. Types                                            *snacks.nvim-health-types*\n\n>lua\n    ---@class snacks.health.Tool\n    ---@field cmd string|string[]\n    ---@field version? string|false\n    ---@field enabled? boolean\n<\n\n>lua\n    ---@alias snacks.health.Tool.spec (string|snacks.health.Tool)[]|snacks.health.Tool|string\n<\n\n\n==============================================================================\n2. Module                                          *snacks.nvim-health-module*\n\n>lua\n    ---@class snacks.health\n    ---@field ok fun(msg: string)\n    ---@field warn fun(msg: string)\n    ---@field error fun(msg: string)\n    ---@field info fun(msg: string)\n    ---@field start fun(msg: string)\n    Snacks.health = {}\n<\n\n\n`Snacks.health.check()`                                *Snacks.health.check()*\n\n>lua\n    Snacks.health.check()\n<\n\n\n`Snacks.health.has_lang()`                          *Snacks.health.has_lang()*\n\nCheck if the given languages are available in treesitter\n\n>lua\n    ---@param langs string[]|string\n    Snacks.health.has_lang(langs)\n<\n\n\n`Snacks.health.have_tool()`                        *Snacks.health.have_tool()*\n\nCheck if any of the tools are available, with an optional version check\n\n>lua\n    ---@param tools snacks.health.Tool.spec\n    Snacks.health.have_tool(tools)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-image.txt",
    "content": "*snacks-image*                                 snacks image docs\n\n==============================================================================\nTable of Contents                        *snacks.nvim-image-table-of-contents*\n\n1. Features                                       |snacks.nvim-image-features|\n2. Setup                                             |snacks.nvim-image-setup|\n3. Config                                           |snacks.nvim-image-config|\n4. Styles                                           |snacks.nvim-image-styles|\n  - snacks_image                       |snacks.nvim-image-styles-snacks_image|\n5. Types                                             |snacks.nvim-image-types|\n6. Module                                           |snacks.nvim-image-module|\n  - Snacks.image.hover()       |snacks.nvim-image-module-snacks.image.hover()|\n  - Snacks.image.langs()       |snacks.nvim-image-module-snacks.image.langs()|\n  - Snacks.image.supports() |snacks.nvim-image-module-snacks.image.supports()|\n  - Snacks.image.supports_file()|snacks.nvim-image-module-snacks.image.supports_file()|\n  - Snacks.image.supports_terminal()|snacks.nvim-image-module-snacks.image.supports_terminal()|\n7. Links                                             |snacks.nvim-image-links|\n\n==============================================================================\n1. Features                                       *snacks.nvim-image-features*\n\n- Image viewer using the Kitty Graphics Protocol <https://sw.kovidgoyal.net/kitty/graphics-protocol/>.\n- open images in a wide range of formats:\n    `pdf`, `png`, `jpg`, `jpeg`, `gif`, `bmp`, `webp`, `tiff`, `heic`, `avif`, `mp4`, `mov`, `avi`, `mkv`, `webm`\n- Supports inline image rendering in:\n    `markdown`, `html`, `norg`, `tsx`, `javascript`, `css`, `vue`, `svelte`, `scss`, `latex`, `typst`\n- LaTex math expressions in `markdown` and `latex` documents\n\nTerminal support:\n\n- kitty <https://sw.kovidgoyal.net/kitty/>\n- ghostty <https://ghostty.org/>\n- wezterm <https://wezfurlong.org/wezterm/>\n    Wezterm has only limited support for the kitty graphics protocol.\n    Inline image rendering is not supported.\n- tmux <https://github.com/tmux/tmux>\n    Snacks automatically tries to enable `allow-passthrough=on` for tmux,\n    but you may need to enable it manually in your tmux configuration.\n- zellij <https://github.com/zellij-org/zellij> is **not** supported,\n    since they don’t have any support for passthrough\n\nImage will be transferred to the terminal by filename or by sending the image\ndate in case `ssh` is detected.\n\nIn some cases you may need to force snacks to detect or not detect a certain\nenvironment. You can do this by setting `SNACKS_${ENV_NAME}` to `true` or\n`false`.\n\nFor example, to force detection of **ghostty** you can set\n`SNACKS_GHOSTTY=true`.\n\nIn order to automatically display the image when opening an image file, or to\nhave imaged displayed in supported document formats like `markdown` or `html`,\nyou need to enable the `image` plugin in your `snacks` config.\n\nImageMagick <https://imagemagick.org/index.php> is required to convert images\nto the supported formats (all except PNG).\n\nIn case of issues, make sure to run `:checkhealth snacks`.\n\n\n==============================================================================\n2. Setup                                             *snacks.nvim-image-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        image = {\n          -- your image configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n3. Config                                           *snacks.nvim-image-config*\n\n>lua\n    ---@class snacks.image.Config\n    ---@field enabled? boolean enable image viewer\n    ---@field wo? vim.wo|{} options for windows showing the image\n    ---@field bo? vim.bo|{} options for the image buffer\n    ---@field formats? string[]\n    --- Resolves a reference to an image with src in a file (currently markdown only).\n    --- Return the absolute path or url to the image.\n    --- When `nil`, the path is resolved relative to the file.\n    ---@field resolve? fun(file: string, src: string): string?\n    ---@field convert? snacks.image.convert.Config\n    {\n      formats = {\n        \"png\",\n        \"jpg\",\n        \"jpeg\",\n        \"gif\",\n        \"bmp\",\n        \"webp\",\n        \"tiff\",\n        \"heic\",\n        \"avif\",\n        \"mp4\",\n        \"mov\",\n        \"avi\",\n        \"mkv\",\n        \"webm\",\n        \"pdf\",\n        \"icns\",\n      },\n      force = false, -- try displaying the image, even if the terminal does not support it\n      doc = {\n        -- enable image viewer for documents\n        -- a treesitter parser must be available for the enabled languages.\n        enabled = true,\n        -- render the image inline in the buffer\n        -- if your env doesn't support unicode placeholders, this will be disabled\n        -- takes precedence over `opts.float` on supported terminals\n        inline = true,\n        -- render the image in a floating window\n        -- only used if `opts.inline` is disabled\n        float = true,\n        max_width = 80,\n        max_height = 40,\n        -- Set to `true`, to conceal the image text when rendering inline.\n        -- (experimental)\n        ---@param lang string tree-sitter language\n        ---@param type snacks.image.Type image type\n        conceal = function(lang, type)\n          -- only conceal math expressions\n          return type == \"math\"\n        end,\n      },\n      img_dirs = { \"img\", \"images\", \"assets\", \"static\", \"public\", \"media\", \"attachments\" },\n      -- window options applied to windows displaying image buffers\n      -- an image buffer is a buffer with `filetype=image`\n      wo = {\n        wrap = false,\n        number = false,\n        relativenumber = false,\n        cursorcolumn = false,\n        signcolumn = \"no\",\n        foldcolumn = \"0\",\n        list = false,\n        spell = false,\n        statuscolumn = \"\",\n      },\n      cache = vim.fn.stdpath(\"cache\") .. \"/snacks/image\",\n      debug = {\n        request = false,\n        convert = false,\n        placement = false,\n      },\n      env = {},\n      -- icons used to show where an inline image is located that is\n      -- rendered below the text.\n      icons = {\n        math = \"󰪚 \",\n        chart = \"󰄧 \",\n        image = \" \",\n      },\n      ---@class snacks.image.convert.Config\n      convert = {\n        notify = false, -- show a notification on error\n        ---@type snacks.image.args\n        mermaid = function()\n          local theme = vim.o.background == \"light\" and \"neutral\" or \"dark\"\n          return { \"-i\", \"{src}\", \"-o\", \"{file}\", \"-b\", \"transparent\", \"-t\", theme, \"-s\", \"{scale}\" }\n        end,\n        ---@type table<string,snacks.image.args>\n        magick = {\n          default = { \"{src}[0]\", \"-scale\", \"1920x1080>\" }, -- default for raster images\n          vector = { \"-density\", 192, \"{src}[{page}]\" }, -- used by vector images like svg\n          math = { \"-density\", 192, \"{src}[{page}]\", \"-trim\" },\n          pdf = { \"-density\", 192, \"{src}[{page}]\", \"-background\", \"white\", \"-alpha\", \"remove\", \"-trim\" },\n        },\n      },\n      math = {\n        enabled = true, -- enable math expression rendering\n        -- in the templates below, `${header}` comes from any section in your document,\n        -- between a start/end header comment. Comment syntax is language-specific.\n        -- * start comment: `// snacks: header start`\n        -- * end comment:   `// snacks: header end`\n        typst = {\n          tpl = [[\n            #set page(width: auto, height: auto, margin: (x: 2pt, y: 2pt))\n            #show math.equation.where(block: false): set text(top-edge: \"bounds\", bottom-edge: \"bounds\")\n            #set text(size: 12pt, fill: rgb(\"${color}\"))\n            ${header}\n            ${content}]],\n        },\n        latex = {\n          font_size = \"Large\", -- see https://www.sascha-frank.com/latex-font-size.html\n          -- for latex documents, the doc packages are included automatically,\n          -- but you can add more packages here. Useful for markdown documents.\n          packages = { \"amsmath\", \"amssymb\", \"amsfonts\", \"amscd\", \"mathtools\" },\n          tpl = [[\n            \\documentclass[preview,border=0pt,varwidth,12pt]{standalone}\n            \\usepackage{${packages}}\n            \\begin{document}\n            ${header}\n            { \\${font_size} \\selectfont\n              \\color[HTML]{${color}}\n            ${content}}\n            \\end{document}]],\n        },\n      },\n    }\n<\n\n\n==============================================================================\n4. Styles                                           *snacks.nvim-image-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nSNACKS_IMAGE                           *snacks.nvim-image-styles-snacks_image*\n\n>lua\n    {\n      relative = \"cursor\",\n      border = true,\n      focusable = false,\n      backdrop = false,\n      row = 1,\n      col = 1,\n      -- width/height are automatically set by the image size unless specified below\n    }\n<\n\n\n==============================================================================\n5. Types                                             *snacks.nvim-image-types*\n\n>lua\n    ---@alias snacks.image.Size {width: number, height: number}\n    ---@alias snacks.image.Pos {[1]: number, [2]: number}\n    ---@alias snacks.image.Loc snacks.image.Pos|snacks.image.Size|{zindex?: number}\n    ---@alias snacks.image.Type \"image\"|\"math\"|\"chart\"\n<\n\n>lua\n    ---@class snacks.image.Env\n    ---@field name string\n    ---@field env? table<string, string|true>\n    ---@field terminal? string\n    ---@field supported? boolean default: false\n    ---@field placeholders? boolean default: false\n    ---@field setup? fun(): boolean?\n    ---@field transform? fun(data: string): string\n    ---@field detected? boolean\n    ---@field remote? boolean this is a remote client, so full transfer of the image data is required\n<\n\n>lua\n    ---@class snacks.image.Opts\n    ---@field pos? snacks.image.Pos (row, col) (1,0)-indexed. defaults to the top-left corner\n    ---@field range? Range4\n    ---@field conceal? boolean\n    ---@field inline? boolean render the image inline in the buffer\n    ---@field width? number\n    ---@field min_width? number\n    ---@field max_width? number\n    ---@field height? number\n    ---@field min_height? number\n    ---@field max_height? number\n    ---@field on_update? fun(placement: snacks.image.Placement)\n    ---@field on_update_pre? fun(placement: snacks.image.Placement)\n    ---@field type? snacks.image.Type\n    ---@field auto_resize? boolean\n<\n\n\n==============================================================================\n6. Module                                           *snacks.nvim-image-module*\n\n>lua\n    ---@class snacks.image\n    ---@field terminal snacks.image.terminal\n    ---@field image snacks.Image\n    ---@field placement snacks.image.Placement\n    ---@field util snacks.image.util\n    ---@field buf snacks.image.buf\n    ---@field doc snacks.image.doc\n    ---@field convert snacks.image.convert\n    ---@field inline snacks.image.inline\n    Snacks.image = {}\n<\n\n\n`Snacks.image.hover()`                                  *Snacks.image.hover()*\n\nShow the image at the cursor in a floating window\n\n>lua\n    Snacks.image.hover()\n<\n\n\n`Snacks.image.langs()`                                  *Snacks.image.langs()*\n\n>lua\n    ---@return string[]\n    Snacks.image.langs()\n<\n\n\n`Snacks.image.supports()`                            *Snacks.image.supports()*\n\nCheck if the file format is supported and the terminal supports the kitty\ngraphics protocol\n\n>lua\n    ---@param file string\n    Snacks.image.supports(file)\n<\n\n\n`Snacks.image.supports_file()`                       *Snacks.image.supports_file()*\n\nCheck if the file format is supported\n\n>lua\n    ---@param file string\n    Snacks.image.supports_file(file)\n<\n\n\n`Snacks.image.supports_terminal()`                       *Snacks.image.supports_terminal()*\n\nCheck if the terminal supports the kitty graphics protocol\n\n>lua\n    Snacks.image.supports_terminal()\n<\n\n==============================================================================\n7. Links                                             *snacks.nvim-image-links*\n\n1. *Image*: https://github.com/user-attachments/assets/4e8a686c-bf41-4989-9d74-1641ecf2835f\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-indent.txt",
    "content": "*snacks-indent*                               snacks indent docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-indent-table-of-contents*\n\n1. Setup                                            |snacks.nvim-indent-setup|\n2. Config                                          |snacks.nvim-indent-config|\n3. Types                                            |snacks.nvim-indent-types|\n4. Module                                          |snacks.nvim-indent-module|\n  - Snacks.indent.debug_win()|snacks.nvim-indent-module-snacks.indent.debug_win()|\n  - Snacks.indent.disable()|snacks.nvim-indent-module-snacks.indent.disable()|\n  - Snacks.indent.enable()  |snacks.nvim-indent-module-snacks.indent.enable()|\n5. Links                                            |snacks.nvim-indent-links|\nVisualize indent guides and scopes based on treesitter or indent.\n\nSimilar plugins:\n\n- indent-blankline.nvim <https://github.com/lukas-reineke/indent-blankline.nvim>\n- mini.indentscope <https://github.com/nvim-mini/mini.indentscope>\n\n\n==============================================================================\n1. Setup                                            *snacks.nvim-indent-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        indent = {\n          -- your indent configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                          *snacks.nvim-indent-config*\n\n>lua\n    ---@class snacks.indent.Config\n    ---@field enabled? boolean\n    {\n      indent = {\n        priority = 1,\n        enabled = true, -- enable indent guides\n        char = \"│\",\n        only_scope = false, -- only show indent guides of the scope\n        only_current = false, -- only show indent guides in the current window\n        hl = \"SnacksIndent\", ---@type string|string[] hl groups for indent guides\n        -- can be a list of hl groups to cycle through\n        -- hl = {\n        --     \"SnacksIndent1\",\n        --     \"SnacksIndent2\",\n        --     \"SnacksIndent3\",\n        --     \"SnacksIndent4\",\n        --     \"SnacksIndent5\",\n        --     \"SnacksIndent6\",\n        --     \"SnacksIndent7\",\n        --     \"SnacksIndent8\",\n        -- },\n      },\n      -- animate scopes. Enabled by default for Neovim >= 0.10\n      -- Works on older versions but has to trigger redraws during animation.\n      ---@class snacks.indent.animate: snacks.animate.Config\n      ---@field enabled? boolean\n      --- * out: animate outwards from the cursor\n      --- * up: animate upwards from the cursor\n      --- * down: animate downwards from the cursor\n      --- * up_down: animate up or down based on the cursor position\n      ---@field style? \"out\"|\"up_down\"|\"down\"|\"up\"\n      animate = {\n        enabled = vim.fn.has(\"nvim-0.10\") == 1,\n        style = \"out\",\n        easing = \"linear\",\n        duration = {\n          step = 20, -- ms per step\n          total = 500, -- maximum duration\n        },\n      },\n      ---@class snacks.indent.Scope.Config: snacks.scope.Config\n      scope = {\n        enabled = true, -- enable highlighting the current scope\n        priority = 200,\n        char = \"│\",\n        underline = false, -- underline the start of the scope\n        only_current = false, -- only show scope in the current window\n        hl = \"SnacksIndentScope\", ---@type string|string[] hl group for scopes\n      },\n      chunk = {\n        -- when enabled, scopes will be rendered as chunks, except for the\n        -- top-level scope which will be rendered as a scope.\n        enabled = false,\n        -- only show chunk scopes in the current window\n        only_current = false,\n        priority = 200,\n        hl = \"SnacksIndentChunk\", ---@type string|string[] hl group for chunk scopes\n        char = {\n          corner_top = \"┌\",\n          corner_bottom = \"└\",\n          -- corner_top = \"╭\",\n          -- corner_bottom = \"╰\",\n          horizontal = \"─\",\n          vertical = \"│\",\n          arrow = \">\",\n        },\n      },\n      -- filter for buffers to enable indent guides\n      ---@param buf number\n      ---@param win number\n      filter = function(buf, win)\n        return vim.g.snacks_indent ~= false and vim.b[buf].snacks_indent ~= false and vim.bo[buf].buftype == \"\"\n      end,\n    }\n<\n\n\n==============================================================================\n3. Types                                            *snacks.nvim-indent-types*\n\n>lua\n    ---@class snacks.indent.Scope: snacks.scope.Scope\n    ---@field win number\n    ---@field step? number\n    ---@field animate? {from: number, to: number}\n<\n\n\n==============================================================================\n4. Module                                          *snacks.nvim-indent-module*\n\n\n`Snacks.indent.debug_win()`                        *Snacks.indent.debug_win()*\n\n>lua\n    Snacks.indent.debug_win()\n<\n\n\n`Snacks.indent.disable()`                            *Snacks.indent.disable()*\n\nDisable indent guides\n\n>lua\n    Snacks.indent.disable()\n<\n\n\n`Snacks.indent.enable()`                              *Snacks.indent.enable()*\n\nEnable indent guides\n\n>lua\n    Snacks.indent.enable()\n<\n\n==============================================================================\n5. Links                                            *snacks.nvim-indent-links*\n\n1. *image*: https://github.com/user-attachments/assets/56a99495-05ab-488e-9619-574cb7ff2b7d\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-init.txt",
    "content": "*snacks-init*                                   snacks init docs\n\n==============================================================================\nTable of Contents                         *snacks.nvim-init-table-of-contents*\n\n1. Config                                            |snacks.nvim-init-config|\n2. Types                                              |snacks.nvim-init-types|\n3. Module                                            |snacks.nvim-init-module|\n  - Snacks.init.config.example()|snacks.nvim-init-module-snacks.init.config.example()|\n  - Snacks.init.config.get()|snacks.nvim-init-module-snacks.init.config.get()|\n  - Snacks.init.config.merge()|snacks.nvim-init-module-snacks.init.config.merge()|\n  - Snacks.init.config.style()|snacks.nvim-init-module-snacks.init.config.style()|\n  - Snacks.init.setup()          |snacks.nvim-init-module-snacks.init.setup()|\n\n==============================================================================\n1. Config                                            *snacks.nvim-init-config*\n\n>lua\n    ---@class snacks.Config\n    ---@field animate? snacks.animate.Config\n    ---@field bigfile? snacks.bigfile.Config\n    ---@field dashboard? snacks.dashboard.Config\n    ---@field dim? snacks.dim.Config\n    ---@field explorer? snacks.explorer.Config\n    ---@field gh? snacks.gh.Config\n    ---@field gitbrowse? snacks.gitbrowse.Config\n    ---@field image? snacks.image.Config\n    ---@field indent? snacks.indent.Config\n    ---@field input? snacks.input.Config\n    ---@field layout? snacks.layout.Config\n    ---@field lazygit? snacks.lazygit.Config\n    ---@field notifier? snacks.notifier.Config\n    ---@field picker? snacks.picker.Config\n    ---@field profiler? snacks.profiler.Config\n    ---@field quickfile? snacks.quickfile.Config\n    ---@field scope? snacks.scope.Config\n    ---@field scratch? snacks.scratch.Config\n    ---@field scroll? snacks.scroll.Config\n    ---@field statuscolumn? snacks.statuscolumn.Config\n    ---@field terminal? snacks.terminal.Config\n    ---@field toggle? snacks.toggle.Config\n    ---@field win? snacks.win.Config\n    ---@field words? snacks.words.Config\n    ---@field zen? snacks.zen.Config\n    ---@field styles? table<string, snacks.win.Config>\n    ---@field image? snacks.image.Config|{}\n    {\n      image = {\n        -- define these here, so that we don't need to load the image module\n        formats = {\n          \"png\",\n          \"jpg\",\n          \"jpeg\",\n          \"gif\",\n          \"bmp\",\n          \"webp\",\n          \"tiff\",\n          \"heic\",\n          \"avif\",\n          \"mp4\",\n          \"mov\",\n          \"avi\",\n          \"mkv\",\n          \"webm\",\n          \"pdf\",\n          \"icns\",\n        },\n      },\n    }\n<\n\n\n==============================================================================\n2. Types                                              *snacks.nvim-init-types*\n\n>lua\n    ---@class snacks.Config.base\n    ---@field example? string\n    ---@field config? fun(opts: table, defaults: table)\n<\n\n\n==============================================================================\n3. Module                                            *snacks.nvim-init-module*\n\n>lua\n    ---@class Snacks\n    ---@field animate snacks.animate\n    ---@field bigfile snacks.bigfile\n    ---@field bufdelete snacks.bufdelete\n    ---@field dashboard snacks.dashboard\n    ---@field debug snacks.debug\n    ---@field dim snacks.dim\n    ---@field explorer snacks.explorer\n    ---@field gh snacks.gh\n    ---@field git snacks.git\n    ---@field gitbrowse snacks.gitbrowse\n    ---@field health snacks.health\n    ---@field image snacks.image\n    ---@field indent snacks.indent\n    ---@field input snacks.input\n    ---@field keymap snacks.keymap\n    ---@field layout snacks.layout\n    ---@field lazygit snacks.lazygit\n    ---@field meta snacks.meta\n    ---@field notifier snacks.notifier\n    ---@field notify snacks.notify\n    ---@field picker snacks.picker\n    ---@field profiler snacks.profiler\n    ---@field quickfile snacks.quickfile\n    ---@field rename snacks.rename\n    ---@field scope snacks.scope\n    ---@field scratch snacks.scratch\n    ---@field scroll snacks.scroll\n    ---@field statuscolumn snacks.statuscolumn\n    ---@field terminal snacks.terminal\n    ---@field toggle snacks.toggle\n    ---@field util snacks.util\n    ---@field win snacks.win\n    ---@field words snacks.words\n    ---@field zen snacks.zen\n    Snacks = {}\n<\n\n\n`Snacks.init.config.example()`                      *Snacks.init.config.example()*\n\nGet an example config from the docs/examples directory.\n\n>lua\n    ---@param snack string\n    ---@param name string\n    ---@param opts? table\n    Snacks.init.config.example(snack, name, opts)\n<\n\n\n`Snacks.init.config.get()`                          *Snacks.init.config.get()*\n\n>lua\n    ---@generic T: table\n    ---@param snack string\n    ---@param defaults T\n    ---@param ... T[]\n    ---@return T\n    Snacks.init.config.get(snack, defaults, ...)\n<\n\n\n`Snacks.init.config.merge()`                      *Snacks.init.config.merge()*\n\nMerges the values similar to vim.tbl_deep_extend with the **force** behavior,\nbut the values can be any type\n\n>lua\n    ---@generic T\n    ---@param ... T\n    ---@return T\n    Snacks.init.config.merge(...)\n<\n\n\n`Snacks.init.config.style()`                      *Snacks.init.config.style()*\n\nRegister a new window style config.\n\n>lua\n    ---@param name string\n    ---@param defaults snacks.win.Config|{}\n    ---@return string\n    Snacks.init.config.style(name, defaults)\n<\n\n\n`Snacks.init.setup()`                                    *Snacks.init.setup()*\n\n>lua\n    ---@param opts snacks.Config?\n    Snacks.init.setup(opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-input.txt",
    "content": "*snacks-input*                                 snacks input docs\n\n==============================================================================\nTable of Contents                        *snacks.nvim-input-table-of-contents*\n\n1. Setup                                             |snacks.nvim-input-setup|\n2. Config                                           |snacks.nvim-input-config|\n3. Styles                                           |snacks.nvim-input-styles|\n  - input                                     |snacks.nvim-input-styles-input|\n4. Types                                             |snacks.nvim-input-types|\n5. Module                                           |snacks.nvim-input-module|\n  - Snacks.input()                   |snacks.nvim-input-module-snacks.input()|\n  - Snacks.input.disable()   |snacks.nvim-input-module-snacks.input.disable()|\n  - Snacks.input.enable()     |snacks.nvim-input-module-snacks.input.enable()|\n  - Snacks.input.input()       |snacks.nvim-input-module-snacks.input.input()|\n6. Links                                             |snacks.nvim-input-links|\nBetter `vim.ui.input`.\n\n\n==============================================================================\n1. Setup                                             *snacks.nvim-input-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        input = {\n          -- your input configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                           *snacks.nvim-input-config*\n\n>lua\n    ---@class snacks.input.Config\n    ---@field enabled? boolean\n    ---@field win? snacks.win.Config|{}\n    ---@field icon? string\n    ---@field icon_pos? snacks.input.Pos\n    ---@field prompt_pos? snacks.input.Pos\n    {\n      icon = \" \",\n      icon_hl = \"SnacksInputIcon\",\n      icon_pos = \"left\",\n      prompt_pos = \"title\",\n      win = { style = \"input\" },\n      expand = true,\n    }\n<\n\n\n==============================================================================\n3. Styles                                           *snacks.nvim-input-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nINPUT                                         *snacks.nvim-input-styles-input*\n\n>lua\n    {\n      backdrop = false,\n      position = \"float\",\n      border = true,\n      title_pos = \"center\",\n      height = 1,\n      width = 60,\n      relative = \"editor\",\n      noautocmd = true,\n      row = 2,\n      -- relative = \"cursor\",\n      -- row = -3,\n      -- col = 0,\n      wo = {\n        winhighlight = \"NormalFloat:SnacksInputNormal,FloatBorder:SnacksInputBorder,FloatTitle:SnacksInputTitle\",\n        cursorline = false,\n      },\n      bo = {\n        filetype = \"snacks_input\",\n        buftype = \"prompt\",\n      },\n      --- buffer local variables\n      b = {\n        completion = false, -- disable blink completions in input\n      },\n      keys = {\n        n_esc = { \"<esc>\", { \"cmp_close\", \"cancel\" }, mode = \"n\", expr = true },\n        i_esc = { \"<esc>\", { \"cmp_close\", \"stopinsert\" }, mode = \"i\", expr = true },\n        i_cr = { \"<cr>\", { \"cmp_accept\", \"confirm\" }, mode = { \"i\", \"n\" }, expr = true },\n        i_tab = { \"<tab>\", { \"cmp_select_next\", \"cmp\" }, mode = \"i\", expr = true },\n        i_ctrl_w = { \"<c-w>\", \"<c-s-w>\", mode = \"i\", expr = true },\n        i_up = { \"<up>\", { \"hist_up\" }, mode = { \"i\", \"n\" } },\n        i_down = { \"<down>\", { \"hist_down\" }, mode = { \"i\", \"n\" } },\n        q = \"cancel\",\n      },\n    }\n<\n\n\n==============================================================================\n4. Types                                             *snacks.nvim-input-types*\n\n>lua\n    ---@alias snacks.input.Pos \"left\"|\"title\"|false\n<\n\n>lua\n    ---@alias snacks.input.Highlight {[1]:number, [2]:number, [3]:string}\n<\n\n>lua\n    ---@class snacks.input.Opts: snacks.input.Config,{}\n    ---@field prompt? string\n    ---@field default? string\n    ---@field completion? string\n    ---@field highlight? fun(text: string): snacks.input.Highlight[]\n<\n\n\n==============================================================================\n5. Module                                           *snacks.nvim-input-module*\n\n\n`Snacks.input()`                                              *Snacks.input()*\n\n>lua\n    ---@type fun(opts: snacks.input.Opts, on_confirm: fun(value?: string)): snacks.win\n    Snacks.input()\n<\n\n\n`Snacks.input.disable()`                              *Snacks.input.disable()*\n\n>lua\n    Snacks.input.disable()\n<\n\n\n`Snacks.input.enable()`                                *Snacks.input.enable()*\n\n>lua\n    Snacks.input.enable()\n<\n\n\n`Snacks.input.input()`                                  *Snacks.input.input()*\n\n>lua\n    ---@param opts? snacks.input.Opts\n    ---@param on_confirm fun(value?: string)\n    Snacks.input.input(opts, on_confirm)\n<\n\n==============================================================================\n6. Links                                             *snacks.nvim-input-links*\n\n1. *image*: https://github.com/user-attachments/assets/f7579302-bea1-4f1c-8b3b-723c3f4ca04b\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-keymap.txt",
    "content": "*snacks-keymap*                               snacks keymap docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-keymap-table-of-contents*\n\n1. Features                                      |snacks.nvim-keymap-features|\n2. Usage                                            |snacks.nvim-keymap-usage|\n  - Filetype-specific Keymaps|snacks.nvim-keymap-usage-filetype-specific-keymaps|\n  - LSP-aware Keymaps             |snacks.nvim-keymap-usage-lsp-aware-keymaps|\n  - Standard Keymaps               |snacks.nvim-keymap-usage-standard-keymaps|\n  - Deleting Keymaps               |snacks.nvim-keymap-usage-deleting-keymaps|\n3. Types                                            |snacks.nvim-keymap-types|\n4. Module                                          |snacks.nvim-keymap-module|\n  - Snacks.keymap.del()        |snacks.nvim-keymap-module-snacks.keymap.del()|\n  - Snacks.keymap.set()        |snacks.nvim-keymap-module-snacks.keymap.set()|\nBetter `vim.keymap.set` and `vim.keymap.del` with support for filetype-specific\nand LSP client-aware keymaps.\n\n\n==============================================================================\n1. Features                                      *snacks.nvim-keymap-features*\n\n- **Filetype-specific keymaps**: Set keymaps that only apply to specific filetypes\n- **LSP-aware keymaps**: Set keymaps based on LSP client capabilities\n- **Automatic setup**: Keymaps are automatically applied to existing and new buffers\n- **Drop-in replacement**: Same API as `vim.keymap.set/del` with additional options\n- **Smart defaults**: Silent by default\n\n\n==============================================================================\n2. Usage                                            *snacks.nvim-keymap-usage*\n\n\nFILETYPE-SPECIFIC KEYMAPS *snacks.nvim-keymap-usage-filetype-specific-keymaps*\n\nSet keymaps that only apply to buffers with specific filetypes:\n\n>lua\n    -- Single filetype - execute the current lua buffer\n    Snacks.keymap.set(\"n\", \"<localleader>r\", function()\n      vim.cmd.source()\n    end, {\n      ft = \"lua\",\n      desc = \"Run Lua File\",\n    })\n    \n    -- Multiple filetypes\n    Snacks.keymap.set(\"n\", \"<leader>t\", \":TestNearest<cr>\", {\n      ft = { \"python\", \"ruby\", \"javascript\" },\n      desc = \"Run Test\",\n    })\n<\n\n\nLSP-AWARE KEYMAPS                 *snacks.nvim-keymap-usage-lsp-aware-keymaps*\n\nSet keymaps based on LSP client capabilities:\n\n>lua\n    -- Set keymap for buffers with any LSP that supports code actions\n    Snacks.keymap.set(\"n\", \"<leader>ca\", vim.lsp.buf.code_action, {\n      lsp = { method = \"textDocument/codeAction\" },\n      desc = \"Code Action\",\n    })\n    \n    -- Set keymap for buffers with a specific LSP client\n    Snacks.keymap.set(\"n\", \"<leader>co\", function()\n      vim.lsp.buf.code_action({\n        apply = true,\n        context = {\n          only = { \"source.organizeImports\" },\n          diagnostics = {},\n        },\n      })\n    end, {\n      lsp = { name = \"vtsls\" },\n      desc = \"Organize Imports\",\n    })\n    \n    -- Set keymap for buffers with LSP that supports definitions\n    Snacks.keymap.set(\"n\", \"gd\", vim.lsp.buf.definition, {\n      lsp = { method = \"textDocument/definition\" },\n      desc = \"Go to Definition\",\n    })\n<\n\n\nSTANDARD KEYMAPS                   *snacks.nvim-keymap-usage-standard-keymaps*\n\nWorks exactly like `vim.keymap.set` without special options:\n\n>lua\n    Snacks.keymap.set(\"n\", \"<leader>w\", \":w<cr>\", { desc = \"Save\" })\n    Snacks.keymap.set({ \"n\", \"v\" }, \"<leader>y\", '\"+y', { desc = \"Copy to clipboard\" })\n<\n\n\nDELETING KEYMAPS                   *snacks.nvim-keymap-usage-deleting-keymaps*\n\n>lua\n    -- Delete a standard keymap\n    Snacks.keymap.del(\"n\", \"<leader>w\")\n    \n    -- Delete a filetype-specific keymap\n    Snacks.keymap.del(\"n\", \"<leader><leader>\", { ft = \"lua\" })\n    \n    -- Delete an LSP-aware keymap\n    Snacks.keymap.del(\"n\", \"<leader>ca\", { lsp = { method = \"textDocument/codeAction\" } })\n<\n\n\n==============================================================================\n3. Types                                            *snacks.nvim-keymap-types*\n\n>lua\n    ---@class snacks.keymap.set.Opts: vim.keymap.set.Opts\n    ---@field ft? string|string[] Filetype(s) to set the keymap for.\n    ---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.\n    ---@field enabled? boolean|fun(buf?:number): boolean condition to enable the keymap.\n<\n\n>lua\n    ---@class snacks.keymap.del.Opts: vim.keymap.del.Opts\n    ---@field buffer? boolean|number If true or 0, use the current buffer.\n    ---@field ft? string|string[] Filetype(s) to set the keymap for.\n    ---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.\n<\n\n>lua\n    ---@class snacks.Keymap\n    ---@field id number           Unique ID for the keymap.\n    ---@field key string          Unique key for the keymap, in the format \"mode:lhs\".\n    ---@field mode string         Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n    ---@field lhs string          Left-hand side |{lhs}| of the mapping.\n    ---@field rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function.\n    ---@field lsp? vim.lsp.get_clients.Filter\n    ---@field opts? snacks.keymap.set.Opts\n    ---@field enabled fun(buf:number): boolean\n<\n\n\n==============================================================================\n4. Module                                          *snacks.nvim-keymap-module*\n\n\n`Snacks.keymap.del()`                                    *Snacks.keymap.del()*\n\n>lua\n    ---@param mode string|string[] Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n    ---@param lhs string           Left-hand side |{lhs}| of the mapping.\n    ---@param opts? snacks.keymap.del.Opts\n    Snacks.keymap.del(mode, lhs, opts)\n<\n\n\n`Snacks.keymap.set()`                                    *Snacks.keymap.set()*\n\n>lua\n    ---@param mode string|string[] Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n    ---@param lhs string           Left-hand side |{lhs}| of the mapping.\n    ---@param rhs string|function  Right-hand side |{rhs}| of the mapping, can be a Lua function.\n    ---@param opts? snacks.keymap.set.Opts\n    Snacks.keymap.set(mode, lhs, rhs, opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-layout.txt",
    "content": "*snacks-layout*                               snacks layout docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-layout-table-of-contents*\n\n1. Setup                                            |snacks.nvim-layout-setup|\n2. Config                                          |snacks.nvim-layout-config|\n3. Types                                            |snacks.nvim-layout-types|\n4. Module                                          |snacks.nvim-layout-module|\n  - Snacks.layout.new()        |snacks.nvim-layout-module-snacks.layout.new()|\n  - layout:close()                  |snacks.nvim-layout-module-layout:close()|\n  - layout:each()                    |snacks.nvim-layout-module-layout:each()|\n  - layout:hide()                    |snacks.nvim-layout-module-layout:hide()|\n  - layout:is_enabled()        |snacks.nvim-layout-module-layout:is_enabled()|\n  - layout:is_hidden()          |snacks.nvim-layout-module-layout:is_hidden()|\n  - layout:maximize()            |snacks.nvim-layout-module-layout:maximize()|\n  - layout:needs_layout()    |snacks.nvim-layout-module-layout:needs_layout()|\n  - layout:show()                    |snacks.nvim-layout-module-layout:show()|\n  - layout:toggle()                |snacks.nvim-layout-module-layout:toggle()|\n  - layout:unhide()                |snacks.nvim-layout-module-layout:unhide()|\n  - layout:valid()                  |snacks.nvim-layout-module-layout:valid()|\n\n==============================================================================\n1. Setup                                            *snacks.nvim-layout-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        layout = {\n          -- your layout configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                          *snacks.nvim-layout-config*\n\n>lua\n    ---@class snacks.layout.Config\n    ---@field show? boolean show the layout on creation (default: true)\n    ---@field wins table<string, snacks.win> windows to include in the layout\n    ---@field layout snacks.layout.Box layout definition\n    ---@field fullscreen? boolean open in fullscreen\n    ---@field hidden? string[] list of windows that will be excluded from the layout (but can be toggled)\n    ---@field on_update? fun(layout: snacks.layout)\n    ---@field on_update_pre? fun(layout: snacks.layout)\n    ---@field on_close? fun(layout: snacks.layout)\n    {\n      layout = {\n        width = 0.6,\n        height = 0.6,\n        zindex = 50,\n      },\n    }\n<\n\n\n==============================================================================\n3. Types                                            *snacks.nvim-layout-types*\n\n>lua\n    ---@class snacks.layout.Win: snacks.win.Config,{}\n    ---@field depth? number\n    ---@field win string layout window name\n<\n\n>lua\n    ---@class snacks.layout.Box: snacks.layout.Win,{}\n    ---@field box \"horizontal\" | \"vertical\"\n    ---@field id? number\n    ---@field [number] snacks.layout.Win | snacks.layout.Box children\n<\n\n>lua\n    ---@alias snacks.layout.Widget snacks.layout.Win | snacks.layout.Box\n<\n\n\n==============================================================================\n4. Module                                          *snacks.nvim-layout-module*\n\n>lua\n    ---@class snacks.layout\n    ---@field opts snacks.layout.Config\n    ---@field root snacks.win\n    ---@field wins table<string, snacks.win|{enabled?:boolean, layout?:boolean}>\n    ---@field box_wins snacks.win[]\n    ---@field win_opts table<string, snacks.win.Config>\n    ---@field closed? boolean\n    ---@field split? boolean\n    ---@field screenpos number[]?\n    Snacks.layout = {}\n<\n\n\n`Snacks.layout.new()`                                    *Snacks.layout.new()*\n\n>lua\n    ---@param opts snacks.layout.Config\n    Snacks.layout.new(opts)\n<\n\n\nLAYOUT:CLOSE()                      *snacks.nvim-layout-module-layout:close()*\n\nClose the layout\n\n>lua\n    ---@param opts? {wins?: boolean}\n    layout:close(opts)\n<\n\n\nLAYOUT:EACH()                        *snacks.nvim-layout-module-layout:each()*\n\n>lua\n    ---@param cb fun(widget: snacks.layout.Widget, parent?: snacks.layout.Box)\n    ---@param opts? {wins?:boolean, boxes?:boolean, box?:snacks.layout.Box}\n    layout:each(cb, opts)\n<\n\n\nLAYOUT:HIDE()                        *snacks.nvim-layout-module-layout:hide()*\n\n>lua\n    layout:hide()\n<\n\n\nLAYOUT:IS_ENABLED()            *snacks.nvim-layout-module-layout:is_enabled()*\n\nCheck if the window has been used in the layout\n\n>lua\n    ---@param w string\n    layout:is_enabled(w)\n<\n\n\nLAYOUT:IS_HIDDEN()              *snacks.nvim-layout-module-layout:is_hidden()*\n\nCheck if a window is hidden\n\n>lua\n    ---@param win string\n    layout:is_hidden(win)\n<\n\n\nLAYOUT:MAXIMIZE()                *snacks.nvim-layout-module-layout:maximize()*\n\nToggle fullscreen\n\n>lua\n    layout:maximize()\n<\n\n\nLAYOUT:NEEDS_LAYOUT()        *snacks.nvim-layout-module-layout:needs_layout()*\n\n>lua\n    ---@param win string\n    layout:needs_layout(win)\n<\n\n\nLAYOUT:SHOW()                        *snacks.nvim-layout-module-layout:show()*\n\nShow the layout\n\n>lua\n    layout:show()\n<\n\n\nLAYOUT:TOGGLE()                    *snacks.nvim-layout-module-layout:toggle()*\n\nToggle a window\n\n>lua\n    ---@param win string\n    ---@param enable? boolean\n    ---@param on_update? fun(enabled: boolean) called when the layout will be updated\n    layout:toggle(win, enable, on_update)\n<\n\n\nLAYOUT:UNHIDE()                    *snacks.nvim-layout-module-layout:unhide()*\n\n>lua\n    layout:unhide()\n<\n\n\nLAYOUT:VALID()                      *snacks.nvim-layout-module-layout:valid()*\n\nCheck if layout is valid (visible)\n\n>lua\n    layout:valid()\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-lazygit.txt",
    "content": "*snacks-lazygit*                             snacks lazygit docs\n\n==============================================================================\nTable of Contents                      *snacks.nvim-lazygit-table-of-contents*\n\n1. Setup                                           |snacks.nvim-lazygit-setup|\n2. Config                                         |snacks.nvim-lazygit-config|\n3. Styles                                         |snacks.nvim-lazygit-styles|\n  - lazygit                               |snacks.nvim-lazygit-styles-lazygit|\n4. Types                                           |snacks.nvim-lazygit-types|\n5. Module                                         |snacks.nvim-lazygit-module|\n  - Snacks.lazygit()             |snacks.nvim-lazygit-module-snacks.lazygit()|\n  - Snacks.lazygit.log()     |snacks.nvim-lazygit-module-snacks.lazygit.log()|\n  - Snacks.lazygit.log_file()|snacks.nvim-lazygit-module-snacks.lazygit.log_file()|\n  - Snacks.lazygit.open()   |snacks.nvim-lazygit-module-snacks.lazygit.open()|\n6. Links                                           |snacks.nvim-lazygit-links|\nAutomatically configures lazygit with a theme generated based on your Neovim\ncolorscheme and integrate edit with the current neovim instance.\n\n\n==============================================================================\n1. Setup                                           *snacks.nvim-lazygit-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        lazygit = {\n          -- your lazygit configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                         *snacks.nvim-lazygit-config*\n\n>lua\n    ---@class snacks.lazygit.Config: snacks.terminal.Opts\n    ---@field args? string[]\n    ---@field theme? snacks.lazygit.Theme\n    {\n      -- automatically configure lazygit to use the current colorscheme\n      -- and integrate edit with the current neovim instance\n      configure = true,\n      -- extra configuration for lazygit that will be merged with the default\n      -- snacks does NOT have a full yaml parser, so if you need `\"test\"` to appear with the quotes\n      -- you need to double quote it: `\"\\\"test\\\"\"`\n      config = {\n        os = { editPreset = \"nvim-remote\" },\n        gui = {\n          -- set to an empty string \"\" to disable icons\n          nerdFontsVersion = \"3\",\n        },\n      },\n      theme_path = svim.fs.normalize(vim.fn.stdpath(\"cache\") .. \"/lazygit-theme.yml\"),\n      -- Theme for lazygit\n      theme = {\n        [241]                      = { fg = \"Special\" },\n        activeBorderColor          = { fg = \"MatchParen\", bold = true },\n        cherryPickedCommitBgColor  = { fg = \"Identifier\" },\n        cherryPickedCommitFgColor  = { fg = \"Function\" },\n        defaultFgColor             = { fg = \"Normal\" },\n        inactiveBorderColor        = { fg = \"FloatBorder\" },\n        optionsTextColor           = { fg = \"Function\" },\n        searchingActiveBorderColor = { fg = \"MatchParen\", bold = true },\n        selectedLineBgColor        = { bg = \"Visual\" }, -- set to `default` to have no background colour\n        unstagedChangesColor       = { fg = \"DiagnosticError\" },\n      },\n      win = {\n        style = \"lazygit\",\n      },\n    }\n<\n\n\n==============================================================================\n3. Styles                                         *snacks.nvim-lazygit-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nLAZYGIT                                   *snacks.nvim-lazygit-styles-lazygit*\n\n>lua\n    {}\n<\n\n\n==============================================================================\n4. Types                                           *snacks.nvim-lazygit-types*\n\n>lua\n    ---@alias snacks.lazygit.Color {fg?:string, bg?:string, bold?:boolean}\n<\n\n>lua\n    ---@class snacks.lazygit.Theme: table<number, snacks.lazygit.Color>\n    ---@field activeBorderColor snacks.lazygit.Color\n    ---@field cherryPickedCommitBgColor snacks.lazygit.Color\n    ---@field cherryPickedCommitFgColor snacks.lazygit.Color\n    ---@field defaultFgColor snacks.lazygit.Color\n    ---@field inactiveBorderColor snacks.lazygit.Color\n    ---@field optionsTextColor snacks.lazygit.Color\n    ---@field searchingActiveBorderColor snacks.lazygit.Color\n    ---@field selectedLineBgColor snacks.lazygit.Color\n    ---@field unstagedChangesColor snacks.lazygit.Color\n<\n\n\n==============================================================================\n5. Module                                         *snacks.nvim-lazygit-module*\n\n\n`Snacks.lazygit()`                                          *Snacks.lazygit()*\n\n>lua\n    ---@type fun(opts?: snacks.lazygit.Config): snacks.win\n    Snacks.lazygit()\n<\n\n\n`Snacks.lazygit.log()`                                  *Snacks.lazygit.log()*\n\nOpens lazygit with the log view\n\n>lua\n    ---@param opts? snacks.lazygit.Config\n    Snacks.lazygit.log(opts)\n<\n\n\n`Snacks.lazygit.log_file()`                         *Snacks.lazygit.log_file()*\n\nOpens lazygit with the log of the current file\n\n>lua\n    ---@param opts? snacks.lazygit.Config|{}\n    Snacks.lazygit.log_file(opts)\n<\n\n\n`Snacks.lazygit.open()`                                *Snacks.lazygit.open()*\n\nOpens lazygit, properly configured to use the current colorscheme and integrate\nwith the current neovim instance\n\n>lua\n    ---@param opts? snacks.lazygit.Config\n    Snacks.lazygit.open(opts)\n<\n\n==============================================================================\n6. Links                                           *snacks.nvim-lazygit-links*\n\n1. *image*: https://github.com/user-attachments/assets/5e5ca232-af65-4ebc-b0ca-02bc9c33d23d\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-meta.txt",
    "content": "*snacks-meta*                                   snacks meta docs\n\n==============================================================================\nTable of Contents                         *snacks.nvim-meta-table-of-contents*\n\n1. Types                                              |snacks.nvim-meta-types|\n2. Module                                            |snacks.nvim-meta-module|\n  - Snacks.meta.file()            |snacks.nvim-meta-module-snacks.meta.file()|\n  - Snacks.meta.get()              |snacks.nvim-meta-module-snacks.meta.get()|\nMeta functions for Snacks\n\n\n==============================================================================\n1. Types                                              *snacks.nvim-meta-types*\n\n>lua\n    ---@class snacks.meta.Meta\n    ---@field desc string\n    ---@field needs_setup? boolean\n    ---@field hide? boolean\n    ---@field readme? boolean\n    ---@field docs? boolean\n    ---@field health? boolean\n    ---@field types? boolean\n    ---@field config? boolean\n    ---@field merge? { [string|number]: string }\n<\n\n>lua\n    ---@class snacks.meta.Plugin\n    ---@field name string\n    ---@field file string\n    ---@field meta snacks.meta.Meta\n    ---@field health? fun()\n<\n\n\n==============================================================================\n2. Module                                            *snacks.nvim-meta-module*\n\n\n`Snacks.meta.file()`                                      *Snacks.meta.file()*\n\n>lua\n    Snacks.meta.file(name)\n<\n\n\n`Snacks.meta.get()`                                        *Snacks.meta.get()*\n\nGet the metadata for all snacks plugins\n\n>lua\n    ---@return snacks.meta.Plugin[]\n    Snacks.meta.get()\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-notifier.txt",
    "content": "*snacks-notifier*                           snacks notifier docs\n\n==============================================================================\nTable of Contents                     *snacks.nvim-notifier-table-of-contents*\n\n1. Notification History            |snacks.nvim-notifier-notification-history|\n2. Examples                                    |snacks.nvim-notifier-examples|\n3. Setup                                          |snacks.nvim-notifier-setup|\n4. Config                                        |snacks.nvim-notifier-config|\n5. Styles                                        |snacks.nvim-notifier-styles|\n  - notification                    |snacks.nvim-notifier-styles-notification|\n  - notification_history    |snacks.nvim-notifier-styles-notification_history|\n6. Types                                          |snacks.nvim-notifier-types|\n  - Notifications                   |snacks.nvim-notifier-types-notifications|\n  - Rendering                           |snacks.nvim-notifier-types-rendering|\n  - History                               |snacks.nvim-notifier-types-history|\n7. Module                                        |snacks.nvim-notifier-module|\n  - Snacks.notifier()          |snacks.nvim-notifier-module-snacks.notifier()|\n  - Snacks.notifier.get_history()|snacks.nvim-notifier-module-snacks.notifier.get_history()|\n  - Snacks.notifier.hide()|snacks.nvim-notifier-module-snacks.notifier.hide()|\n  - Snacks.notifier.notify()|snacks.nvim-notifier-module-snacks.notifier.notify()|\n  - Snacks.notifier.show_history()|snacks.nvim-notifier-module-snacks.notifier.show_history()|\n8. Links                                          |snacks.nvim-notifier-links|\n\n==============================================================================\n1. Notification History            *snacks.nvim-notifier-notification-history*\n\n\n==============================================================================\n2. Examples                                    *snacks.nvim-notifier-examples*\n\nReplace a notification ~\n\n>lua\n    -- to replace an existing notification just use the same id.\n    -- you can also use the return value of the notify function as id.\n    for i = 1, 10 do\n      vim.defer_fn(function()\n        vim.notify(\"Hello \" .. i, \"info\", { id = \"test\" })\n      end, i * 500)\n    end\n<\n\nSimple LSP Progress ~\n\n>lua\n    vim.api.nvim_create_autocmd(\"LspProgress\", {\n      ---@param ev {data: {client_id: integer, params: lsp.ProgressParams}}\n      callback = function(ev)\n        local spinner = { \"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\" }\n        vim.notify(vim.lsp.status(), \"info\", {\n          id = \"lsp_progress\",\n          title = \"LSP Progress\",\n          opts = function(notif)\n            notif.icon = ev.data.params.value.kind == \"end\" and \" \"\n              or spinner[math.floor(vim.uv.hrtime() / (1e6 * 80)) % #spinner + 1]\n          end,\n        })\n      end,\n    })\n<\n\nAdvanced LSP Progress ~\n\n>lua\n    ---@type table<number, {token:lsp.ProgressToken, msg:string, done:boolean}[]>\n    local progress = vim.defaulttable()\n    vim.api.nvim_create_autocmd(\"LspProgress\", {\n      ---@param ev {data: {client_id: integer, params: lsp.ProgressParams}}\n      callback = function(ev)\n        local client = vim.lsp.get_client_by_id(ev.data.client_id)\n        local value = ev.data.params.value --[[@as {percentage?: number, title?: string, message?: string, kind: \"begin\" | \"report\" | \"end\"}]]\n        if not client or type(value) ~= \"table\" then\n          return\n        end\n        local p = progress[client.id]\n    \n        for i = 1, #p + 1 do\n          if i == #p + 1 or p[i].token == ev.data.params.token then\n            p[i] = {\n              token = ev.data.params.token,\n              msg = (\"[%3d%%] %s%s\"):format(\n                value.kind == \"end\" and 100 or value.percentage or 100,\n                value.title or \"\",\n                value.message and (\" **%s**\"):format(value.message) or \"\"\n              ),\n              done = value.kind == \"end\",\n            }\n            break\n          end\n        end\n    \n        local msg = {} ---@type string[]\n        progress[client.id] = vim.tbl_filter(function(v)\n          return table.insert(msg, v.msg) or not v.done\n        end, p)\n    \n        local spinner = { \"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\" }\n        vim.notify(table.concat(msg, \"\\n\"), \"info\", {\n          id = \"lsp_progress\",\n          title = client.name,\n          opts = function(notif)\n            notif.icon = #progress[client.id] == 0 and \" \"\n              or spinner[math.floor(vim.uv.hrtime() / (1e6 * 80)) % #spinner + 1]\n          end,\n        })\n      end,\n    })\n<\n\n\n==============================================================================\n3. Setup                                          *snacks.nvim-notifier-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        notifier = {\n          -- your notifier configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n4. Config                                        *snacks.nvim-notifier-config*\n\n>lua\n    ---@class snacks.notifier.Config\n    ---@field enabled? boolean\n    ---@field keep? fun(notif: snacks.notifier.Notif): boolean # global keep function\n    ---@field filter? fun(notif: snacks.notifier.Notif): boolean # filter our unwanted notifications (return false to hide)\n    {\n      timeout = 3000, -- default timeout in ms\n      width = { min = 40, max = 0.4 },\n      height = { min = 1, max = 0.6 },\n      -- editor margin to keep free. tabline and statusline are taken into account automatically\n      margin = { top = 0, right = 1, bottom = 0 },\n      padding = true, -- add 1 cell of left/right padding to the notification window\n      gap = 0, -- gap between notifications\n      sort = { \"level\", \"added\" }, -- sort by level and time\n      -- minimum log level to display. TRACE is the lowest\n      -- all notifications are stored in history\n      level = vim.log.levels.TRACE,\n      icons = {\n        error = \" \",\n        warn = \" \",\n        info = \" \",\n        debug = \" \",\n        trace = \" \",\n      },\n      keep = function(notif)\n        return vim.fn.getcmdpos() > 0\n      end,\n      ---@type snacks.notifier.style\n      style = \"compact\",\n      top_down = true, -- place notifications from top to bottom\n      date_format = \"%R\", -- time format for notifications\n      -- format for footer when more lines are available\n      -- `%d` is replaced with the number of lines.\n      -- only works for styles with a border\n      ---@type string|boolean\n      more_format = \" ↓ %d lines \",\n      refresh = 50, -- refresh at most every 50ms\n    }\n<\n\n\n==============================================================================\n5. Styles                                        *snacks.nvim-notifier-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nNOTIFICATION                        *snacks.nvim-notifier-styles-notification*\n\n>lua\n    {\n      border = true,\n      zindex = 100,\n      ft = \"markdown\",\n      wo = {\n        winblend = 5,\n        wrap = false,\n        conceallevel = 2,\n        colorcolumn = \"\",\n      },\n      bo = { filetype = \"snacks_notif\" },\n    }\n<\n\n\nNOTIFICATION_HISTORY        *snacks.nvim-notifier-styles-notification_history*\n\n>lua\n    {\n      border = true,\n      zindex = 100,\n      width = 0.6,\n      height = 0.6,\n      minimal = false,\n      title = \" Notification History \",\n      title_pos = \"center\",\n      ft = \"markdown\",\n      bo = { filetype = \"snacks_notif_history\", modifiable = false },\n      wo = { winhighlight = \"Normal:SnacksNotifierHistory\" },\n      keys = { q = \"close\" },\n    }\n<\n\n\n==============================================================================\n6. Types                                          *snacks.nvim-notifier-types*\n\nRender styles: compact: use border for icon and title minimal: no border, only\nicon and message fancy: similar to the default nvim-notify style\n\n>lua\n    ---@alias snacks.notifier.style snacks.notifier.render|\"compact\"|\"fancy\"|\"minimal\"\n<\n\n\nNOTIFICATIONS                       *snacks.nvim-notifier-types-notifications*\n\nNotification options\n\n>lua\n    ---@class snacks.notifier.Notif.opts\n    ---@field id? number|string\n    ---@field msg? string\n    ---@field level? number|snacks.notifier.level\n    ---@field title? string\n    ---@field icon? string\n    ---@field timeout? number|boolean timeout in ms. Set to 0|false to keep until manually closed\n    ---@field ft? string\n    ---@field keep? fun(notif: snacks.notifier.Notif): boolean\n    ---@field style? snacks.notifier.style\n    ---@field opts? fun(notif: snacks.notifier.Notif) -- dynamic opts\n    ---@field hl? snacks.notifier.hl -- highlight overrides\n    ---@field history? boolean\n<\n\nNotification object\n\n>lua\n    ---@class snacks.notifier.Notif: snacks.notifier.Notif.opts\n    ---@field id number|string\n    ---@field msg string\n    ---@field win? snacks.win\n    ---@field icon string\n    ---@field level snacks.notifier.level\n    ---@field timeout number\n    ---@field dirty? boolean\n    ---@field added number timestamp with nano precision\n    ---@field updated number timestamp with nano precision\n    ---@field shown? number timestamp with nano precision\n    ---@field hidden? number timestamp with nano precision\n    ---@field layout? { top?: number, width: number, height: number }\n<\n\n\nRENDERING                               *snacks.nvim-notifier-types-rendering*\n\n>lua\n    ---@alias snacks.notifier.render fun(buf: number, notif: snacks.notifier.Notif, ctx: snacks.notifier.ctx)\n<\n\n>lua\n    ---@class snacks.notifier.hl\n    ---@field title string\n    ---@field icon string\n    ---@field border string\n    ---@field footer string\n    ---@field msg string\n<\n\n>lua\n    ---@class snacks.notifier.ctx\n    ---@field opts snacks.win.Config\n    ---@field notifier snacks.notifier.Class\n    ---@field hl snacks.notifier.hl\n    ---@field ns number\n<\n\n\nHISTORY                                   *snacks.nvim-notifier-types-history*\n\n>lua\n    ---@class snacks.notifier.history\n    ---@field filter? vim.log.levels|snacks.notifier.level|fun(notif: snacks.notifier.Notif): boolean\n    ---@field sort? string[] # sort fields, default: {\"added\"}\n    ---@field reverse? boolean\n<\n\n>lua\n    ---@alias snacks.notifier.level \"trace\"|\"debug\"|\"info\"|\"warn\"|\"error\"\n<\n\n\n==============================================================================\n7. Module                                        *snacks.nvim-notifier-module*\n\n\n`Snacks.notifier()`                                        *Snacks.notifier()*\n\n>lua\n    ---@type fun(msg: string, level?: snacks.notifier.level|number, opts?: snacks.notifier.Notif.opts): number|string\n    Snacks.notifier()\n<\n\n\n`Snacks.notifier.get_history()`                          *Snacks.notifier.get_history()*\n\n>lua\n    ---@param opts? snacks.notifier.history\n    Snacks.notifier.get_history(opts)\n<\n\n\n`Snacks.notifier.hide()`                              *Snacks.notifier.hide()*\n\n>lua\n    ---@param id? number|string\n    Snacks.notifier.hide(id)\n<\n\n\n`Snacks.notifier.notify()`                          *Snacks.notifier.notify()*\n\n>lua\n    ---@param msg string\n    ---@param level? snacks.notifier.level|number\n    ---@param opts? snacks.notifier.Notif.opts\n    Snacks.notifier.notify(msg, level, opts)\n<\n\n\n`Snacks.notifier.show_history()`                          *Snacks.notifier.show_history()*\n\n>lua\n    ---@param opts? snacks.notifier.history\n    Snacks.notifier.show_history(opts)\n<\n\n==============================================================================\n8. Links                                          *snacks.nvim-notifier-links*\n\n1. *image*: https://github.com/user-attachments/assets/b89eb279-08fb-40b2-9330-9a77014b9389\n2. *image*: https://github.com/user-attachments/assets/0dc449f4-b275-49e4-a25f-f58efcba3079\n3. *image*: https://github.com/user-attachments/assets/a81b411c-150a-43ec-8def-87270c6f8dde\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-notify.txt",
    "content": "*snacks-notify*                               snacks notify docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-notify-table-of-contents*\n\n1. Types                                            |snacks.nvim-notify-types|\n2. Module                                          |snacks.nvim-notify-module|\n  - Snacks.notify()                |snacks.nvim-notify-module-snacks.notify()|\n  - Snacks.notify.error()    |snacks.nvim-notify-module-snacks.notify.error()|\n  - Snacks.notify.info()      |snacks.nvim-notify-module-snacks.notify.info()|\n  - Snacks.notify.notify()  |snacks.nvim-notify-module-snacks.notify.notify()|\n  - Snacks.notify.warn()      |snacks.nvim-notify-module-snacks.notify.warn()|\n\n==============================================================================\n1. Types                                            *snacks.nvim-notify-types*\n\n>lua\n    ---@alias snacks.notify.Opts snacks.notifier.Notif.opts|{once?: boolean}\n<\n\n\n==============================================================================\n2. Module                                          *snacks.nvim-notify-module*\n\n\n`Snacks.notify()`                                            *Snacks.notify()*\n\n>lua\n    ---@type fun(msg: string|string[], opts?: snacks.notify.Opts)\n    Snacks.notify()\n<\n\n\n`Snacks.notify.error()`                                *Snacks.notify.error()*\n\n>lua\n    ---@param msg string|string[]\n    ---@param opts? snacks.notify.Opts\n    Snacks.notify.error(msg, opts)\n<\n\n\n`Snacks.notify.info()`                                  *Snacks.notify.info()*\n\n>lua\n    ---@param msg string|string[]\n    ---@param opts? snacks.notify.Opts\n    Snacks.notify.info(msg, opts)\n<\n\n\n`Snacks.notify.notify()`                              *Snacks.notify.notify()*\n\n>lua\n    ---@param msg string|string[]\n    ---@param opts? snacks.notify.Opts\n    Snacks.notify.notify(msg, opts)\n<\n\n\n`Snacks.notify.warn()`                                  *Snacks.notify.warn()*\n\n>lua\n    ---@param msg string|string[]\n    ---@param opts? snacks.notify.Opts\n    Snacks.notify.warn(msg, opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-picker.txt",
    "content": "*snacks-picker*                               snacks picker docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-picker-table-of-contents*\n\n1. Features                                      |snacks.nvim-picker-features|\n2. Usage                                            |snacks.nvim-picker-usage|\n3. Setup                                            |snacks.nvim-picker-setup|\n4. Config                                          |snacks.nvim-picker-config|\n5. Examples                                      |snacks.nvim-picker-examples|\n  - flash                                  |snacks.nvim-picker-examples-flash|\n  - general                              |snacks.nvim-picker-examples-general|\n  - todo_comments                  |snacks.nvim-picker-examples-todo_comments|\n  - trouble                              |snacks.nvim-picker-examples-trouble|\n6. Types                                            |snacks.nvim-picker-types|\n7. Module                                          |snacks.nvim-picker-module|\n  - Snacks.picker()                |snacks.nvim-picker-module-snacks.picker()|\n  - Snacks.picker.get()        |snacks.nvim-picker-module-snacks.picker.get()|\n  - Snacks.picker.pick()      |snacks.nvim-picker-module-snacks.picker.pick()|\n  - Snacks.picker.resume()  |snacks.nvim-picker-module-snacks.picker.resume()|\n  - Snacks.picker.select()  |snacks.nvim-picker-module-snacks.picker.select()|\n8. Sources                                        |snacks.nvim-picker-sources|\n  - autocmds                             |snacks.nvim-picker-sources-autocmds|\n  - buffers                               |snacks.nvim-picker-sources-buffers|\n  - cliphist                             |snacks.nvim-picker-sources-cliphist|\n  - colorschemes                     |snacks.nvim-picker-sources-colorschemes|\n  - command_history               |snacks.nvim-picker-sources-command_history|\n  - commands                             |snacks.nvim-picker-sources-commands|\n  - diagnostics                       |snacks.nvim-picker-sources-diagnostics|\n  - diagnostics_buffer         |snacks.nvim-picker-sources-diagnostics_buffer|\n  - explorer                             |snacks.nvim-picker-sources-explorer|\n  - files                                   |snacks.nvim-picker-sources-files|\n  - gh_actions                         |snacks.nvim-picker-sources-gh_actions|\n  - gh_diff                               |snacks.nvim-picker-sources-gh_diff|\n  - gh_issue                             |snacks.nvim-picker-sources-gh_issue|\n  - gh_labels                           |snacks.nvim-picker-sources-gh_labels|\n  - gh_pr                                   |snacks.nvim-picker-sources-gh_pr|\n  - gh_reactions                     |snacks.nvim-picker-sources-gh_reactions|\n  - git_branches                     |snacks.nvim-picker-sources-git_branches|\n  - git_diff                             |snacks.nvim-picker-sources-git_diff|\n  - git_files                           |snacks.nvim-picker-sources-git_files|\n  - git_grep                             |snacks.nvim-picker-sources-git_grep|\n  - git_log                               |snacks.nvim-picker-sources-git_log|\n  - git_log_file                     |snacks.nvim-picker-sources-git_log_file|\n  - git_log_line                     |snacks.nvim-picker-sources-git_log_line|\n  - git_stash                           |snacks.nvim-picker-sources-git_stash|\n  - git_status                         |snacks.nvim-picker-sources-git_status|\n  - grep                                     |snacks.nvim-picker-sources-grep|\n  - grep_buffers                     |snacks.nvim-picker-sources-grep_buffers|\n  - grep_word                           |snacks.nvim-picker-sources-grep_word|\n  - help                                     |snacks.nvim-picker-sources-help|\n  - highlights                         |snacks.nvim-picker-sources-highlights|\n  - icons                                   |snacks.nvim-picker-sources-icons|\n  - jumps                                   |snacks.nvim-picker-sources-jumps|\n  - keymaps                               |snacks.nvim-picker-sources-keymaps|\n  - lazy                                     |snacks.nvim-picker-sources-lazy|\n  - lines                                   |snacks.nvim-picker-sources-lines|\n  - loclist                               |snacks.nvim-picker-sources-loclist|\n  - lsp_config                         |snacks.nvim-picker-sources-lsp_config|\n  - lsp_declarations             |snacks.nvim-picker-sources-lsp_declarations|\n  - lsp_definitions               |snacks.nvim-picker-sources-lsp_definitions|\n  - lsp_implementations       |snacks.nvim-picker-sources-lsp_implementations|\n  - lsp_incoming_calls         |snacks.nvim-picker-sources-lsp_incoming_calls|\n  - lsp_outgoing_calls         |snacks.nvim-picker-sources-lsp_outgoing_calls|\n  - lsp_references                 |snacks.nvim-picker-sources-lsp_references|\n  - lsp_symbols                       |snacks.nvim-picker-sources-lsp_symbols|\n  - lsp_type_definitions     |snacks.nvim-picker-sources-lsp_type_definitions|\n  - lsp_workspace_symbols   |snacks.nvim-picker-sources-lsp_workspace_symbols|\n  - man                                       |snacks.nvim-picker-sources-man|\n  - marks                                   |snacks.nvim-picker-sources-marks|\n  - notifications                   |snacks.nvim-picker-sources-notifications|\n  - picker_actions                 |snacks.nvim-picker-sources-picker_actions|\n  - picker_format                   |snacks.nvim-picker-sources-picker_format|\n  - picker_layouts                 |snacks.nvim-picker-sources-picker_layouts|\n  - picker_preview                 |snacks.nvim-picker-sources-picker_preview|\n  - pickers                               |snacks.nvim-picker-sources-pickers|\n  - projects                             |snacks.nvim-picker-sources-projects|\n  - qflist                                 |snacks.nvim-picker-sources-qflist|\n  - recent                                 |snacks.nvim-picker-sources-recent|\n  - registers                           |snacks.nvim-picker-sources-registers|\n  - resume                                 |snacks.nvim-picker-sources-resume|\n  - scratch                               |snacks.nvim-picker-sources-scratch|\n  - search_history                 |snacks.nvim-picker-sources-search_history|\n  - select                                 |snacks.nvim-picker-sources-select|\n  - smart                                   |snacks.nvim-picker-sources-smart|\n  - spelling                             |snacks.nvim-picker-sources-spelling|\n  - tags                                     |snacks.nvim-picker-sources-tags|\n  - treesitter                         |snacks.nvim-picker-sources-treesitter|\n  - undo                                     |snacks.nvim-picker-sources-undo|\n  - zoxide                                 |snacks.nvim-picker-sources-zoxide|\n9. Layouts                                        |snacks.nvim-picker-layouts|\n  - bottom                                 |snacks.nvim-picker-layouts-bottom|\n  - default                               |snacks.nvim-picker-layouts-default|\n  - dropdown                             |snacks.nvim-picker-layouts-dropdown|\n  - ivy                                       |snacks.nvim-picker-layouts-ivy|\n  - ivy_split                           |snacks.nvim-picker-layouts-ivy_split|\n  - left                                     |snacks.nvim-picker-layouts-left|\n  - right                                   |snacks.nvim-picker-layouts-right|\n  - select                                 |snacks.nvim-picker-layouts-select|\n  - sidebar                               |snacks.nvim-picker-layouts-sidebar|\n  - telescope                           |snacks.nvim-picker-layouts-telescope|\n  - top                                       |snacks.nvim-picker-layouts-top|\n  - vertical                             |snacks.nvim-picker-layouts-vertical|\n  - vscode                                 |snacks.nvim-picker-layouts-vscode|\n10. snacks.picker.actions           |snacks.nvim-picker-snacks.picker.actions|\n  - Snacks.picker.actions.bufdelete()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.bufdelete()|\n  - Snacks.picker.actions.cancel()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cancel()|\n  - Snacks.picker.actions.cd()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cd()|\n  - Snacks.picker.actions.close()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.close()|\n  - Snacks.picker.actions.cmd()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cmd()|\n  - Snacks.picker.actions.cycle_win()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cycle_win()|\n  - Snacks.picker.actions.focus_input()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.focus_input()|\n  - Snacks.picker.actions.focus_list()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.focus_list()|\n  - Snacks.picker.actions.focus_preview()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.focus_preview()|\n  - Snacks.picker.actions.git_branch_add()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_branch_add()|\n  - Snacks.picker.actions.git_branch_del()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_branch_del()|\n  - Snacks.picker.actions.git_checkout()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_checkout()|\n  - Snacks.picker.actions.git_restore()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_restore()|\n  - Snacks.picker.actions.git_stage()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_stage()|\n  - Snacks.picker.actions.git_stash_apply()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_stash_apply()|\n  - Snacks.picker.actions.help()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.help()|\n  - Snacks.picker.actions.history_back()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.history_back()|\n  - Snacks.picker.actions.history_forward()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.history_forward()|\n  - Snacks.picker.actions.insert()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.insert()|\n  - Snacks.picker.actions.inspect()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.inspect()|\n  - Snacks.picker.actions.item_action()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.item_action()|\n  - Snacks.picker.actions.jump()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.jump()|\n  - Snacks.picker.actions.layout()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.layout()|\n  - Snacks.picker.actions.lcd()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.lcd()|\n  - Snacks.picker.actions.list_bottom()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_bottom()|\n  - Snacks.picker.actions.list_down()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_down()|\n  - Snacks.picker.actions.list_scroll_bottom()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_bottom()|\n  - Snacks.picker.actions.list_scroll_center()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_center()|\n  - Snacks.picker.actions.list_scroll_down()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_down()|\n  - Snacks.picker.actions.list_scroll_top()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_top()|\n  - Snacks.picker.actions.list_scroll_up()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_up()|\n  - Snacks.picker.actions.list_top()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_top()|\n  - Snacks.picker.actions.list_up()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_up()|\n  - Snacks.picker.actions.load_session()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.load_session()|\n  - Snacks.picker.actions.loclist()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.loclist()|\n  - Snacks.picker.actions.mark_delete()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.mark_delete()|\n  - Snacks.picker.actions.paste()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.paste()|\n  - Snacks.picker.actions.pick_win()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.pick_win()|\n  - Snacks.picker.actions.picker()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.picker()|\n  - Snacks.picker.actions.picker_grep()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.picker_grep()|\n  - Snacks.picker.actions.preview_scroll_down()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_down()|\n  - Snacks.picker.actions.preview_scroll_left()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_left()|\n  - Snacks.picker.actions.preview_scroll_right()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_right()|\n  - Snacks.picker.actions.preview_scroll_up()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_up()|\n  - Snacks.picker.actions.print_cwd()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.print_cwd()|\n  - Snacks.picker.actions.print_dir()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.print_dir()|\n  - Snacks.picker.actions.print_path()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.print_path()|\n  - Snacks.picker.actions.qflist()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.qflist()|\n  - Snacks.picker.actions.qflist_all()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.qflist_all()|\n  - Snacks.picker.actions.search()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.search()|\n  - Snacks.picker.actions.select_all()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.select_all()|\n  - Snacks.picker.actions.select_and_next()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.select_and_next()|\n  - Snacks.picker.actions.select_and_prev()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.select_and_prev()|\n  - Snacks.picker.actions.tcd()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.tcd()|\n  - Snacks.picker.actions.terminal()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.terminal()|\n  - Snacks.picker.actions.toggle_focus()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_focus()|\n  - Snacks.picker.actions.toggle_help_input()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_help_input()|\n  - Snacks.picker.actions.toggle_help_list()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_help_list()|\n  - Snacks.picker.actions.toggle_input()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_input()|\n  - Snacks.picker.actions.toggle_live()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_live()|\n  - Snacks.picker.actions.toggle_maximize()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_maximize()|\n  - Snacks.picker.actions.toggle_preview()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_preview()|\n  - Snacks.picker.actions.yank()|snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.yank()|\n11. snacks.picker.core.picker   |snacks.nvim-picker-snacks.picker.core.picker|\n  - Snacks.picker.picker.get()|snacks.nvim-picker-snacks.picker.core.picker-snacks.picker.picker.get()|\n  - picker:action()|snacks.nvim-picker-snacks.picker.core.picker-picker:action()|\n  - picker:close()|snacks.nvim-picker-snacks.picker.core.picker-picker:close()|\n  - picker:count()|snacks.nvim-picker-snacks.picker.core.picker-picker:count()|\n  - picker:current()|snacks.nvim-picker-snacks.picker.core.picker-picker:current()|\n  - picker:current_win()|snacks.nvim-picker-snacks.picker.core.picker-picker:current_win()|\n  - picker:cwd()   |snacks.nvim-picker-snacks.picker.core.picker-picker:cwd()|\n  - picker:dir()   |snacks.nvim-picker-snacks.picker.core.picker-picker:dir()|\n  - picker:empty()|snacks.nvim-picker-snacks.picker.core.picker-picker:empty()|\n  - picker:filter()|snacks.nvim-picker-snacks.picker.core.picker-picker:filter()|\n  - picker:find() |snacks.nvim-picker-snacks.picker.core.picker-picker:find()|\n  - picker:focus()|snacks.nvim-picker-snacks.picker.core.picker-picker:focus()|\n  - picker:hist() |snacks.nvim-picker-snacks.picker.core.picker-picker:hist()|\n  - picker:is_active()|snacks.nvim-picker-snacks.picker.core.picker-picker:is_active()|\n  - picker:is_focused()|snacks.nvim-picker-snacks.picker.core.picker-picker:is_focused()|\n  - picker:items()|snacks.nvim-picker-snacks.picker.core.picker-picker:items()|\n  - picker:iter() |snacks.nvim-picker-snacks.picker.core.picker-picker:iter()|\n  - picker:norm() |snacks.nvim-picker-snacks.picker.core.picker-picker:norm()|\n  - picker:on_current_tab()|snacks.nvim-picker-snacks.picker.core.picker-picker:on_current_tab()|\n  - picker:ref()   |snacks.nvim-picker-snacks.picker.core.picker-picker:ref()|\n  - picker:refresh()|snacks.nvim-picker-snacks.picker.core.picker-picker:refresh()|\n  - picker:resolve()|snacks.nvim-picker-snacks.picker.core.picker-picker:resolve()|\n  - picker:selected()|snacks.nvim-picker-snacks.picker.core.picker-picker:selected()|\n  - picker:set_cwd()|snacks.nvim-picker-snacks.picker.core.picker-picker:set_cwd()|\n  - picker:set_layout()|snacks.nvim-picker-snacks.picker.core.picker-picker:set_layout()|\n  - picker:show_preview()|snacks.nvim-picker-snacks.picker.core.picker-picker:show_preview()|\n  - picker:toggle()|snacks.nvim-picker-snacks.picker.core.picker-picker:toggle()|\n  - picker:word() |snacks.nvim-picker-snacks.picker.core.picker-picker:word()|\n12. Links                                           |snacks.nvim-picker-links|\nSnacks now comes with a modern fuzzy-finder to navigate the Neovim universe.\n\n\n\n\n==============================================================================\n1. Features                                      *snacks.nvim-picker-features*\n\n- over 40 built-in sources <https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#-sources>\n- Fast and powerful fuzzy matching engine that supports the fzf <https://junegunn.github.io/fzf/search-syntax/> search syntax\n    - additionally supports field searches like `file:lua$ 'function`\n    - `files` and `grep` additionally support adding options like `foo -- -e=lua`\n- uses **treesitter** highlighting where it makes sense\n- Sane default settings so you can start using it right away\n- Finders and matchers run asynchronously for maximum performance\n- Different layouts <https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#%EF%B8%8F-layouts> to suit your needs, or create your own.\n    Uses Snacks.layout <https://github.com/folke/snacks.nvim/blob/main/docs/layout.md>\n    under the hood.\n- Simple API to create your own pickers\n- Better `vim.ui.select`\n\nSome acknowledgements:\n\n- fzf-lua <https://github.com/ibhagwan/fzf-lua>\n- telescope.nvim <https://github.com/nvim-telescope/telescope.nvim>\n- mini.pick <https://github.com/nvim-mini/mini.pick>\n\n\n==============================================================================\n2. Usage                                            *snacks.nvim-picker-usage*\n\nThe best way to get started is to copy some of the example configs\n<https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#-examples>\nbelow.\n\n>lua\n    -- Show all pickers\n    Snacks.picker()\n    \n    -- run files picker (all three are equivalent)\n    Snacks.picker.files(opts)\n    Snacks.picker.pick(\"files\", opts)\n    Snacks.picker.pick({source = \"files\", ...})\n<\n\n\n==============================================================================\n3. Setup                                            *snacks.nvim-picker-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        picker = {\n          -- your picker configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n4. Config                                          *snacks.nvim-picker-config*\n\n>lua\n    ---@class snacks.picker.Config\n    ---@field multi? (string|snacks.picker.Config)[]\n    ---@field source? string source name and config to use\n    ---@field pattern? string|fun(picker:snacks.Picker):string pattern used to filter items by the matcher\n    ---@field search? string|fun(picker:snacks.Picker):string search string used by finders\n    ---@field cwd? string current working directory\n    ---@field live? boolean when true, typing will trigger live searches\n    ---@field limit? number when set, the finder will stop after finding this number of items. useful for live searches\n    ---@field limit_live? number when set, the finder will stop after finding this number of items during live searches. useful for performance\n    ---@field ui_select? boolean set `vim.ui.select` to a snacks picker\n    ---@field filter? snacks.picker.filter.Config generic filter used by some finders\n    --- Source definition\n    ---@field items? snacks.picker.finder.Item[] items to show instead of using a finder\n    ---@field format? string|snacks.picker.format|string format function or preset\n    ---@field finder? string|snacks.picker.finder|snacks.picker.finder.multi finder function or preset\n    ---@field preview? snacks.picker.preview|string preview function or preset\n    ---@field matcher? snacks.picker.matcher.Config|{} matcher config\n    ---@field sort? snacks.picker.sort|snacks.picker.sort.Config sort function or config\n    ---@field transform? string|snacks.picker.transform transform/filter function\n    --- UI\n    ---@field win? snacks.picker.win.Config\n    ---@field layout? snacks.picker.layout.Config|string|{}|fun(source:string):(snacks.picker.layout.Config|string)\n    ---@field icons? snacks.picker.icons\n    ---@field prompt? string prompt text / icon\n    ---@field title? string defaults to a capitalized source name\n    ---@field auto_close? boolean automatically close the picker when focusing another window (defaults to true)\n    ---@field show_empty? boolean show the picker even when there are no items\n    ---@field show_delay? number delay (in ms) to wait before showing the picker while no results yet\n    ---@field focus? \"input\"|\"list\" where to focus when the picker is opened (defaults to \"input\")\n    ---@field enter? boolean enter the picker when opening it\n    ---@field toggles? table<string, string|false|snacks.picker.toggle>\n    --- Preset options\n    ---@field previewers? snacks.picker.previewers.Config|{}\n    ---@field formatters? snacks.picker.formatters.Config|{}\n    ---@field sources? snacks.picker.sources.Config|{}|table<string, snacks.picker.Config|{}>\n    ---@field layouts? table<string, snacks.picker.layout.Config>\n    --- Actions\n    ---@field actions? table<string, snacks.picker.Action.spec> actions used by keymaps\n    ---@field confirm? snacks.picker.Action.spec shortcut for confirm action\n    ---@field auto_confirm? boolean automatically confirm if there is only one item\n    ---@field main? snacks.picker.main.Config main editor window config\n    ---@field on_change? fun(picker:snacks.Picker, item?:snacks.picker.Item) called when the cursor changes\n    ---@field on_show? fun(picker:snacks.Picker) called when the picker is shown\n    ---@field on_close? fun(picker:snacks.Picker) called when the picker is closed\n    ---@field jump? snacks.picker.jump.Config|{}\n    --- Other\n    ---@field config? fun(opts:snacks.picker.Config):snacks.picker.Config? custom config function\n    ---@field db? snacks.picker.db.Config|{}\n    ---@field debug? snacks.picker.debug|{}\n    {\n      prompt = \" \",\n      sources = {},\n      focus = \"input\",\n      show_delay = 5000,\n      limit_live = 10000,\n      layout = {\n        cycle = true,\n        --- Use the default layout or vertical if the window is too narrow\n        preset = function()\n          return vim.o.columns >= 120 and \"default\" or \"vertical\"\n        end,\n      },\n      ---@class snacks.picker.matcher.Config\n      matcher = {\n        fuzzy = true, -- use fuzzy matching\n        smartcase = true, -- use smartcase\n        ignorecase = true, -- use ignorecase\n        sort_empty = false, -- sort results when the search string is empty\n        filename_bonus = true, -- give bonus for matching file names (last part of the path)\n        file_pos = true, -- support patterns like `file:line:col` and `file:line`\n        -- the bonusses below, possibly require string concatenation and path normalization,\n        -- so this can have a performance impact for large lists and increase memory usage\n        cwd_bonus = false, -- give bonus for matching files in the cwd\n        frecency = false, -- frecency bonus\n        history_bonus = false, -- give more weight to chronological order\n      },\n      sort = {\n        -- default sort is by score, text length and index\n        fields = { \"score:desc\", \"#text\", \"idx\" },\n      },\n      ui_select = true, -- replace `vim.ui.select` with the snacks picker\n      ---@class snacks.picker.formatters.Config\n      formatters = {\n        text = {\n          ft = nil, ---@type string? filetype for highlighting\n        },\n        file = {\n          filename_first = false, -- display filename before the file path\n          --- * left: truncate the beginning of the path\n          --- * center: truncate the middle of the path\n          --- * right: truncate the end of the path\n          ---@type \"left\"|\"center\"|\"right\"\n          truncate = \"center\",\n          min_width = 40, -- minimum length of the truncated path\n          filename_only = false, -- only show the filename\n          icon_width = 2, -- width of the icon (in characters)\n          git_status_hl = true, -- use the git status highlight group for the filename\n        },\n        selected = {\n          show_always = false, -- only show the selected column when there are multiple selections\n          unselected = true, -- use the unselected icon for unselected items\n        },\n        severity = {\n          icons = true, -- show severity icons\n          level = false, -- show severity level\n          ---@type \"left\"|\"right\"\n          pos = \"left\", -- position of the diagnostics\n        },\n      },\n      ---@class snacks.picker.previewers.Config\n      previewers = {\n        diff = {\n          -- fancy: Snacks fancy diff (borders, multi-column line numbers, syntax highlighting)\n          -- syntax: Neovim's built-in diff syntax highlighting\n          -- terminal: external command (git's pager for git commands, `cmd` for other diffs)\n          style = \"fancy\", ---@type \"fancy\"|\"syntax\"|\"terminal\"\n          cmd = { \"delta\" }, -- example for using `delta` as the external diff command\n          ---@type vim.wo?|{} window options for the fancy diff preview window\n          wo = {\n            breakindent = true,\n            wrap = true,\n            linebreak = true,\n            showbreak = \"\",\n          },\n        },\n        git = {\n          args = {}, -- additional arguments passed to the git command. Useful to set pager options usin `-c ...`\n        },\n        file = {\n          max_size = 1024 * 1024, -- 1MB\n          max_line_length = 500, -- max line length\n          ft = nil, ---@type string? filetype for highlighting. Use `nil` for auto detect\n        },\n        man_pager = nil, ---@type string? MANPAGER env to use for `man` preview\n      },\n      ---@class snacks.picker.jump.Config\n      jump = {\n        jumplist = true, -- save the current position in the jumplist\n        tagstack = false, -- save the current position in the tagstack\n        reuse_win = false, -- reuse an existing window if the buffer is already open\n        close = true, -- close the picker when jumping/editing to a location (defaults to true)\n        match = false, -- jump to the first match position. (useful for `lines`)\n      },\n      toggles = {\n        follow = \"f\",\n        hidden = \"h\",\n        ignored = \"i\",\n        modified = \"m\",\n        regex = { icon = \"R\", value = false },\n      },\n      win = {\n        -- input window\n        input = {\n          keys = {\n            -- to close the picker on ESC instead of going to normal mode,\n            -- add the following keymap to your config\n            -- [\"<Esc>\"] = { \"close\", mode = { \"n\", \"i\" } },\n            [\"/\"] = \"toggle_focus\",\n            [\"<C-Down>\"] = { \"history_forward\", mode = { \"i\", \"n\" } },\n            [\"<C-Up>\"] = { \"history_back\", mode = { \"i\", \"n\" } },\n            [\"<C-c>\"] = { \"cancel\", mode = \"i\" },\n            [\"<C-w>\"] = { \"<c-s-w>\", mode = { \"i\" }, expr = true, desc = \"delete word\" },\n            [\"<CR>\"] = { \"confirm\", mode = { \"n\", \"i\" } },\n            [\"<Down>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n            [\"<Esc>\"] = \"cancel\",\n            [\"<S-CR>\"] = { { \"pick_win\", \"jump\" }, mode = { \"n\", \"i\" } },\n            [\"<S-Tab>\"] = { \"select_and_prev\", mode = { \"i\", \"n\" } },\n            [\"<Tab>\"] = { \"select_and_next\", mode = { \"i\", \"n\" } },\n            [\"<Up>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n            [\"<a-d>\"] = { \"inspect\", mode = { \"n\", \"i\" } },\n            [\"<a-f>\"] = { \"toggle_follow\", mode = { \"i\", \"n\" } },\n            [\"<a-h>\"] = { \"toggle_hidden\", mode = { \"i\", \"n\" } },\n            [\"<a-i>\"] = { \"toggle_ignored\", mode = { \"i\", \"n\" } },\n            [\"<a-r>\"] = { \"toggle_regex\", mode = { \"i\", \"n\" } },\n            [\"<a-m>\"] = { \"toggle_maximize\", mode = { \"i\", \"n\" } },\n            [\"<a-p>\"] = { \"toggle_preview\", mode = { \"i\", \"n\" } },\n            [\"<a-w>\"] = { \"cycle_win\", mode = { \"i\", \"n\" } },\n            [\"<c-a>\"] = { \"select_all\", mode = { \"n\", \"i\" } },\n            [\"<c-b>\"] = { \"preview_scroll_up\", mode = { \"i\", \"n\" } },\n            [\"<c-d>\"] = { \"list_scroll_down\", mode = { \"i\", \"n\" } },\n            [\"<c-f>\"] = { \"preview_scroll_down\", mode = { \"i\", \"n\" } },\n            [\"<c-g>\"] = { \"toggle_live\", mode = { \"i\", \"n\" } },\n            [\"<c-j>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n            [\"<c-k>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n            [\"<c-n>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n            [\"<c-p>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n            [\"<c-q>\"] = { \"qflist\", mode = { \"i\", \"n\" } },\n            [\"<c-s>\"] = { \"edit_split\", mode = { \"i\", \"n\" } },\n            [\"<c-t>\"] = { \"tab\", mode = { \"n\", \"i\" } },\n            [\"<c-u>\"] = { \"list_scroll_up\", mode = { \"i\", \"n\" } },\n            [\"<c-v>\"] = { \"edit_vsplit\", mode = { \"i\", \"n\" } },\n            [\"<c-r>#\"] = { \"insert_alt\", mode = \"i\" },\n            [\"<c-r>%\"] = { \"insert_filename\", mode = \"i\" },\n            [\"<c-r><c-a>\"] = { \"insert_cWORD\", mode = \"i\" },\n            [\"<c-r><c-f>\"] = { \"insert_file\", mode = \"i\" },\n            [\"<c-r><c-l>\"] = { \"insert_line\", mode = \"i\" },\n            [\"<c-r><c-p>\"] = { \"insert_file_full\", mode = \"i\" },\n            [\"<c-r><c-w>\"] = { \"insert_cword\", mode = \"i\" },\n            [\"<c-w>H\"] = \"layout_left\",\n            [\"<c-w>J\"] = \"layout_bottom\",\n            [\"<c-w>K\"] = \"layout_top\",\n            [\"<c-w>L\"] = \"layout_right\",\n            [\"?\"] = \"toggle_help_input\",\n            [\"G\"] = \"list_bottom\",\n            [\"gg\"] = \"list_top\",\n            [\"j\"] = \"list_down\",\n            [\"k\"] = \"list_up\",\n            [\"q\"] = \"cancel\",\n          },\n          b = {\n            minipairs_disable = true,\n          },\n        },\n        -- result list window\n        list = {\n          keys = {\n            [\"/\"] = \"toggle_focus\",\n            [\"<2-LeftMouse>\"] = \"confirm\",\n            [\"<CR>\"] = \"confirm\",\n            [\"<Down>\"] = \"list_down\",\n            [\"<Esc>\"] = \"cancel\",\n            [\"<S-CR>\"] = { { \"pick_win\", \"jump\" } },\n            [\"<S-Tab>\"] = { \"select_and_prev\", mode = { \"n\", \"x\" } },\n            [\"<Tab>\"] = { \"select_and_next\", mode = { \"n\", \"x\" } },\n            [\"<Up>\"] = \"list_up\",\n            [\"<a-d>\"] = \"inspect\",\n            [\"<a-f>\"] = \"toggle_follow\",\n            [\"<a-h>\"] = \"toggle_hidden\",\n            [\"<a-i>\"] = \"toggle_ignored\",\n            [\"<a-m>\"] = \"toggle_maximize\",\n            [\"<a-p>\"] = \"toggle_preview\",\n            [\"<a-w>\"] = \"cycle_win\",\n            [\"<c-a>\"] = \"select_all\",\n            [\"<c-b>\"] = \"preview_scroll_up\",\n            [\"<c-d>\"] = \"list_scroll_down\",\n            [\"<c-f>\"] = \"preview_scroll_down\",\n            [\"<c-j>\"] = \"list_down\",\n            [\"<c-k>\"] = \"list_up\",\n            [\"<c-n>\"] = \"list_down\",\n            [\"<c-p>\"] = \"list_up\",\n            [\"<c-q>\"] = \"qflist\",\n            [\"<c-g>\"] = \"print_path\",\n            [\"<c-s>\"] = \"edit_split\",\n            [\"<c-t>\"] = \"tab\",\n            [\"<c-u>\"] = \"list_scroll_up\",\n            [\"<c-v>\"] = \"edit_vsplit\",\n            [\"<c-w>H\"] = \"layout_left\",\n            [\"<c-w>J\"] = \"layout_bottom\",\n            [\"<c-w>K\"] = \"layout_top\",\n            [\"<c-w>L\"] = \"layout_right\",\n            [\"?\"] = \"toggle_help_list\",\n            [\"G\"] = \"list_bottom\",\n            [\"gg\"] = \"list_top\",\n            [\"i\"] = \"focus_input\",\n            [\"j\"] = \"list_down\",\n            [\"k\"] = \"list_up\",\n            [\"q\"] = \"cancel\",\n            [\"zb\"] = \"list_scroll_bottom\",\n            [\"zt\"] = \"list_scroll_top\",\n            [\"zz\"] = \"list_scroll_center\",\n          },\n          wo = {\n            conceallevel = 2,\n            concealcursor = \"nvc\",\n          },\n        },\n        -- preview window\n        preview = {\n          keys = {\n            [\"<Esc>\"] = \"cancel\",\n            [\"q\"] = \"cancel\",\n            [\"i\"] = \"focus_input\",\n            [\"<a-w>\"] = \"cycle_win\",\n          },\n        },\n      },\n      ---@class snacks.picker.icons\n      icons = {\n        files = {\n          enabled = true, -- show file icons\n          dir = \"󰉋 \",\n          dir_open = \"󰝰 \",\n          file = \"󰈔 \"\n        },\n        keymaps = {\n          nowait = \"󰓅 \"\n        },\n        tree = {\n          vertical = \"│ \",\n          middle   = \"├╴\",\n          last     = \"└╴\",\n        },\n        undo = {\n          saved   = \" \",\n        },\n        ui = {\n          live        = \"󰐰 \",\n          hidden      = \"h\",\n          ignored     = \"i\",\n          follow      = \"f\",\n          selected    = \"● \",\n          unselected  = \"○ \",\n          -- selected = \" \",\n        },\n        git = {\n          enabled   = true, -- show git icons\n          commit    = \"󰜘 \", -- used by git log\n          staged    = \"●\", -- staged changes. always overrides the type icons\n          added     = \"\",\n          deleted   = \"\",\n          ignored   = \" \",\n          modified  = \"○\",\n          renamed   = \"\",\n          unmerged  = \" \",\n          untracked = \"?\",\n        },\n        diagnostics = {\n          Error = \" \",\n          Warn  = \" \",\n          Hint  = \" \",\n          Info  = \" \",\n        },\n        lsp = {\n          unavailable = \"\",\n          enabled = \" \",\n          disabled = \" \",\n          attached = \"󰖩 \"\n        },\n        kinds = {\n          Array         = \" \",\n          Boolean       = \"󰨙 \",\n          Class         = \" \",\n          Color         = \" \",\n          Control       = \" \",\n          Collapsed     = \" \",\n          Constant      = \"󰏿 \",\n          Constructor   = \" \",\n          Copilot       = \" \",\n          Enum          = \" \",\n          EnumMember    = \" \",\n          Event         = \" \",\n          Field         = \" \",\n          File          = \" \",\n          Folder        = \" \",\n          Function      = \"󰊕 \",\n          Interface     = \" \",\n          Key           = \" \",\n          Keyword       = \" \",\n          Method        = \"󰊕 \",\n          Module        = \" \",\n          Namespace     = \"󰦮 \",\n          Null          = \" \",\n          Number        = \"󰎠 \",\n          Object        = \" \",\n          Operator      = \" \",\n          Package       = \" \",\n          Property      = \" \",\n          Reference     = \" \",\n          Snippet       = \"󱄽 \",\n          String        = \" \",\n          Struct        = \"󰆼 \",\n          Text          = \" \",\n          TypeParameter = \" \",\n          Unit          = \" \",\n          Unknown        = \" \",\n          Value         = \" \",\n          Variable      = \"󰀫 \",\n        },\n      },\n      ---@class snacks.picker.db.Config\n      db = {\n        -- path to the sqlite3 library\n        -- If not set, it will try to load the library by name.\n        -- On Windows it will download the library from the internet.\n        sqlite3_path = nil, ---@type string?\n      },\n      ---@class snacks.picker.debug\n      debug = {\n        scores = false, -- show scores in the list\n        leaks = false, -- show when pickers don't get garbage collected\n        explorer = false, -- show explorer debug info\n        files = false, -- show file debug info\n        grep = false, -- show file debug info\n        proc = false, -- show proc debug info\n        extmarks = false, -- show extmarks errors\n      },\n    }\n<\n\n\n==============================================================================\n5. Examples                                      *snacks.nvim-picker-examples*\n\n\nFLASH                                      *snacks.nvim-picker-examples-flash*\n\n>lua\n    {\n      \"folke/flash.nvim\",\n      optional = true,\n      specs = {\n        {\n          \"folke/snacks.nvim\",\n          opts = {\n            picker = {\n              win = {\n                input = {\n                  keys = {\n                    [\"<a-s>\"] = { \"flash\", mode = { \"n\", \"i\" } },\n                    [\"s\"] = { \"flash\" },\n                  },\n                },\n              },\n              actions = {\n                flash = function(picker)\n                  require(\"flash\").jump({\n                    pattern = \"^\",\n                    label = { after = { 0, 0 } },\n                    search = {\n                      mode = \"search\",\n                      exclude = {\n                        function(win)\n                          return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= \"snacks_picker_list\"\n                        end,\n                      },\n                    },\n                    action = function(match)\n                      local idx = picker.list:row2idx(match.pos[1])\n                      picker.list:_move(idx, true, true)\n                    end,\n                  })\n                end,\n              },\n            },\n          },\n        },\n      },\n    }\n<\n\n\nGENERAL                                  *snacks.nvim-picker-examples-general*\n\n>lua\n    {\n      \"folke/snacks.nvim\",\n      opts = {\n        picker = {},\n        explorer = {},\n      },\n      keys = {\n        -- Top Pickers & Explorer\n        { \"<leader><space>\", function() Snacks.picker.smart() end, desc = \"Smart Find Files\" },\n        { \"<leader>,\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n        { \"<leader>/\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n        { \"<leader>:\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n        { \"<leader>n\", function() Snacks.picker.notifications() end, desc = \"Notification History\" },\n        { \"<leader>e\", function() Snacks.explorer() end, desc = \"File Explorer\" },\n        -- find\n        { \"<leader>fb\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n        { \"<leader>fc\", function() Snacks.picker.files({ cwd = vim.fn.stdpath(\"config\") }) end, desc = \"Find Config File\" },\n        { \"<leader>ff\", function() Snacks.picker.files() end, desc = \"Find Files\" },\n        { \"<leader>fg\", function() Snacks.picker.git_files() end, desc = \"Find Git Files\" },\n        { \"<leader>fp\", function() Snacks.picker.projects() end, desc = \"Projects\" },\n        { \"<leader>fr\", function() Snacks.picker.recent() end, desc = \"Recent\" },\n        -- git\n        { \"<leader>gb\", function() Snacks.picker.git_branches() end, desc = \"Git Branches\" },\n        { \"<leader>gl\", function() Snacks.picker.git_log() end, desc = \"Git Log\" },\n        { \"<leader>gL\", function() Snacks.picker.git_log_line() end, desc = \"Git Log Line\" },\n        { \"<leader>gs\", function() Snacks.picker.git_status() end, desc = \"Git Status\" },\n        { \"<leader>gS\", function() Snacks.picker.git_stash() end, desc = \"Git Stash\" },\n        { \"<leader>gd\", function() Snacks.picker.git_diff() end, desc = \"Git Diff (Hunks)\" },\n        { \"<leader>gf\", function() Snacks.picker.git_log_file() end, desc = \"Git Log File\" },\n        -- gh\n        { \"<leader>gi\", function() Snacks.picker.gh_issue() end, desc = \"GitHub Issues (open)\" },\n        { \"<leader>gI\", function() Snacks.picker.gh_issue({ state = \"all\" }) end, desc = \"GitHub Issues (all)\" },\n        { \"<leader>gp\", function() Snacks.picker.gh_pr() end, desc = \"GitHub Pull Requests (open)\" },\n        { \"<leader>gP\", function() Snacks.picker.gh_pr({ state = \"all\" }) end, desc = \"GitHub Pull Requests (all)\" },\n        -- Grep\n        { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n        { \"<leader>sB\", function() Snacks.picker.grep_buffers() end, desc = \"Grep Open Buffers\" },\n        { \"<leader>sg\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n        { \"<leader>sw\", function() Snacks.picker.grep_word() end, desc = \"Visual selection or word\", mode = { \"n\", \"x\" } },\n        -- search\n        { '<leader>s\"', function() Snacks.picker.registers() end, desc = \"Registers\" },\n        { '<leader>s/', function() Snacks.picker.search_history() end, desc = \"Search History\" },\n        { \"<leader>sa\", function() Snacks.picker.autocmds() end, desc = \"Autocmds\" },\n        { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n        { \"<leader>sc\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n        { \"<leader>sC\", function() Snacks.picker.commands() end, desc = \"Commands\" },\n        { \"<leader>sd\", function() Snacks.picker.diagnostics() end, desc = \"Diagnostics\" },\n        { \"<leader>sD\", function() Snacks.picker.diagnostics_buffer() end, desc = \"Buffer Diagnostics\" },\n        { \"<leader>sh\", function() Snacks.picker.help() end, desc = \"Help Pages\" },\n        { \"<leader>sH\", function() Snacks.picker.highlights() end, desc = \"Highlights\" },\n        { \"<leader>si\", function() Snacks.picker.icons() end, desc = \"Icons\" },\n        { \"<leader>sj\", function() Snacks.picker.jumps() end, desc = \"Jumps\" },\n        { \"<leader>sk\", function() Snacks.picker.keymaps() end, desc = \"Keymaps\" },\n        { \"<leader>sl\", function() Snacks.picker.loclist() end, desc = \"Location List\" },\n        { \"<leader>sm\", function() Snacks.picker.marks() end, desc = \"Marks\" },\n        { \"<leader>sM\", function() Snacks.picker.man() end, desc = \"Man Pages\" },\n        { \"<leader>sp\", function() Snacks.picker.lazy() end, desc = \"Search for Plugin Spec\" },\n        { \"<leader>sq\", function() Snacks.picker.qflist() end, desc = \"Quickfix List\" },\n        { \"<leader>sR\", function() Snacks.picker.resume() end, desc = \"Resume\" },\n        { \"<leader>su\", function() Snacks.picker.undo() end, desc = \"Undo History\" },\n        { \"<leader>uC\", function() Snacks.picker.colorschemes() end, desc = \"Colorschemes\" },\n        -- LSP\n        { \"gd\", function() Snacks.picker.lsp_definitions() end, desc = \"Goto Definition\" },\n        { \"gD\", function() Snacks.picker.lsp_declarations() end, desc = \"Goto Declaration\" },\n        { \"gr\", function() Snacks.picker.lsp_references() end, nowait = true, desc = \"References\" },\n        { \"gI\", function() Snacks.picker.lsp_implementations() end, desc = \"Goto Implementation\" },\n        { \"gy\", function() Snacks.picker.lsp_type_definitions() end, desc = \"Goto T[y]pe Definition\" },\n        { \"gai\", function() Snacks.picker.lsp_incoming_calls() end, desc = \"C[a]lls Incoming\" },\n        { \"gao\", function() Snacks.picker.lsp_outgoing_calls() end, desc = \"C[a]lls Outgoing\" },\n        { \"<leader>ss\", function() Snacks.picker.lsp_symbols() end, desc = \"LSP Symbols\" },\n        { \"<leader>sS\", function() Snacks.picker.lsp_workspace_symbols() end, desc = \"LSP Workspace Symbols\" },\n      },\n    }\n<\n\n\nTODO_COMMENTS                      *snacks.nvim-picker-examples-todo_comments*\n\n>lua\n    {\n      \"folke/todo-comments.nvim\",\n      optional = true,\n      keys = {\n        { \"<leader>st\", function() Snacks.picker.todo_comments() end, desc = \"Todo\" },\n        { \"<leader>sT\", function () Snacks.picker.todo_comments({ keywords = { \"TODO\", \"FIX\", \"FIXME\" } }) end, desc = \"Todo/Fix/Fixme\" },\n      },\n    }\n<\n\n\nTROUBLE                                  *snacks.nvim-picker-examples-trouble*\n\n>lua\n    {\n      \"folke/trouble.nvim\",\n      optional = true,\n      specs = {\n        \"folke/snacks.nvim\",\n        opts = function(_, opts)\n          return vim.tbl_deep_extend(\"force\", opts or {}, {\n            picker = {\n              actions = require(\"trouble.sources.snacks\").actions,\n              win = {\n                input = {\n                  keys = {\n                    [\"<c-t>\"] = {\n                      \"trouble_open\",\n                      mode = { \"n\", \"i\" },\n                    },\n                  },\n                },\n              },\n            },\n          })\n        end,\n      },\n    }\n<\n\n\n==============================================================================\n6. Types                                            *snacks.nvim-picker-types*\n\n>lua\n    ---@class snacks.picker.resume.Opts\n    ---@field source? string\n    ---@field include? string[]\n    ---@field exclude? string[]\n<\n\n>lua\n    ---@class snacks.picker.jump.Action: snacks.picker.Action\n    ---@field cmd? snacks.picker.EditCmd\n<\n\n>lua\n    ---@class snacks.picker.layout.Action: snacks.picker.Action\n    ---@field layout? snacks.picker.layout.Config|string\n<\n\n>lua\n    ---@class snacks.picker.yank.Action: snacks.picker.Action\n    ---@field reg? string\n    ---@field field? string\n    ---@field notify? boolean\n<\n\n>lua\n    ---@class snacks.picker.insert.Action: snacks.picker.Action\n    ---@field expr string\n<\n\n>lua\n    ---@alias snacks.picker.format.resolve fun(max_width:number):snacks.picker.Highlight[]\n    ---@alias snacks.picker.Extmark vim.api.keyset.set_extmark|{col:number, row?:number, field?:string}\n    ---@alias snacks.picker.Meta {[string]:any}\n    ---@alias snacks.picker.Text {[1]:string, [2]:(string|string[])?, virtual?:boolean, field?:string, resolve?:snacks.picker.format.resolve, inline?:boolean}\n    ---@alias snacks.picker.Highlight snacks.picker.Text|snacks.picker.Extmark|{meta?:snacks.picker.Meta}\n    ---@alias snacks.picker.format fun(item:snacks.picker.Item, picker:snacks.Picker):snacks.picker.Highlight[]\n    ---@alias snacks.picker.preview fun(ctx: snacks.picker.preview.ctx):boolean?\n    ---@alias snacks.picker.sort fun(a:snacks.picker.Item, b:snacks.picker.Item):boolean\n    ---@alias snacks.picker.transform fun(item:snacks.picker.finder.Item, ctx:snacks.picker.finder.ctx):(boolean|snacks.picker.finder.Item|nil)\n    ---@alias snacks.picker.Pos {[1]:number, [2]:number}\n    ---@alias snacks.picker.toggle {icon?:string, enabled?:boolean, value?:boolean}\n<\n\nGeneric filter used by some finders to pre-filter items\n\n>lua\n    ---@class snacks.picker.filter.Config\n    ---@field cwd? boolean|string only show files for the given cwd\n    ---@field buf? boolean|number only show items for the current or given buffer\n    ---@field paths? table<string, boolean> only show items that include or exclude the given paths\n    ---@field filter? fun(item:snacks.picker.finder.Item, filter:snacks.picker.Filter):boolean? custom filter function\n    ---@field transform? fun(picker:snacks.Picker, filter:snacks.picker.Filter):boolean? filter transform. Return `true` to force refresh\n<\n\nThis is only used when using `opts.preview = \"preview\"`. It’s a previewer\nthat shows a preview based on the item data.\n\n>lua\n    ---@class snacks.picker.Item.preview\n    ---@field text string text to show in the preview buffer\n    ---@field ft? string optional filetype used tohighlight the preview buffer\n    ---@field extmarks? snacks.picker.Extmark[] additional extmarks\n    ---@field loc? boolean set to false to disable showing the item location in the preview\n<\n\n>lua\n    ---@class snacks.picker.Item\n    ---@field [string] any\n    ---@field idx number\n    ---@field score number\n    ---@field frecency? number\n    ---@field score_add? number\n    ---@field score_mul? number\n    ---@field source_id? number\n    ---@field file? string\n    ---@field text string\n    ---@field pos? snacks.picker.Pos\n    ---@field loc? snacks.picker.lsp.Loc\n    ---@field end_pos? snacks.picker.Pos\n    ---@field highlights? snacks.picker.Highlight[][]\n    ---@field preview? snacks.picker.Item.preview\n    ---@field resolve? fun(item:snacks.picker.Item)\n    ---@field positions? number[] indices of matched characters in `text`\n<\n\n>lua\n    ---@class snacks.picker.finder.Item: snacks.picker.Item\n    ---@field idx? number\n    ---@field score? number\n<\n\n>lua\n    ---@class snacks.picker.layout.Config\n    ---@field layout snacks.layout.Box\n    ---@field reverse? boolean when true, the list will be reversed (bottom-up)\n    ---@field fullscreen? boolean open in fullscreen\n    ---@field cycle? boolean cycle through the list\n    ---@field preview? \"main\" show preview window in the picker or the main window\n    ---@field preset? string|fun(source:string):string\n    ---@field hidden? (\"input\"|\"preview\"|\"list\")[] don't show the given windows when opening the picker. (only \"input\" and \"preview\" make sense)\n    ---@field auto_hide? (\"input\"|\"preview\"|\"list\")[] hide the given windows when not focused (only \"input\" makes real sense)\n    ---@field config? fun(layout:snacks.picker.layout.Config) customize the resolved layout config\n<\n\n>lua\n    ---@class snacks.picker.win.Config\n    ---@field input? snacks.win.Config|{} input window config\n    ---@field list? snacks.win.Config|{} result list window config\n    ---@field preview? snacks.win.Config|{} preview window config\n<\n\n>lua\n    ---@alias snacks.Picker.ref (fun():snacks.Picker?)|{value?: snacks.Picker}\n<\n\n>lua\n    ---@alias snacks.picker.history.Record {pattern: string, search: string, live?: boolean}\n<\n\n\n==============================================================================\n7. Module                                          *snacks.nvim-picker-module*\n\n>lua\n    ---@class snacks.picker\n    ---@field actions snacks.picker.actions\n    ---@field config snacks.picker.config\n    ---@field format snacks.picker.formatters\n    ---@field preview snacks.picker.previewers\n    ---@field sort snacks.picker.sorters\n    ---@field util snacks.picker.util\n    ---@field current? snacks.Picker\n    ---@field highlight snacks.picker.highlight\n    ---@field resume fun(opts?: snacks.picker.Config):snacks.Picker\n    ---@field sources snacks.picker.sources.Config\n    Snacks.picker = {}\n<\n\n\n`Snacks.picker()`                                            *Snacks.picker()*\n\n>lua\n    ---@type fun(source: string, opts: snacks.picker.Config): snacks.Picker\n    Snacks.picker()\n<\n\n>lua\n    ---@type fun(opts: snacks.picker.Config): snacks.Picker\n    Snacks.picker()\n<\n\n\n`Snacks.picker.get()`                                    *Snacks.picker.get()*\n\nGet active pickers, optionally filtered by source, or the current tab\n\n>lua\n    ---@param opts? {source?: string, tab?: boolean} tab defaults to true\n    Snacks.picker.get(opts)\n<\n\n\n`Snacks.picker.pick()`                                  *Snacks.picker.pick()*\n\nCreate a new picker\n\n>lua\n    ---@param source? string\n    ---@param opts? snacks.picker.Config\n    ---@overload fun(opts: snacks.picker.Config): snacks.Picker\n    Snacks.picker.pick(source, opts)\n<\n\n\n`Snacks.picker.resume()`                              *Snacks.picker.resume()*\n\n>lua\n    ---@param opts? snacks.picker.resume.Opts\n    ---@overload fun(source:string):snacks.Picker?\n    ---@return snacks.Picker?\n    Snacks.picker.resume(opts)\n<\n\n\n`Snacks.picker.select()`                              *Snacks.picker.select()*\n\nImplementation for `vim.ui.select`\n\n>lua\n    ---@type snacks.picker.ui_select\n    Snacks.picker.select(...)\n<\n\n\n==============================================================================\n8. Sources                                        *snacks.nvim-picker-sources*\n\n\nAUTOCMDS                                 *snacks.nvim-picker-sources-autocmds*\n\n>vim\n    :lua Snacks.picker.autocmds(opts?)\n<\n\n>lua\n    {\n      finder = \"vim_autocmds\",\n      format = \"autocmd\",\n      preview = \"preview\",\n    }\n<\n\n\nBUFFERS                                   *snacks.nvim-picker-sources-buffers*\n\n>vim\n    :lua Snacks.picker.buffers(opts?)\n<\n\n>lua\n    ---@class snacks.picker.buffers.Config: snacks.picker.Config\n    ---@field hidden? boolean show hidden buffers (unlisted)\n    ---@field unloaded? boolean show loaded buffers\n    ---@field current? boolean show current buffer\n    ---@field nofile? boolean show `buftype=nofile` buffers\n    ---@field modified? boolean show only modified buffers\n    ---@field sort_lastused? boolean sort by last used\n    ---@field filter? snacks.picker.filter.Config\n    {\n      finder = \"buffers\",\n      format = \"buffer\",\n      hidden = false,\n      unloaded = true,\n      current = true,\n      sort_lastused = true,\n      win = {\n        input = {\n          keys = {\n            [\"<c-x>\"] = { \"bufdelete\", mode = { \"n\", \"i\" } },\n          },\n        },\n        list = { keys = { [\"dd\"] = \"bufdelete\" } },\n      },\n    }\n<\n\n\nCLIPHIST                                 *snacks.nvim-picker-sources-cliphist*\n\n>vim\n    :lua Snacks.picker.cliphist(opts?)\n<\n\n>lua\n    {\n      finder = \"system_cliphist\",\n      format = \"text\",\n      preview = \"preview\",\n      confirm = { \"copy\", \"close\" },\n    }\n<\n\n\nCOLORSCHEMES                         *snacks.nvim-picker-sources-colorschemes*\n\n>vim\n    :lua Snacks.picker.colorschemes(opts?)\n<\n\nNeovim colorschemes with live preview\n\n>lua\n    {\n      finder = \"vim_colorschemes\",\n      format = \"text\",\n      preview = \"colorscheme\",\n      preset = \"vertical\",\n      confirm = function(picker, item)\n        picker:close()\n        if item then\n          picker.preview.state.colorscheme = nil\n          vim.schedule(function()\n            vim.cmd(\"colorscheme \" .. item.text)\n          end)\n        end\n      end,\n    }\n<\n\n\nCOMMAND_HISTORY                   *snacks.nvim-picker-sources-command_history*\n\n>vim\n    :lua Snacks.picker.command_history(opts?)\n<\n\nNeovim command history\n\n>lua\n    ---@type snacks.picker.history.Config\n    {\n      finder = \"vim_history\",\n      name = \"cmd\",\n      format = \"text\",\n      preview = \"none\",\n      main = { current = true },\n      layout = {\n        preset = \"vscode\",\n      },\n      confirm = \"cmd\",\n      formatters = { text = { ft = \"vim\" } },\n    }\n<\n\n\nCOMMANDS                                 *snacks.nvim-picker-sources-commands*\n\n>vim\n    :lua Snacks.picker.commands(opts?)\n<\n\nNeovim commands\n\n>lua\n    {\n      finder = \"vim_commands\",\n      format = \"command\",\n      preview = \"preview\",\n      confirm = \"cmd\",\n    }\n<\n\n\nDIAGNOSTICS                           *snacks.nvim-picker-sources-diagnostics*\n\n>vim\n    :lua Snacks.picker.diagnostics(opts?)\n<\n\n>lua\n    ---@class snacks.picker.diagnostics.Config: snacks.picker.Config\n    ---@field filter? snacks.picker.filter.Config\n    ---@field severity? vim.diagnostic.SeverityFilter\n    {\n      finder = \"diagnostics\",\n      format = \"diagnostic\",\n      sort = {\n        fields = {\n          \"is_current\",\n          \"is_cwd\",\n          \"severity\",\n          \"file\",\n          \"lnum\",\n        },\n      },\n      matcher = { sort_empty = true },\n      -- only show diagnostics from the cwd by default\n      filter = { cwd = true },\n    }\n<\n\n\nDIAGNOSTICS_BUFFER             *snacks.nvim-picker-sources-diagnostics_buffer*\n\n>vim\n    :lua Snacks.picker.diagnostics_buffer(opts?)\n<\n\n>lua\n    ---@type snacks.picker.diagnostics.Config\n    {\n      finder = \"diagnostics\",\n      format = \"diagnostic\",\n      sort = {\n        fields = { \"severity\", \"file\", \"lnum\" },\n      },\n      matcher = { sort_empty = true },\n      filter = { buf = true },\n    }\n<\n\n\nEXPLORER                                 *snacks.nvim-picker-sources-explorer*\n\n>vim\n    :lua Snacks.picker.explorer(opts?)\n<\n\n>lua\n    ---@class snacks.picker.explorer.Config: snacks.picker.files.Config|{}\n    ---@field follow_file? boolean follow the file from the current buffer\n    ---@field tree? boolean show the file tree (default: true)\n    ---@field git_status? boolean show git status (default: true)\n    ---@field git_status_open? boolean show recursive git status for open directories\n    ---@field git_untracked? boolean needed to show untracked git status\n    ---@field diagnostics? boolean show diagnostics\n    ---@field diagnostics_open? boolean show recursive diagnostics for open directories\n    ---@field watch? boolean watch for file changes\n    ---@field exclude? string[] exclude glob patterns\n    ---@field include? string[] include glob patterns. These take precedence over `exclude`, `ignored` and `hidden`\n    {\n      finder = \"explorer\",\n      sort = { fields = { \"sort\" } },\n      supports_live = true,\n      tree = true,\n      watch = true,\n      diagnostics = true,\n      diagnostics_open = false,\n      git_status = true,\n      git_status_open = false,\n      git_untracked = true,\n      follow_file = true,\n      focus = \"list\",\n      auto_close = false,\n      jump = { close = false },\n      layout = { preset = \"sidebar\", preview = false },\n      -- to show the explorer to the right, add the below to\n      -- your config under `opts.picker.sources.explorer`\n      -- layout = { layout = { position = \"right\" } },\n      formatters = {\n        file = { filename_only = true },\n        severity = { pos = \"right\" },\n      },\n      matcher = { sort_empty = false, fuzzy = false },\n      config = function(opts)\n        return require(\"snacks.picker.source.explorer\").setup(opts)\n      end,\n      win = {\n        list = {\n          keys = {\n            [\"<BS>\"] = \"explorer_up\",\n            [\"l\"] = \"confirm\",\n            [\"h\"] = \"explorer_close\", -- close directory\n            [\"a\"] = \"explorer_add\",\n            [\"d\"] = \"explorer_del\",\n            [\"r\"] = \"explorer_rename\",\n            [\"c\"] = \"explorer_copy\",\n            [\"m\"] = \"explorer_move\",\n            [\"o\"] = \"explorer_open\", -- open with system application\n            [\"P\"] = \"toggle_preview\",\n            [\"y\"] = { \"explorer_yank\", mode = { \"n\", \"x\" } },\n            [\"p\"] = \"explorer_paste\",\n            [\"u\"] = \"explorer_update\",\n            [\"<c-c>\"] = \"tcd\",\n            [\"<leader>/\"] = \"picker_grep\",\n            [\"<c-t>\"] = \"terminal\",\n            [\".\"] = \"explorer_focus\",\n            [\"I\"] = \"toggle_ignored\",\n            [\"H\"] = \"toggle_hidden\",\n            [\"Z\"] = \"explorer_close_all\",\n            [\"]g\"] = \"explorer_git_next\",\n            [\"[g\"] = \"explorer_git_prev\",\n            [\"]d\"] = \"explorer_diagnostic_next\",\n            [\"[d\"] = \"explorer_diagnostic_prev\",\n            [\"]w\"] = \"explorer_warn_next\",\n            [\"[w\"] = \"explorer_warn_prev\",\n            [\"]e\"] = \"explorer_error_next\",\n            [\"[e\"] = \"explorer_error_prev\",\n          },\n        },\n      },\n    }\n<\n\n\nFILES                                       *snacks.nvim-picker-sources-files*\n\n>vim\n    :lua Snacks.picker.files(opts?)\n<\n\n>lua\n    ---@class snacks.picker.files.Config: snacks.picker.proc.Config\n    ---@field cmd? \"fd\"| \"rg\"| \"find\" command to use. Leave empty to auto-detect\n    ---@field hidden? boolean show hidden files\n    ---@field ignored? boolean show ignored files\n    ---@field dirs? string[] directories to search\n    ---@field follow? boolean follow symlinks\n    ---@field exclude? string[] exclude patterns\n    ---@field args? string[] additional arguments\n    ---@field ft? string|string[] file extension(s)\n    ---@field rtp? boolean search in runtimepath\n    {\n      finder = \"files\",\n      format = \"file\",\n      show_empty = true,\n      hidden = false,\n      ignored = false,\n      follow = false,\n      supports_live = true,\n    }\n<\n\n\nGH_ACTIONS                             *snacks.nvim-picker-sources-gh_actions*\n\n>vim\n    :lua Snacks.picker.gh_actions(opts?)\n<\n\n>lua\n    ---@class snacks.picker.gh.actions.Config: snacks.picker.Config\n    ---@field number number issue or PR number\n    ---@field repo string GitHub repository (owner/repo). Defaults to current git repo\n    ---@field type \"issue\" | \"pr\"\n    ---@field item? snacks.picker.gh.Item\n    {\n      layout = { preset = \"select\", layout = { max_width = 50 } },\n      title = \"  Actions\",\n      main = { current = true },\n      finder = \"gh_get_actions\",\n      format = \"gh_format_action\",\n      confirm = \"gh_perform_action\",\n    }\n<\n\n\nGH_DIFF                                   *snacks.nvim-picker-sources-gh_diff*\n\n>vim\n    :lua Snacks.picker.gh_diff(opts?)\n<\n\n>lua\n    ---@class snacks.picker.gh.diff.Config: snacks.picker.Config\n    ---@field group? boolean group changes by file (when false, show individual hunks)\n    ---@field pr number number PR number to diff against\n    ---@field repo? string GitHub repository (owner/repo). Defaults to current git repo\n    {\n      title = \"  Pull Request Diff\",\n      group = true,\n      finder = \"gh_diff\",\n      format = \"git_status\",\n      preview = \"gh_preview_diff\",\n      win = {\n        preview = {\n          keys = {\n            [\"a\"] = { \"gh_comment\", mode = { \"n\", \"x\" } },\n            [\"<cr>\"] = { \"gh_actions\", mode = { \"n\", \"x\" } },\n          },\n        },\n      },\n    }\n<\n\n\nGH_ISSUE                                 *snacks.nvim-picker-sources-gh_issue*\n\n>vim\n    :lua Snacks.picker.gh_issue(opts?)\n<\n\n>lua\n    ---@class snacks.picker.gh.issue.Config: snacks.picker.gh.Config\n    ---@field state \"open\" | \"closed\" | \"all\"\n    ---@field mention? string filter by mention\n    ---@field milestone? string filter by milestone\n    {\n      title = \"  Issues\",\n      finder = \"gh_issue\",\n      format = \"gh_format\",\n      preview = \"gh_preview\",\n      sort = { fields = { \"score:desc\", \"idx\" } },\n      supports_live = true,\n      live = true,\n      confirm = \"gh_actions\",\n      win = {\n        input = {\n          keys = {\n            [\"<a-b>\"] = { \"gh_browse\", mode = { \"n\", \"i\" } },\n            [\"<c-y>\"] = { \"gh_yank\", mode = { \"n\", \"i\" } },\n          },\n        },\n        list = {\n          keys = {\n            [\"y\"] = { \"gh_yank\", mode = { \"n\", \"x\" } },\n          },\n        },\n      },\n    }\n<\n\n\nGH_LABELS                               *snacks.nvim-picker-sources-gh_labels*\n\n>vim\n    :lua Snacks.picker.gh_labels(opts?)\n<\n\n>lua\n    ---@class snacks.picker.gh.labels.Config: snacks.picker.Config\n    ---@field number number issue or PR number\n    ---@field repo string GitHub repository (owner/repo). Defaults to current git repo\n    {\n      layout = { preset = \"select\", layout = { max_width = 50 } },\n      title = \"  Labels\",\n      main = { current = true },\n      group = true,\n      finder = \"gh_labels\",\n      format = \"gh_format_label\",\n    }\n<\n\n\nGH_PR                                       *snacks.nvim-picker-sources-gh_pr*\n\n>vim\n    :lua Snacks.picker.gh_pr(opts?)\n<\n\n>lua\n    ---@class snacks.picker.gh.pr.Config: snacks.picker.gh.Config\n    ---@field state \"open\" | \"closed\" | \"merged\" | \"all\"\n    ---@field draft? boolean filter draft PRs\n    ---@field base? string filter by base branch\n    {\n      title = \"  Pull Requests\",\n      finder = \"gh_pr\",\n      format = \"gh_format\",\n      preview = \"gh_preview\",\n      sort = { fields = { \"score:desc\", \"idx\" } },\n      supports_live = true,\n      live = true,\n      confirm = \"gh_actions\",\n      win = {\n        input = {\n          keys = {\n            [\"<a-b>\"] = { \"gh_browse\", mode = { \"n\", \"i\" } },\n            [\"<c-y>\"] = { \"gh_yank\", mode = { \"n\", \"i\" } },\n          },\n        },\n        list = {\n          keys = {\n            [\"y\"] = { \"gh_yank\", mode = { \"n\", \"x\" } },\n          },\n        },\n      },\n    }\n<\n\n\nGH_REACTIONS                         *snacks.nvim-picker-sources-gh_reactions*\n\n>vim\n    :lua Snacks.picker.gh_reactions(opts?)\n<\n\n>lua\n    ---@class snacks.picker.gh.reactions.Config: snacks.picker.Config\n    ---@field number number issue or PR number\n    ---@field repo string GitHub repository (owner/repo). Defaults to current git repo\n    {\n      layout = { preset = \"select\", layout = { max_width = 50 } },\n      title = \"  Reactions\",\n      main = { current = true },\n      group = true,\n      finder = \"gh_reactions\",\n      format = \"gh_format_reaction\",\n    }\n<\n\n\nGIT_BRANCHES                         *snacks.nvim-picker-sources-git_branches*\n\n>vim\n    :lua Snacks.picker.git_branches(opts?)\n<\n\n>lua\n    ---@class snacks.picker.git.branches.Config: snacks.picker.git.Config\n    ---@field all? boolean show all branches, including remote\n    {\n      all = false,\n      finder = \"git_branches\",\n      format = \"git_branch\",\n      preview = \"git_log\",\n      confirm = \"git_checkout\",\n      win = {\n        input = {\n          keys = {\n            [\"<c-a>\"] = { \"git_branch_add\", mode = { \"n\", \"i\" } },\n            [\"<c-x>\"] = { \"git_branch_del\", mode = { \"n\", \"i\" } },\n          },\n        },\n      },\n      ---@param picker snacks.Picker\n      on_show = function(picker)\n        for i, item in ipairs(picker:items()) do\n          if item.current then\n            picker.list:view(i)\n            Snacks.picker.actions.list_scroll_center(picker)\n            break\n          end\n        end\n      end,\n    }\n<\n\n\nGIT_DIFF                                 *snacks.nvim-picker-sources-git_diff*\n\n>vim\n    :lua Snacks.picker.git_diff(opts?)\n<\n\n>lua\n    ---@class snacks.picker.git.diff.Config: snacks.picker.git.Config\n    ---@field group? boolean group changes by file (when false, show individual hunks)\n    ---@field staged? boolean show staged changes\n    ---@field base? string base commit/branch/tag to diff against (default: HEAD)\n    {\n      group = false,\n      finder = \"git_diff\",\n      format = \"git_status\",\n      preview = \"diff\",\n      matcher = { sort_empty = true },\n      sort = { fields = { \"score:desc\", \"file\", \"idx\" } },\n      win = {\n        input = {\n          keys = {\n            [\"<Tab>\"] = { \"git_stage\", mode = { \"n\", \"i\" } },\n            [\"<c-r>\"] = { \"git_restore\", mode = { \"n\", \"i\" }, nowait = true },\n          },\n        },\n      },\n    }\n<\n\n\nGIT_FILES                               *snacks.nvim-picker-sources-git_files*\n\n>vim\n    :lua Snacks.picker.git_files(opts?)\n<\n\nFind git files\n\n>lua\n    ---@class snacks.picker.git.files.Config: snacks.picker.git.Config\n    ---@field untracked? boolean show untracked files\n    ---@field submodules? boolean show submodule files\n    {\n      finder = \"git_files\",\n      show_empty = true,\n      format = \"file\",\n      untracked = false,\n      submodules = false,\n    }\n<\n\n\nGIT_GREP                                 *snacks.nvim-picker-sources-git_grep*\n\n>vim\n    :lua Snacks.picker.git_grep(opts?)\n<\n\nGrep in git files\n\n>lua\n    ---@class snacks.picker.git.grep.Config: snacks.picker.git.Config\n    ---@field untracked? boolean search in untracked files\n    ---@field submodules? boolean search in submodule files\n    ---@field need_search? boolean require a search pattern\n    ---@field pathspec? string|string[] pathspec pattern(s)\n    ---@field ignorecase? boolean ignore case\n    {\n      finder = \"git_grep\",\n      format = \"file\",\n      untracked = false,\n      need_search = true,\n      submodules = false,\n      show_empty = true,\n      supports_live = true,\n      live = true,\n    }\n<\n\n\nGIT_LOG                                   *snacks.nvim-picker-sources-git_log*\n\n>vim\n    :lua Snacks.picker.git_log(opts?)\n<\n\nGit log\n\n>lua\n    ---@class snacks.picker.git.log.Config: snacks.picker.git.Config\n    ---@field follow? boolean track file history across renames\n    ---@field current_file? boolean show current file log\n    ---@field current_line? boolean show current line log\n    ---@field author? string filter commits by author\n    {\n      finder = \"git_log\",\n      format = \"git_log\",\n      preview = \"git_show\",\n      confirm = \"git_checkout\",\n      supports_live = true,\n      sort = { fields = { \"score:desc\", \"idx\" } },\n    }\n<\n\n\nGIT_LOG_FILE                         *snacks.nvim-picker-sources-git_log_file*\n\n>vim\n    :lua Snacks.picker.git_log_file(opts?)\n<\n\n>lua\n    ---@type snacks.picker.git.log.Config\n    {\n      finder = \"git_log\",\n      format = \"git_log\",\n      preview = \"git_show\",\n      current_file = true,\n      follow = true,\n      confirm = \"git_checkout\",\n      sort = { fields = { \"score:desc\", \"idx\" } },\n    }\n<\n\n\nGIT_LOG_LINE                         *snacks.nvim-picker-sources-git_log_line*\n\n>vim\n    :lua Snacks.picker.git_log_line(opts?)\n<\n\n>lua\n    ---@type snacks.picker.git.log.Config\n    {\n      finder = \"git_log\",\n      format = \"git_log\",\n      preview = \"git_show\",\n      current_line = true,\n      follow = true,\n      confirm = \"git_checkout\",\n      sort = { fields = { \"score:desc\", \"idx\" } },\n    }\n<\n\n\nGIT_STASH                               *snacks.nvim-picker-sources-git_stash*\n\n>vim\n    :lua Snacks.picker.git_stash(opts?)\n<\n\n>lua\n    {\n      finder = \"git_stash\",\n      format = \"git_stash\",\n      preview = \"git_stash\",\n      confirm = \"git_stash_apply\",\n    }\n<\n\n\nGIT_STATUS                             *snacks.nvim-picker-sources-git_status*\n\n>vim\n    :lua Snacks.picker.git_status(opts?)\n<\n\n>lua\n    ---@class snacks.picker.git.status.Config: snacks.picker.git.Config\n    ---@field ignored? boolean show ignored files\n    {\n      finder = \"git_status\",\n      format = \"git_status\",\n      preview = \"git_status\",\n      win = {\n        input = {\n          keys = {\n            [\"<Tab>\"] = { \"git_stage\", mode = { \"n\", \"i\" } },\n            [\"<c-r>\"] = { \"git_restore\", mode = { \"n\", \"i\" }, nowait = true },\n          },\n        },\n      },\n    }\n<\n\n\nGREP                                         *snacks.nvim-picker-sources-grep*\n\n>vim\n    :lua Snacks.picker.grep(opts?)\n<\n\n>lua\n    ---@class snacks.picker.grep.Config: snacks.picker.proc.Config\n    ---@field cmd? string\n    ---@field hidden? boolean show hidden files\n    ---@field ignored? boolean show ignored files\n    ---@field dirs? string[] directories to search\n    ---@field follow? boolean follow symlinks\n    ---@field glob? string|string[] glob file pattern(s)\n    ---@field ft? string|string[] ripgrep file type(s). See `rg --type-list`\n    ---@field regex? boolean use regex search pattern (defaults to `true`)\n    ---@field buffers? boolean search in open buffers\n    ---@field need_search? boolean require a search pattern\n    ---@field exclude? string[] exclude patterns\n    ---@field args? string[] additional arguments\n    ---@field rtp? boolean search in runtimepath\n    {\n      finder = \"grep\",\n      regex = true,\n      format = \"file\",\n      show_empty = true,\n      live = true, -- live grep by default\n      supports_live = true,\n    }\n<\n\n\nGREP_BUFFERS                         *snacks.nvim-picker-sources-grep_buffers*\n\n>vim\n    :lua Snacks.picker.grep_buffers(opts?)\n<\n\n>lua\n    ---@type snacks.picker.grep.Config|{}\n    {\n      finder = \"grep\",\n      format = \"file\",\n      live = true,\n      buffers = true,\n      need_search = false,\n      supports_live = true,\n    }\n<\n\n\nGREP_WORD                               *snacks.nvim-picker-sources-grep_word*\n\n>vim\n    :lua Snacks.picker.grep_word(opts?)\n<\n\n>lua\n    ---@type snacks.picker.grep.Config|{}\n    {\n      finder = \"grep\",\n      regex = false,\n      args = { \"--word-regexp\" },\n      format = \"file\",\n      search = function(picker)\n        return picker:word()\n      end,\n      live = false,\n      supports_live = true,\n    }\n<\n\n\nHELP                                         *snacks.nvim-picker-sources-help*\n\n>vim\n    :lua Snacks.picker.help(opts?)\n<\n\nNeovim help tags\n\n>lua\n    ---@class snacks.picker.help.Config: snacks.picker.Config\n    ---@field lang? string[] defaults to `vim.opt.helplang`\n    {\n      finder = \"help\",\n      format = \"text\",\n      previewers = {\n        file = { ft = \"help\" },\n      },\n      win = { preview = { minimal = true } },\n      confirm = \"help\",\n    }\n<\n\n\nHIGHLIGHTS                             *snacks.nvim-picker-sources-highlights*\n\n>vim\n    :lua Snacks.picker.highlights(opts?)\n<\n\n>lua\n    {\n      finder = \"vim_highlights\",\n      format = \"hl\",\n      preview = \"preview\",\n      confirm = \"close\",\n    }\n<\n\n\nICONS                                       *snacks.nvim-picker-sources-icons*\n\n>vim\n    :lua Snacks.picker.icons(opts?)\n<\n\n>lua\n    ---@class snacks.picker.icons.Config: snacks.picker.Config\n    ---@field icon_sources? string[] list of sources to use\n    --- Custom icon sources can be added here. The key is the source name,\n    --- and the value is the file path or URL to load icons from.\n    --- The file should be a JSON array of:\n    --- `{[1]:string, [2]:string}|{icon:string, name:string, category:string}`\n    --- The format is compatible with https://github.com/nvim-telescope/telescope-symbols.nvim\n    ---@field custom_sources? table<string,string> additional icon sources `table<source,file|url>`\n    {\n      main = { current = true },\n      finder = \"icons\",\n      format = \"icon\",\n      layout = { preset = \"vscode\" },\n      confirm = \"put\",\n    }\n<\n\n\nJUMPS                                       *snacks.nvim-picker-sources-jumps*\n\n>vim\n    :lua Snacks.picker.jumps(opts?)\n<\n\n>lua\n    {\n      finder = \"vim_jumps\",\n      format = \"file\",\n      main = { current = true },\n    }\n<\n\n\nKEYMAPS                                   *snacks.nvim-picker-sources-keymaps*\n\n>vim\n    :lua Snacks.picker.keymaps(opts?)\n<\n\n>lua\n    ---@class snacks.picker.keymaps.Config: snacks.picker.Config\n    ---@field global? boolean show global keymaps\n    ---@field local? boolean show buffer keymaps\n    ---@field plugs? boolean show plugin keymaps\n    ---@field modes? string[]\n    {\n      finder = \"vim_keymaps\",\n      format = \"keymap\",\n      preview = \"preview\",\n      global = true,\n      plugs = false,\n      [\"local\"] = true,\n      modes = { \"n\", \"v\", \"x\", \"s\", \"o\", \"i\", \"c\", \"t\" },\n      ---@param picker snacks.Picker\n      confirm = function(picker, item)\n        picker:norm(function()\n          if item then\n            picker:close()\n            vim.api.nvim_input(item.item.lhs)\n          end\n        end)\n      end,\n      actions = {\n        toggle_global = function(picker)\n          picker.opts.global = not picker.opts.global\n          picker:find()\n        end,\n        toggle_buffer = function(picker)\n          picker.opts[\"local\"] = not picker.opts[\"local\"]\n          picker:find()\n        end,\n      },\n      win = {\n        input = {\n          keys = {\n            [\"<a-g>\"] = { \"toggle_global\", mode = { \"n\", \"i\" }, desc = \"Toggle Global Keymaps\" },\n            [\"<a-b>\"] = { \"toggle_buffer\", mode = { \"n\", \"i\" }, desc = \"Toggle Buffer Keymaps\" },\n          },\n        },\n      },\n    }\n<\n\n\nLAZY                                         *snacks.nvim-picker-sources-lazy*\n\n>vim\n    :lua Snacks.picker.lazy(opts?)\n<\n\nSearch for a lazy.nvim plugin spec\n\n>lua\n    {\n      finder = \"lazy_spec\",\n      pattern = \"'\",\n    }\n<\n\n\nLINES                                       *snacks.nvim-picker-sources-lines*\n\n>vim\n    :lua Snacks.picker.lines(opts?)\n<\n\nSearch lines in the current buffer\n\n>lua\n    ---@class snacks.picker.lines.Config: snacks.picker.Config\n    ---@field buf? number\n    {\n      finder = \"lines\",\n      format = \"lines\",\n      layout = {\n        preview = \"main\",\n        preset = \"ivy\",\n      },\n      jump = { match = true },\n      -- allow any window to be used as the main window\n      main = { current = true },\n      ---@param picker snacks.Picker\n      on_show = function(picker)\n        local cursor = vim.api.nvim_win_get_cursor(picker.main)\n        local info = vim.api.nvim_win_call(picker.main, vim.fn.winsaveview)\n        picker.list:view(cursor[1], info.topline)\n        picker:show_preview()\n      end,\n      sort = { fields = { \"score:desc\", \"idx\" } },\n    }\n<\n\n\nLOCLIST                                   *snacks.nvim-picker-sources-loclist*\n\n>vim\n    :lua Snacks.picker.loclist(opts?)\n<\n\nLoclist\n\n>lua\n    ---@type snacks.picker.qf.Config\n    {\n      finder = \"qf\",\n      format = \"file\",\n      qf_win = 0,\n      main = { current = true },\n    }\n<\n\n\nLSP_CONFIG                             *snacks.nvim-picker-sources-lsp_config*\n\n>vim\n    :lua Snacks.picker.lsp_config(opts?)\n<\n\n>lua\n    ---@class snacks.picker.lsp.config.Config: snacks.picker.Config\n    ---@field installed? boolean only show installed servers\n    ---@field configured? boolean only show configured servers (setup with lspconfig)\n    ---@field attached? boolean|number only show attached servers. When `number`, show only servers attached to that buffer (can be 0)\n    {\n      finder = \"lsp.config#find\",\n      format = \"lsp.config#format\",\n      preview = \"lsp.config#preview\",\n      confirm = \"close\",\n      sort = { fields = { \"score:desc\", \"attached_buf\", \"attached\", \"enabled\", \"installed\", \"name\" } },\n      matcher = { sort_empty = true },\n    }\n<\n\n\nLSP_DECLARATIONS                 *snacks.nvim-picker-sources-lsp_declarations*\n\n>vim\n    :lua Snacks.picker.lsp_declarations(opts?)\n<\n\nLSP declarations\n\n>lua\n    ---@type snacks.picker.lsp.Config\n    {\n      finder = \"lsp_declarations\",\n      format = \"file\",\n      include_current = false,\n      auto_confirm = true,\n      jump = { tagstack = true, reuse_win = true },\n    }\n<\n\n\nLSP_DEFINITIONS                   *snacks.nvim-picker-sources-lsp_definitions*\n\n>vim\n    :lua Snacks.picker.lsp_definitions(opts?)\n<\n\nLSP definitions\n\n>lua\n    ---@type snacks.picker.lsp.Config\n    {\n      finder = \"lsp_definitions\",\n      format = \"file\",\n      include_current = false,\n      auto_confirm = true,\n      jump = { tagstack = true, reuse_win = true },\n    }\n<\n\n\nLSP_IMPLEMENTATIONS           *snacks.nvim-picker-sources-lsp_implementations*\n\n>vim\n    :lua Snacks.picker.lsp_implementations(opts?)\n<\n\nLSP implementations\n\n>lua\n    ---@type snacks.picker.lsp.Config\n    {\n      finder = \"lsp_implementations\",\n      format = \"file\",\n      include_current = false,\n      auto_confirm = true,\n      jump = { tagstack = true, reuse_win = true },\n    }\n<\n\n\nLSP_INCOMING_CALLS             *snacks.nvim-picker-sources-lsp_incoming_calls*\n\n>vim\n    :lua Snacks.picker.lsp_incoming_calls(opts?)\n<\n\nLSP incoming calls\n\n>lua\n    ---@type snacks.picker.lsp.Config\n    {\n      finder = \"lsp_incoming_calls\",\n      format = \"lsp_symbol\",\n      include_current = false,\n      workspace = true, -- this ensures the file is included in the formatter\n      auto_confirm = true,\n      jump = { tagstack = true, reuse_win = true },\n    }\n<\n\n\nLSP_OUTGOING_CALLS             *snacks.nvim-picker-sources-lsp_outgoing_calls*\n\n>vim\n    :lua Snacks.picker.lsp_outgoing_calls(opts?)\n<\n\nLSP outgoing calls\n\n>lua\n    ---@type snacks.picker.lsp.Config\n    {\n      finder = \"lsp_outgoing_calls\",\n      format = \"lsp_symbol\",\n      include_current = false,\n      workspace = true, -- this ensures the file is included in the formatter\n      auto_confirm = true,\n      jump = { tagstack = true, reuse_win = true },\n    }\n<\n\n\nLSP_REFERENCES                     *snacks.nvim-picker-sources-lsp_references*\n\n>vim\n    :lua Snacks.picker.lsp_references(opts?)\n<\n\nLSP references\n\n>lua\n    ---@class snacks.picker.lsp.references.Config: snacks.picker.lsp.Config\n    ---@field include_declaration? boolean default true\n    {\n      finder = \"lsp_references\",\n      format = \"file\",\n      include_declaration = true,\n      include_current = false,\n      auto_confirm = true,\n      jump = { tagstack = true, reuse_win = true },\n    }\n<\n\n\nLSP_SYMBOLS                           *snacks.nvim-picker-sources-lsp_symbols*\n\n>vim\n    :lua Snacks.picker.lsp_symbols(opts?)\n<\n\nLSP document symbols\n\n>lua\n    ---@class snacks.picker.lsp.symbols.Config: snacks.picker.Config\n    ---@field tree? boolean show symbol tree\n    ---@field keep_parents? boolean keep parent symbols when filtering\n    ---@field filter table<string, string[]|boolean>? symbol kind filter\n    ---@field workspace? boolean show workspace symbols\n    {\n      finder = \"lsp_symbols\",\n      format = \"lsp_symbol\",\n      tree = true,\n      filter = {\n        default = {\n          \"Class\",\n          \"Constructor\",\n          \"Enum\",\n          \"Field\",\n          \"Function\",\n          \"Interface\",\n          \"Method\",\n          \"Module\",\n          \"Namespace\",\n          \"Package\",\n          \"Property\",\n          \"Struct\",\n          \"Trait\",\n        },\n        -- set to `true` to include all symbols\n        markdown = true,\n        help = true,\n        -- you can specify a different filter for each filetype\n        lua = {\n          \"Class\",\n          \"Constructor\",\n          \"Enum\",\n          \"Field\",\n          \"Function\",\n          \"Interface\",\n          \"Method\",\n          \"Module\",\n          \"Namespace\",\n          -- \"Package\", -- remove package since luals uses it for control flow structures\n          \"Property\",\n          \"Struct\",\n          \"Trait\",\n        },\n      },\n    }\n<\n\n\nLSP_TYPE_DEFINITIONS         *snacks.nvim-picker-sources-lsp_type_definitions*\n\n>vim\n    :lua Snacks.picker.lsp_type_definitions(opts?)\n<\n\nLSP type definitions\n\n>lua\n    ---@type snacks.picker.lsp.Config\n    {\n      finder = \"lsp_type_definitions\",\n      format = \"file\",\n      include_current = false,\n      auto_confirm = true,\n      jump = { tagstack = true, reuse_win = true },\n    }\n<\n\n\nLSP_WORKSPACE_SYMBOLS       *snacks.nvim-picker-sources-lsp_workspace_symbols*\n\n>vim\n    :lua Snacks.picker.lsp_workspace_symbols(opts?)\n<\n\n>lua\n    ---@type snacks.picker.lsp.symbols.Config\n    vim.tbl_extend(\"force\", {}, M.lsp_symbols, {\n      workspace = true,\n      tree = false,\n      supports_live = true,\n      live = true, -- live by default\n    })\n<\n\n\nMAN                                           *snacks.nvim-picker-sources-man*\n\n>vim\n    :lua Snacks.picker.man(opts?)\n<\n\n>lua\n    {\n      finder = \"system_man\",\n      format = \"man\",\n      preview = \"man\",\n      confirm = function(picker, item, action)\n        ---@cast action snacks.picker.jump.Action\n        picker:close()\n        if item then\n          vim.schedule(function()\n            local cmd = \"Man \" .. item.ref ---@type string\n            if action.cmd == \"vsplit\" then\n              cmd = \"vert \" .. cmd\n            elseif action.cmd == \"tab\" then\n              cmd = \"tab \" .. cmd\n            end\n            vim.cmd(cmd)\n          end)\n        end\n      end,\n    }\n<\n\n\nMARKS                                       *snacks.nvim-picker-sources-marks*\n\n>vim\n    :lua Snacks.picker.marks(opts?)\n<\n\n>lua\n    ---@class snacks.picker.marks.Config: snacks.picker.Config\n    ---@field global? boolean show global marks\n    ---@field local? boolean show buffer marks\n    {\n      finder = \"vim_marks\",\n      format = \"file\",\n      global = true,\n      [\"local\"] = true,\n      win = {\n        input = {\n          keys = {\n            [\"<c-x>\"] = { \"mark_delete\", mode = { \"n\", \"i\" } },\n          },\n        },\n      },\n    }\n<\n\n\nNOTIFICATIONS                       *snacks.nvim-picker-sources-notifications*\n\n>vim\n    :lua Snacks.picker.notifications(opts?)\n<\n\n>lua\n    ---@class snacks.picker.notifications.Config: snacks.picker.Config\n    ---@field filter? snacks.notifier.level|fun(notif: snacks.notifier.Notif): boolean\n    {\n      finder = \"snacks_notifier\",\n      format = \"notification\",\n      preview = \"preview\",\n      formatters = { severity = { level = true } },\n      confirm = \"close\",\n    }\n<\n\n\nPICKER_ACTIONS                     *snacks.nvim-picker-sources-picker_actions*\n\n>vim\n    :lua Snacks.picker.picker_actions(opts?)\n<\n\n>lua\n    {\n      finder = \"meta_actions\",\n      format = \"text\",\n    }\n<\n\n\nPICKER_FORMAT                       *snacks.nvim-picker-sources-picker_format*\n\n>vim\n    :lua Snacks.picker.picker_format(opts?)\n<\n\n>lua\n    {\n      finder = \"meta_format\",\n      format = \"text\",\n    }\n<\n\n\nPICKER_LAYOUTS                     *snacks.nvim-picker-sources-picker_layouts*\n\n>vim\n    :lua Snacks.picker.picker_layouts(opts?)\n<\n\n>lua\n    {\n      finder = \"meta_layouts\",\n      format = \"text\",\n      on_change = function(picker, item)\n        vim.schedule(function()\n          picker:set_layout(item.text)\n        end)\n      end,\n    }\n<\n\n\nPICKER_PREVIEW                     *snacks.nvim-picker-sources-picker_preview*\n\n>vim\n    :lua Snacks.picker.picker_preview(opts?)\n<\n\n>lua\n    {\n      finder = \"meta_preview\",\n      format = \"text\",\n    }\n<\n\n\nPICKERS                                   *snacks.nvim-picker-sources-pickers*\n\n>vim\n    :lua Snacks.picker.pickers(opts?)\n<\n\nList all available sources\n\n>lua\n    {\n      finder = \"meta_pickers\",\n      format = \"text\",\n      confirm = function(picker, item)\n        picker:close()\n        if item then\n          vim.schedule(function()\n            Snacks.picker(item.text)\n          end)\n        end\n      end,\n    }\n<\n\n\nPROJECTS                                 *snacks.nvim-picker-sources-projects*\n\n>vim\n    :lua Snacks.picker.projects(opts?)\n<\n\nOpen recent projects\n\n>lua\n    ---@class snacks.picker.projects.Config: snacks.picker.Config\n    ---@field filter? snacks.picker.filter.Config\n    ---@field dev? string|string[] top-level directories containing multiple projects (sub-folders that contains a root pattern)\n    ---@field projects? string[] list of project directories\n    ---@field patterns? string[] patterns to detect project root directories\n    ---@field recent? boolean include project directories of recent files\n    ---@field max_depth? number maximum depth to search in dev directories (default: 2)\n    {\n      finder = \"recent_projects\",\n      format = \"file\",\n      dev = { \"~/dev\", \"~/projects\" },\n      confirm = \"load_session\",\n      patterns = { \".git\", \"_darcs\", \".hg\", \".bzr\", \".svn\", \"package.json\", \"Makefile\" },\n      recent = true,\n      matcher = {\n        frecency = true, -- use frecency boosting\n        sort_empty = true, -- sort even when the filter is empty\n        cwd_bonus = false,\n      },\n      sort = { fields = { \"score:desc\", \"idx\" } },\n      win = {\n        preview = { minimal = true },\n        input = {\n          keys = {\n            -- every action will always first change the cwd of the current tabpage to the project\n            [\"<c-e>\"] = { { \"tcd\", \"picker_explorer\" }, mode = { \"n\", \"i\" } },\n            [\"<c-f>\"] = { { \"tcd\", \"picker_files\" }, mode = { \"n\", \"i\" } },\n            [\"<c-g>\"] = { { \"tcd\", \"picker_grep\" }, mode = { \"n\", \"i\" } },\n            [\"<c-r>\"] = { { \"tcd\", \"picker_recent\" }, mode = { \"n\", \"i\" }, nowait = true },\n            [\"<c-w>\"] = { { \"tcd\" }, mode = { \"n\", \"i\" } },\n            [\"<c-t>\"] = {\n              function(picker)\n                vim.cmd(\"tabnew\")\n                Snacks.notify(\"New tab opened\")\n                picker:close()\n                Snacks.picker.projects()\n              end,\n              mode = { \"n\", \"i\" },\n            },\n          },\n        },\n      },\n    }\n<\n\n\nQFLIST                                     *snacks.nvim-picker-sources-qflist*\n\n>vim\n    :lua Snacks.picker.qflist(opts?)\n<\n\nQuickfix list\n\n>lua\n    ---@type snacks.picker.qf.Config\n    {\n      finder = \"qf\",\n      format = \"file\",\n    }\n<\n\n\nRECENT                                     *snacks.nvim-picker-sources-recent*\n\n>vim\n    :lua Snacks.picker.recent(opts?)\n<\n\nFind recent files\n\n>lua\n    ---@class snacks.picker.recent.Config: snacks.picker.Config\n    ---@field filter? snacks.picker.filter.Config\n    {\n      finder = \"recent_files\",\n      format = \"file\",\n      filter = {\n        paths = {\n          [vim.fn.stdpath(\"data\")] = false,\n          [vim.fn.stdpath(\"cache\")] = false,\n          [vim.fn.stdpath(\"state\")] = false,\n        },\n      },\n    }\n<\n\n\nREGISTERS                               *snacks.nvim-picker-sources-registers*\n\n>vim\n    :lua Snacks.picker.registers(opts?)\n<\n\nNeovim registers\n\n>lua\n    {\n      finder = \"vim_registers\",\n      main = { current = true },\n      format = \"register\",\n      preview = \"preview\",\n      confirm = { \"copy\", \"close\" },\n    }\n<\n\n\nRESUME                                     *snacks.nvim-picker-sources-resume*\n\n>vim\n    :lua Snacks.picker.resume(opts?)\n<\n\nSpecial picker that resumes the last picker\n\n>lua\n    {}\n<\n\n\nSCRATCH                                   *snacks.nvim-picker-sources-scratch*\n\n>vim\n    :lua Snacks.picker.scratch(opts?)\n<\n\nOpen or create scratch buffers\n\n>lua\n    {\n      finder = \"scratch\",\n      format = \"scratch_format\",\n      confirm = \"scratch_open\",\n      win = {\n        input = {\n          keys = {\n            [\"<c-x>\"] = { \"scratch_delete\", mode = { \"n\", \"i\" } },\n            [\"<c-n>\"] = { \"scratch_new\", mode = { \"n\", \"i\" } },\n          },\n        },\n      },\n    }\n<\n\n\nSEARCH_HISTORY                     *snacks.nvim-picker-sources-search_history*\n\n>vim\n    :lua Snacks.picker.search_history(opts?)\n<\n\nNeovim search history\n\n>lua\n    ---@type snacks.picker.history.Config\n    {\n      finder = \"vim_history\",\n      name = \"search\",\n      format = \"text\",\n      preview = \"none\",\n      main = { current = true },\n      layout = { preset = \"vscode\" },\n      confirm = \"search\",\n      formatters = { text = { ft = \"regex\" } },\n    }\n<\n\n\nSELECT                                     *snacks.nvim-picker-sources-select*\n\n>vim\n    :lua Snacks.picker.select(opts?)\n<\n\nConfig used by `vim.ui.select`. Not meant to be used directly.\n\n>lua\n    ---@class snacks.picker.select.Config: snacks.picker.Config\n    ---@field kinds? table<string, snacks.picker.Config|{}> custom snacks picker configs for specific `vim.ui.select` kinds\n    {\n      items = {}, -- these are set dynamically\n      main = { current = true },\n      layout = { preset = \"select\" },\n    }\n<\n\n\nSMART                                       *snacks.nvim-picker-sources-smart*\n\n>vim\n    :lua Snacks.picker.smart(opts?)\n<\n\n>lua\n    ---@class snacks.picker.smart.Config: snacks.picker.Config\n    ---@field finders? string[] list of finders to use\n    ---@field filter? snacks.picker.filter.Config\n    {\n      multi = { \"buffers\", \"recent\", \"files\" },\n      format = \"file\", -- use `file` format for all sources\n      matcher = {\n        cwd_bonus = true, -- boost cwd matches\n        frecency = true, -- use frecency boosting\n        sort_empty = true, -- sort even when the filter is empty\n      },\n      transform = \"unique_file\",\n    }\n<\n\n\nSPELLING                                 *snacks.nvim-picker-sources-spelling*\n\n>vim\n    :lua Snacks.picker.spelling(opts?)\n<\n\n>lua\n    {\n      finder = \"vim_spelling\",\n      format = \"text\",\n      main = { current = true },\n      layout = { preset = \"vscode\" },\n      confirm = \"item_action\",\n    }\n<\n\n\nTAGS                                         *snacks.nvim-picker-sources-tags*\n\n>vim\n    :lua Snacks.picker.tags(opts?)\n<\n\nSearch tags file\n\n>lua\n    ---@class snacks.picker.tags.Config: snacks.picker.Config\n    {\n      workspace = true, -- search tags in the workspace\n      finder = \"vim_tags\",\n      format = \"lsp_symbol\",\n    }\n<\n\n\nTREESITTER                             *snacks.nvim-picker-sources-treesitter*\n\n>vim\n    :lua Snacks.picker.treesitter(opts?)\n<\n\n>lua\n    ---@class snacks.picker.treesitter.Config: snacks.picker.Config\n    ---@field filter table<string, string[]|boolean>? symbol kind filter\n    ---@field tree? boolean show symbol tree\n    {\n      finder = \"treesitter_symbols\",\n      format = \"lsp_symbol\",\n      tree = true,\n      filter = {\n        default = {\n          \"Class\",\n          \"Enum\",\n          \"Field\",\n          \"Function\",\n          \"Method\",\n          \"Module\",\n          \"Namespace\",\n          \"Struct\",\n          \"Trait\",\n        },\n        -- set to `true` to include all symbols\n        markdown = true,\n        help = true,\n      },\n    }\n<\n\n\nUNDO                                         *snacks.nvim-picker-sources-undo*\n\n>vim\n    :lua Snacks.picker.undo(opts?)\n<\n\n>lua\n    ---@class snacks.picker.undo.Config: snacks.picker.Config\n    ---@field diff? vim.text.diff.Opts\n    {\n      finder = \"vim_undo\",\n      format = \"undo\",\n      preview = \"diff\",\n      confirm = \"item_action\",\n      win = {\n        preview = { wo = { number = false, relativenumber = false, signcolumn = \"no\" } },\n        input = {\n          keys = {\n            [\"<c-y>\"] = { \"yank_add\", mode = { \"n\", \"i\" } },\n            [\"<c-s-y>\"] = { \"yank_del\", mode = { \"n\", \"i\" } },\n          },\n        },\n      },\n      actions = {\n        yank_add = { action = \"yank\", field = \"added_lines\" },\n        yank_del = { action = \"yank\", field = \"removed_lines\" },\n      },\n      icons = { tree = { last = \"┌╴\" } }, -- the tree is upside down\n      diff = {\n        ctxlen = 4,\n        ignore_cr_at_eol = true,\n        ignore_whitespace_change_at_eol = true,\n        indent_heuristic = true,\n      },\n    }\n<\n\n\nZOXIDE                                     *snacks.nvim-picker-sources-zoxide*\n\n>vim\n    :lua Snacks.picker.zoxide(opts?)\n<\n\nOpen a project from zoxide\n\n>lua\n    {\n      finder = \"files_zoxide\",\n      format = \"file\",\n      confirm = \"load_session\",\n      win = {\n        preview = {\n          minimal = true,\n        },\n      },\n    }\n<\n\n\n==============================================================================\n9. Layouts                                        *snacks.nvim-picker-layouts*\n\n\nBOTTOM                                     *snacks.nvim-picker-layouts-bottom*\n\n>lua\n    { preset = \"ivy\", layout = { position = \"bottom\" } }\n<\n\n\nDEFAULT                                   *snacks.nvim-picker-layouts-default*\n\n>lua\n    {\n      layout = {\n        box = \"horizontal\",\n        width = 0.8,\n        min_width = 120,\n        height = 0.8,\n        {\n          box = \"vertical\",\n          border = true,\n          title = \"{title} {live} {flags}\",\n          { win = \"input\", height = 1, border = \"bottom\" },\n          { win = \"list\", border = \"none\" },\n        },\n        { win = \"preview\", title = \"{preview}\", border = true, width = 0.5 },\n      },\n    }\n<\n\n\nDROPDOWN                                 *snacks.nvim-picker-layouts-dropdown*\n\n>lua\n    {\n      layout = {\n        backdrop = false,\n        row = 1,\n        width = 0.4,\n        min_width = 80,\n        height = 0.8,\n        border = \"none\",\n        box = \"vertical\",\n        { win = \"preview\", title = \"{preview}\", height = 0.4, border = true },\n        {\n          box = \"vertical\",\n          border = true,\n          title = \"{title} {live} {flags}\",\n          title_pos = \"center\",\n          { win = \"input\", height = 1, border = \"bottom\" },\n          { win = \"list\", border = \"none\" },\n        },\n      },\n    }\n<\n\n\nIVY                                           *snacks.nvim-picker-layouts-ivy*\n\n>lua\n    {\n      layout = {\n        box = \"vertical\",\n        backdrop = false,\n        row = -1,\n        width = 0,\n        height = 0.4,\n        border = \"top\",\n        title = \" {title} {live} {flags}\",\n        title_pos = \"left\",\n        { win = \"input\", height = 1, border = \"bottom\" },\n        {\n          box = \"horizontal\",\n          { win = \"list\", border = \"none\" },\n          { win = \"preview\", title = \"{preview}\", width = 0.6, border = \"left\" },\n        },\n      },\n    }\n<\n\n\nIVY_SPLIT                               *snacks.nvim-picker-layouts-ivy_split*\n\n>lua\n    {\n      preview = \"main\",\n      layout = {\n        box = \"vertical\",\n        backdrop = false,\n        width = 0,\n        height = 0.4,\n        position = \"bottom\",\n        border = \"top\",\n        title = \" {title} {live} {flags}\",\n        title_pos = \"left\",\n        { win = \"input\", height = 1, border = \"bottom\" },\n        {\n          box = \"horizontal\",\n          { win = \"list\", border = \"none\" },\n          { win = \"preview\", title = \"{preview}\", width = 0.6, border = \"left\" },\n        },\n      },\n    }\n<\n\n\nLEFT                                         *snacks.nvim-picker-layouts-left*\n\n>lua\n    M.sidebar\n<\n\n\nRIGHT                                       *snacks.nvim-picker-layouts-right*\n\n>lua\n    { preset = \"sidebar\", layout = { position = \"right\" } }\n<\n\n\nSELECT                                     *snacks.nvim-picker-layouts-select*\n\n>lua\n    {\n      hidden = { \"preview\" },\n      layout = {\n        backdrop = false,\n        width = 0.5,\n        min_width = 80,\n        max_width = 100,\n        height = 0.4,\n        min_height = 2,\n        box = \"vertical\",\n        border = true,\n        title = \"{title}\",\n        title_pos = \"center\",\n        { win = \"input\", height = 1, border = \"bottom\" },\n        { win = \"list\", border = \"none\" },\n        { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n      },\n    }\n<\n\n\nSIDEBAR                                   *snacks.nvim-picker-layouts-sidebar*\n\n>lua\n    {\n      preview = \"main\",\n      layout = {\n        backdrop = false,\n        width = 40,\n        min_width = 40,\n        height = 0,\n        position = \"left\",\n        border = \"none\",\n        box = \"vertical\",\n        {\n          win = \"input\",\n          height = 1,\n          border = true,\n          title = \"{title} {live} {flags}\",\n          title_pos = \"center\",\n        },\n        { win = \"list\", border = \"none\" },\n        { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n      },\n    }\n<\n\n\nTELESCOPE                               *snacks.nvim-picker-layouts-telescope*\n\n>lua\n    {\n      reverse = true,\n      layout = {\n        box = \"horizontal\",\n        backdrop = false,\n        width = 0.8,\n        height = 0.9,\n        border = \"none\",\n        {\n          box = \"vertical\",\n          { win = \"list\", title = \" Results \", title_pos = \"center\", border = true },\n          { win = \"input\", height = 1, border = true, title = \"{title} {live} {flags}\", title_pos = \"center\" },\n        },\n        {\n          win = \"preview\",\n          title = \"{preview:Preview}\",\n          width = 0.45,\n          border = true,\n          title_pos = \"center\",\n        },\n      },\n    }\n<\n\n\nTOP                                           *snacks.nvim-picker-layouts-top*\n\n>lua\n    { preset = \"ivy\", layout = { position = \"top\" } }\n<\n\n\nVERTICAL                                 *snacks.nvim-picker-layouts-vertical*\n\n>lua\n    {\n      layout = {\n        backdrop = false,\n        width = 0.5,\n        min_width = 80,\n        height = 0.8,\n        min_height = 30,\n        box = \"vertical\",\n        border = true,\n        title = \"{title} {live} {flags}\",\n        title_pos = \"center\",\n        { win = \"input\", height = 1, border = \"bottom\" },\n        { win = \"list\", border = \"none\" },\n        { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n      },\n    }\n<\n\n\nVSCODE                                     *snacks.nvim-picker-layouts-vscode*\n\n>lua\n    {\n      hidden = { \"preview\" },\n      layout = {\n        backdrop = false,\n        row = 1,\n        width = 0.4,\n        min_width = 80,\n        height = 0.4,\n        border = \"none\",\n        box = \"vertical\",\n        { win = \"input\", height = 1, border = true, title = \"{title} {live} {flags}\", title_pos = \"center\" },\n        { win = \"list\", border = \"hpad\" },\n        { win = \"preview\", title = \"{preview}\", border = true },\n      },\n    }\n<\n\n\n==============================================================================\n10. snacks.picker.actions           *snacks.nvim-picker-snacks.picker.actions*\n\n>lua\n    ---@class snacks.picker.actions\n    ---@field [string] snacks.picker.Action.spec\n    local M = {}\n<\n\n\nSNACKS.PICKER.ACTIONS.BUFDELETE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.bufdelete()*\n\n>lua\n    Snacks.picker.actions.bufdelete(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.CANCEL()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cancel()*\n\n>lua\n    Snacks.picker.actions.cancel(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.CD()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cd()*\n\n>lua\n    Snacks.picker.actions.cd(_, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.CLOSE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.close()*\n\n>lua\n    Snacks.picker.actions.close(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.CMD()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cmd()*\n\n>lua\n    Snacks.picker.actions.cmd(picker, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.CYCLE_WIN()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.cycle_win()*\n\n>lua\n    Snacks.picker.actions.cycle_win(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.FOCUS_INPUT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.focus_input()*\n\n>lua\n    Snacks.picker.actions.focus_input(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.FOCUS_LIST()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.focus_list()*\n\n>lua\n    Snacks.picker.actions.focus_list(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.FOCUS_PREVIEW()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.focus_preview()*\n\n>lua\n    Snacks.picker.actions.focus_preview(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.GIT_BRANCH_ADD()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_branch_add()*\n\n>lua\n    Snacks.picker.actions.git_branch_add(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.GIT_BRANCH_DEL()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_branch_del()*\n\n>lua\n    Snacks.picker.actions.git_branch_del(picker, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.GIT_CHECKOUT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_checkout()*\n\n>lua\n    Snacks.picker.actions.git_checkout(picker, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.GIT_RESTORE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_restore()*\n\n>lua\n    Snacks.picker.actions.git_restore(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.GIT_STAGE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_stage()*\n\n>lua\n    Snacks.picker.actions.git_stage(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.GIT_STASH_APPLY()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.git_stash_apply()*\n\n>lua\n    Snacks.picker.actions.git_stash_apply(_, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.HELP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.help()*\n\n>lua\n    Snacks.picker.actions.help(picker, item, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.HISTORY_BACK()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.history_back()*\n\n>lua\n    Snacks.picker.actions.history_back(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.HISTORY_FORWARD()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.history_forward()*\n\n>lua\n    Snacks.picker.actions.history_forward(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.INSERT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.insert()*\n\n>lua\n    Snacks.picker.actions.insert(picker, _, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.INSPECT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.inspect()*\n\n>lua\n    Snacks.picker.actions.inspect(picker, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.ITEM_ACTION()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.item_action()*\n\n>lua\n    Snacks.picker.actions.item_action(picker, item, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.JUMP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.jump()*\n\n>lua\n    Snacks.picker.actions.jump(picker, _, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.LAYOUT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.layout()*\n\n>lua\n    Snacks.picker.actions.layout(picker, _, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.LCD()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.lcd()*\n\n>lua\n    Snacks.picker.actions.lcd(_, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_BOTTOM()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_bottom()*\n\n>lua\n    Snacks.picker.actions.list_bottom(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_DOWN()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_down()*\n\n>lua\n    Snacks.picker.actions.list_down(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_SCROLL_BOTTOM()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_bottom()*\n\n>lua\n    Snacks.picker.actions.list_scroll_bottom(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_SCROLL_CENTER()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_center()*\n\n>lua\n    Snacks.picker.actions.list_scroll_center(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_SCROLL_DOWN()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_down()*\n\n>lua\n    Snacks.picker.actions.list_scroll_down(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_SCROLL_TOP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_top()*\n\n>lua\n    Snacks.picker.actions.list_scroll_top(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_SCROLL_UP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_scroll_up()*\n\n>lua\n    Snacks.picker.actions.list_scroll_up(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_TOP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_top()*\n\n>lua\n    Snacks.picker.actions.list_top(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LIST_UP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.list_up()*\n\n>lua\n    Snacks.picker.actions.list_up(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.LOAD_SESSION()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.load_session()*\n\nTries to load the session, if it fails, it will open the picker.\n\n>lua\n    Snacks.picker.actions.load_session(picker, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.LOCLIST()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.loclist()*\n\nSend selected or all items to the location list.\n\n>lua\n    Snacks.picker.actions.loclist(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.MARK_DELETE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.mark_delete()*\n\n>lua\n    Snacks.picker.actions.mark_delete(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.PASTE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.paste()*\n\n>lua\n    Snacks.picker.actions.paste(picker, item, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.PICK_WIN()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.pick_win()*\n\n>lua\n    Snacks.picker.actions.pick_win(picker, item, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.PICKER()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.picker()*\n\n>lua\n    Snacks.picker.actions.picker(picker, item, action)\n<\n\n\nSNACKS.PICKER.ACTIONS.PICKER_GREP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.picker_grep()*\n\n>lua\n    Snacks.picker.actions.picker_grep(_, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.PREVIEW_SCROLL_DOWN()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_down()*\n\n>lua\n    Snacks.picker.actions.preview_scroll_down(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.PREVIEW_SCROLL_LEFT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_left()*\n\n>lua\n    Snacks.picker.actions.preview_scroll_left(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.PREVIEW_SCROLL_RIGHT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_right()*\n\n>lua\n    Snacks.picker.actions.preview_scroll_right(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.PREVIEW_SCROLL_UP()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.preview_scroll_up()*\n\n>lua\n    Snacks.picker.actions.preview_scroll_up(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.PRINT_CWD()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.print_cwd()*\n\n>lua\n    Snacks.picker.actions.print_cwd(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.PRINT_DIR()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.print_dir()*\n\n>lua\n    Snacks.picker.actions.print_dir(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.PRINT_PATH()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.print_path()*\n\n>lua\n    Snacks.picker.actions.print_path(picker, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.QFLIST()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.qflist()*\n\nSend selected or all items to the quickfix list.\n\n>lua\n    Snacks.picker.actions.qflist(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.QFLIST_ALL()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.qflist_all()*\n\nSend all items to the quickfix list.\n\n>lua\n    Snacks.picker.actions.qflist_all(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.SEARCH()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.search()*\n\n>lua\n    Snacks.picker.actions.search(picker, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.SELECT_ALL()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.select_all()*\n\nSelects all items in the list. Or clears the selection if all items are\nselected.\n\n>lua\n    Snacks.picker.actions.select_all(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.SELECT_AND_NEXT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.select_and_next()*\n\nToggles the selection of the current item, and moves the cursor to the next\nitem.\n\n>lua\n    Snacks.picker.actions.select_and_next(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.SELECT_AND_PREV()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.select_and_prev()*\n\nToggles the selection of the current item, and moves the cursor to the prev\nitem.\n\n>lua\n    Snacks.picker.actions.select_and_prev(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.TCD()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.tcd()*\n\n>lua\n    Snacks.picker.actions.tcd(_, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.TERMINAL()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.terminal()*\n\n>lua\n    Snacks.picker.actions.terminal(_, item)\n<\n\n\nSNACKS.PICKER.ACTIONS.TOGGLE_FOCUS()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_focus()*\n\n>lua\n    Snacks.picker.actions.toggle_focus(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.TOGGLE_HELP_INPUT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_help_input()*\n\n>lua\n    Snacks.picker.actions.toggle_help_input(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.TOGGLE_HELP_LIST()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_help_list()*\n\n>lua\n    Snacks.picker.actions.toggle_help_list(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.TOGGLE_INPUT()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_input()*\n\n>lua\n    Snacks.picker.actions.toggle_input(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.TOGGLE_LIVE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_live()*\n\n>lua\n    Snacks.picker.actions.toggle_live(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.TOGGLE_MAXIMIZE()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_maximize()*\n\n>lua\n    Snacks.picker.actions.toggle_maximize(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.TOGGLE_PREVIEW()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.toggle_preview()*\n\n>lua\n    Snacks.picker.actions.toggle_preview(picker)\n<\n\n\nSNACKS.PICKER.ACTIONS.YANK()*snacks.nvim-picker-snacks.picker.actions-snacks.picker.actions.yank()*\n\n>lua\n    Snacks.picker.actions.yank(picker, item, action)\n<\n\n\n==============================================================================\n11. snacks.picker.core.picker   *snacks.nvim-picker-snacks.picker.core.picker*\n\n>lua\n    ---@class snacks.Picker\n    ---@field id number\n    ---@field opts snacks.picker.Config\n    ---@field init_opts? snacks.picker.Config\n    ---@field finder snacks.picker.Finder\n    ---@field format snacks.picker.format\n    ---@field input snacks.picker.input\n    ---@field layout snacks.layout\n    ---@field resolved_layout snacks.picker.layout.Config\n    ---@field list snacks.picker.list\n    ---@field matcher snacks.picker.Matcher\n    ---@field main number\n    ---@field _main snacks.picker.Main\n    ---@field preview snacks.picker.Preview\n    ---@field shown? boolean\n    ---@field sort snacks.picker.sort\n    ---@field updater uv.uv_timer_t\n    ---@field start_time number\n    ---@field title string\n    ---@field closed? boolean\n    ---@field history snacks.picker.History\n    ---@field visual? snacks.picker.Visual\n    local M = {}\n<\n\n\nSNACKS.PICKER.PICKER.GET()*snacks.nvim-picker-snacks.picker.core.picker-snacks.picker.picker.get()*\n\n>lua\n    ---@param opts? {source?: string, tab?: boolean}\n    Snacks.picker.picker.get(opts)\n<\n\n\nPICKER:ACTION() *snacks.nvim-picker-snacks.picker.core.picker-picker:action()*\n\nExecute the given action(s)\n\n>lua\n    ---@param actions string|string[]\n    picker:action(actions)\n<\n\n\nPICKER:CLOSE()   *snacks.nvim-picker-snacks.picker.core.picker-picker:close()*\n\nClose the picker\n\n>lua\n    picker:close()\n<\n\n\nPICKER:COUNT()   *snacks.nvim-picker-snacks.picker.core.picker-picker:count()*\n\nTotal number of items in the picker\n\n>lua\n    picker:count()\n<\n\n\nPICKER:CURRENT()*snacks.nvim-picker-snacks.picker.core.picker-picker:current()*\n\nGet the current item at the cursor\n\n>lua\n    ---@param opts? {resolve?: boolean} default is `true`\n    picker:current(opts)\n<\n\n\nPICKER:CURRENT_WIN()*snacks.nvim-picker-snacks.picker.core.picker-picker:current_win()*\n\n>lua\n    ---@return string? name, snacks.win? win\n    picker:current_win()\n<\n\n\nPICKER:CWD()       *snacks.nvim-picker-snacks.picker.core.picker-picker:cwd()*\n\n>lua\n    picker:cwd()\n<\n\n\nPICKER:DIR()       *snacks.nvim-picker-snacks.picker.core.picker-picker:dir()*\n\nReturns the directory of the current item or the cwd. When the item is a\ndirectory, return item path, otherwise return the directory of the item.\n\n>lua\n    picker:dir()\n<\n\n\nPICKER:EMPTY()   *snacks.nvim-picker-snacks.picker.core.picker-picker:empty()*\n\nCheck if the picker is empty\n\n>lua\n    picker:empty()\n<\n\n\nPICKER:FILTER() *snacks.nvim-picker-snacks.picker.core.picker-picker:filter()*\n\nGet the active filter\n\n>lua\n    picker:filter()\n<\n\n\nPICKER:FIND()     *snacks.nvim-picker-snacks.picker.core.picker-picker:find()*\n\nCheck if the finder and/or matcher need to run, based on the current pattern\nand search string.\n\n>lua\n    ---@param opts? { on_done?: fun(), refresh?: boolean }\n    picker:find(opts)\n<\n\n\nPICKER:FOCUS()   *snacks.nvim-picker-snacks.picker.core.picker-picker:focus()*\n\nFocuses the given or configured window. Falls back to the first available\nwindow if the window is hidden.\n\n>lua\n    ---@param win? \"input\"|\"list\"|\"preview\"\n    ---@param opts? {show?: boolean} when enable is true, the window will be shown if hidden\n    picker:focus(win, opts)\n<\n\n\nPICKER:HIST()     *snacks.nvim-picker-snacks.picker.core.picker-picker:hist()*\n\nMove the history cursor\n\n>lua\n    ---@param forward? boolean\n    picker:hist(forward)\n<\n\n\nPICKER:IS_ACTIVE()*snacks.nvim-picker-snacks.picker.core.picker-picker:is_active()*\n\nCheck if the finder or matcher is running\n\n>lua\n    picker:is_active()\n<\n\n\nPICKER:IS_FOCUSED()*snacks.nvim-picker-snacks.picker.core.picker-picker:is_focused()*\n\n>lua\n    picker:is_focused()\n<\n\n\nPICKER:ITEMS()   *snacks.nvim-picker-snacks.picker.core.picker-picker:items()*\n\nGet all filtered items in the picker.\n\n>lua\n    picker:items()\n<\n\n\nPICKER:ITER()     *snacks.nvim-picker-snacks.picker.core.picker-picker:iter()*\n\nReturns an iterator over the filtered items in the picker. Items will be in\nsorted order.\n\n>lua\n    ---@return fun():(snacks.picker.Item?, number?)\n    picker:iter()\n<\n\n\nPICKER:NORM()     *snacks.nvim-picker-snacks.picker.core.picker-picker:norm()*\n\nExecute the callback in normal mode. When still in insert mode, stop insert\nmode first, and then`vim.schedule` the callback.\n\n>lua\n    ---@param cb fun()\n    picker:norm(cb)\n<\n\n\nPICKER:ON_CURRENT_TAB()*snacks.nvim-picker-snacks.picker.core.picker-picker:on_current_tab()*\n\n>lua\n    picker:on_current_tab()\n<\n\n\nPICKER:REF()       *snacks.nvim-picker-snacks.picker.core.picker-picker:ref()*\n\n>lua\n    ---@return snacks.Picker.ref\n    picker:ref()\n<\n\n\nPICKER:REFRESH()*snacks.nvim-picker-snacks.picker.core.picker-picker:refresh()*\n\nClears the selection, set the target to the current item, and refresh the\nfinder and matcher.\n\n>lua\n    picker:refresh()\n<\n\n\nPICKER:RESOLVE()*snacks.nvim-picker-snacks.picker.core.picker-picker:resolve()*\n\n>lua\n    ---@param item snacks.picker.Item?\n    picker:resolve(item)\n<\n\n\nPICKER:SELECTED()*snacks.nvim-picker-snacks.picker.core.picker-picker:selected()*\n\nGet the selected items. If `fallback=true` and there is no selection, return\nthe current item.\n\n>lua\n    ---@param opts? {fallback?: boolean} default is `false`\n    ---@return snacks.picker.Item[]\n    picker:selected(opts)\n<\n\n\nPICKER:SET_CWD()*snacks.nvim-picker-snacks.picker.core.picker-picker:set_cwd()*\n\n>lua\n    picker:set_cwd(cwd)\n<\n\n\nPICKER:SET_LAYOUT()*snacks.nvim-picker-snacks.picker.core.picker-picker:set_layout()*\n\nSet the picker layout. Can be either the name of a preset layout or a custom\nlayout configuration.\n\n>lua\n    ---@param layout? string|snacks.picker.layout.Config\n    picker:set_layout(layout)\n<\n\n\nPICKER:SHOW_PREVIEW()*snacks.nvim-picker-snacks.picker.core.picker-picker:show_preview()*\n\nShow the preview. Show instantly when no item is yet in the preview, otherwise\nthrottle the preview.\n\n>lua\n    picker:show_preview()\n<\n\n\nPICKER:TOGGLE() *snacks.nvim-picker-snacks.picker.core.picker-picker:toggle()*\n\nToggle the given window and optionally focus\n\n>lua\n    ---@param win \"input\"|\"list\"|\"preview\"\n    ---@param opts? {enable?: boolean, focus?: boolean|string}\n    picker:toggle(win, opts)\n<\n\n\nPICKER:WORD()     *snacks.nvim-picker-snacks.picker.core.picker-picker:word()*\n\nGet the word under the cursor or the current visual selection\n\n>lua\n    picker:word()\n<\n\n==============================================================================\n12. Links                                           *snacks.nvim-picker-links*\n\n1. *image*: https://github.com/user-attachments/assets/b454fc3c-6613-4aa4-9296-f57a8b02bf6d\n2. *image*: https://github.com/user-attachments/assets/3203aec4-7d75-4bca-b3d5-18d931277e4e\n3. *image*: https://github.com/user-attachments/assets/e09d25f8-8559-441c-a0f7-576d2aa57097\n4. *image*: https://github.com/user-attachments/assets/291dcf63-0c1d-4e9a-97cb-dd5503660e6f\n5. *image*: https://github.com/user-attachments/assets/1aba5737-a650-4a00-94f8-033b7d8d21ba\n6. *image*: https://github.com/user-attachments/assets/976e0ed8-eb80-43e1-93ac-4683136c0a3c\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-profiler.txt",
    "content": "*snacks-profiler*                           snacks profiler docs\n\n==============================================================================\nTable of Contents                     *snacks.nvim-profiler-table-of-contents*\n\n1. Features                                    |snacks.nvim-profiler-features|\n2. Why?                                            |snacks.nvim-profiler-why?|\n3. Usage                                          |snacks.nvim-profiler-usage|\n  - Caveats                               |snacks.nvim-profiler-usage-caveats|\n  - Recommended Setup           |snacks.nvim-profiler-usage-recommended-setup|\n  - Profiling Neovim Startup|snacks.nvim-profiler-usage-profiling-neovim-startup|\n  - Filtering                           |snacks.nvim-profiler-usage-filtering|\n4. Setup                                          |snacks.nvim-profiler-setup|\n5. Config                                        |snacks.nvim-profiler-config|\n6. Types                                          |snacks.nvim-profiler-types|\n  - Traces                                 |snacks.nvim-profiler-types-traces|\n  - Pick: grouping, filtering and sorting|snacks.nvim-profiler-types-pick:-grouping,-filtering-and-sorting|\n  - UI                                         |snacks.nvim-profiler-types-ui|\n  - Other                                   |snacks.nvim-profiler-types-other|\n7. Module                                        |snacks.nvim-profiler-module|\n  - Snacks.profiler.find()|snacks.nvim-profiler-module-snacks.profiler.find()|\n  - Snacks.profiler.highlight()|snacks.nvim-profiler-module-snacks.profiler.highlight()|\n  - Snacks.profiler.pick()|snacks.nvim-profiler-module-snacks.profiler.pick()|\n  - Snacks.profiler.running()|snacks.nvim-profiler-module-snacks.profiler.running()|\n  - Snacks.profiler.scratch()|snacks.nvim-profiler-module-snacks.profiler.scratch()|\n  - Snacks.profiler.start()|snacks.nvim-profiler-module-snacks.profiler.start()|\n  - Snacks.profiler.startup()|snacks.nvim-profiler-module-snacks.profiler.startup()|\n  - Snacks.profiler.status()|snacks.nvim-profiler-module-snacks.profiler.status()|\n  - Snacks.profiler.stop()|snacks.nvim-profiler-module-snacks.profiler.stop()|\n  - Snacks.profiler.toggle()|snacks.nvim-profiler-module-snacks.profiler.toggle()|\n8. Links                                          |snacks.nvim-profiler-links|\nA low overhead Lua profiler for Neovim.\n\n\n==============================================================================\n1. Features                                    *snacks.nvim-profiler-features*\n\n- low overhead **instrumentation**\n- captures a function’s **def**inition and **ref**erence (_caller_) locations\n- profiling of **autocmds**\n- profiling of **require**d modules\n- buffer **highlighting** of functions and calls\n- lots of different ways to **filter** and **group** traces\n- show traces with:\n    - fzf-lua <https://github.com/ibhagwan/fzf-lua>\n    - telescope.nvim <https://github.com/nvim-telescope/telescope.nvim>\n    - trouble.nvim <https://github.com/folke/trouble.nvim>\n\n\n==============================================================================\n2. Why?                                            *snacks.nvim-profiler-why?*\n\nBefore the snacks profiler, I used to use a combination of my own profiler(s),\n**lazy.nvim**’s internal profiler, profile.nvim\n<https://github.com/stevearc/profile.nvim> and perfanno.nvim\n<https://github.com/t-troebst/perfanno.nvim>.\n\nThey all have their strengths and weaknesses:\n\n- **lazy.nvim**’s profiler is great for structured traces, but needed a lot of\n    manual work to get the traces I wanted.\n- **profile.nvim** does proper instrumentation, but was lacking in the UI department.\n- **perfanno.nvim** has a great UI, but uses `jit.profile` which is not as\n    detailed as instrumentation.\n\nThe snacks profiler tries to combine the best of all worlds.\n\n\n==============================================================================\n3. Usage                                          *snacks.nvim-profiler-usage*\n\nThe easiest way to use the profiler is to toggle it with the suggested\nkeybindings.\n\nWhen the profiler stops, it will show a picker using the `on_stop` preset.\n\nTo quickly change picker options, you can use the `Snacks.profiler.scratch()`\nscratch buffer.\n\n\nCAVEATS                                   *snacks.nvim-profiler-usage-caveats*\n\n- your Neovim session might slow down when profiling\n- due to the overhead of instrumentation, fast functions that are called\n    often, might skew the results. Best to add those to the `opts.filter_fn` config.\n- by default, only captures functions defined on lua modules.\n    If you want to profile others, add them to `opts.globals`\n- the profiler is not perfect and might not capture all calls\n- the profiler might not work well with some plugins\n- it can only profile `autocmds` created when the profiler is running.\n- only `autocmds` with a lua function callback can be profiled\n- functions that `resume` or `yield` won’t be captured correctly\n- functions that do blocking calls like `vim.fn.getchar` will work,\n    but the time will include the time spent waiting for the blocking call\n\n\nRECOMMENDED SETUP               *snacks.nvim-profiler-usage-recommended-setup*\n\n>lua\n    {\n      {\n        \"folke/snacks.nvim\",\n        opts = function()\n          -- Toggle the profiler\n          Snacks.toggle.profiler():map(\"<leader>pp\")\n          -- Toggle the profiler highlights\n          Snacks.toggle.profiler_highlights():map(\"<leader>ph\")\n        end,\n        keys = {\n          { \"<leader>ps\", function() Snacks.profiler.scratch() end, desc = \"Profiler Scratch Bufer\" },\n        }\n      },\n      -- optional lualine component to show captured events\n      -- when the profiler is running\n      {\n        \"nvim-lualine/lualine.nvim\",\n        opts = function(_, opts)\n          table.insert(opts.sections.lualine_x, Snacks.profiler.status())\n        end,\n      },\n    }\n<\n\n\nPROFILING NEOVIM STARTUP *snacks.nvim-profiler-usage-profiling-neovim-startup*\n\nIn order to profile Neovim’s startup, you need to make sure `snacks.nvim` is\ninstalled and loaded **before** doing anything else. So also before loading\nyour plugin manager.\n\nYou can add something like the below to the top of your `init.lua`.\n\nThen you can profile your Neovim session, with `PROF=1 nvim`.\n\n>lua\n    if vim.env.PROF then\n      -- example for lazy.nvim\n      -- change this to the correct path for your plugin manager\n      local snacks = vim.fn.stdpath(\"data\") .. \"/lazy/snacks.nvim\"\n      vim.opt.rtp:append(snacks)\n      require(\"snacks.profiler\").startup({\n        startup = {\n          event = \"VimEnter\", -- stop profiler on this event. Defaults to `VimEnter`\n          -- event = \"UIEnter\",\n          -- event = \"VeryLazy\",\n        },\n      })\n    end\n<\n\n\nFILTERING                               *snacks.nvim-profiler-usage-filtering*\n\nFor the full definition, see the `snacks.profiler.Filter` type.\n\nEach field can be a string or a boolean.\n\nWhen a field is a string, it will match the exact value, unless it starts with\n`^` in which case it will match the pattern.\n\nWhen any of the `def`/`ref` fields are `true`, the filter matches the current\nlocation of the cursor.\n\nFor example, `{ref_file = true}` will match all traces calling something, in\nthe current file.\n\nAll other fields equal to `true` will match if the trace has a value for that\nfield.\n\n\n==============================================================================\n4. Setup                                          *snacks.nvim-profiler-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        profiler = {\n          -- your profiler configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n5. Config                                        *snacks.nvim-profiler-config*\n\n>lua\n    ---@class snacks.profiler.Config\n    {\n      autocmds = true,\n      runtime = vim.env.VIMRUNTIME, ---@type string\n      -- thresholds for buttons to be shown as info, warn or error\n      -- value is a tuple of [warn, error]\n      thresholds = {\n        time = { 2, 10 },\n        pct = { 10, 20 },\n        count = { 10, 100 },\n      },\n      on_stop = {\n        highlights = true, -- highlight entries after stopping the profiler\n        pick = true, -- show a picker after stopping the profiler (uses the `on_stop` preset)\n      },\n      ---@type snacks.profiler.Highlights\n      highlights = {\n        min_time = 0, -- only highlight entries with time > min_time (in ms)\n        max_shade = 20, -- time in ms for the darkest shade\n        badges = { \"time\", \"pct\", \"count\", \"trace\" },\n        align = 80,\n      },\n      pick = {\n        picker = \"snacks\", ---@type snacks.profiler.Picker\n        ---@type snacks.profiler.Badge.type[]\n        badges = { \"time\", \"count\", \"name\" },\n        ---@type snacks.profiler.Highlights\n        preview = {\n          badges = { \"time\", \"pct\", \"count\" },\n          align = \"right\",\n        },\n      },\n      startup = {\n        event = \"VimEnter\", -- stop profiler on this event. Defaults to `VimEnter`\n        after = true, -- stop the profiler **after** the event. When false it stops **at** the event\n        pattern = nil, -- pattern to match for the autocmd\n        pick = true, -- show a picker after starting the profiler (uses the `startup` preset)\n      },\n      ---@type table<string, snacks.profiler.Pick|fun():snacks.profiler.Pick?>\n      presets = {\n        startup = { min_time = 1, sort = false },\n        on_stop = {},\n        filter_by_plugin = function()\n          return { filter = { def_plugin = vim.fn.input(\"Filter by plugin: \") } }\n        end,\n      },\n      ---@type string[]\n      globals = {\n        -- \"vim\",\n        -- \"vim.api\",\n        -- \"vim.keymap\",\n        -- \"Snacks.dashboard.Dashboard\",\n      },\n      -- filter modules by pattern.\n      -- longest patterns are matched first\n      filter_mod = {\n        default = true, -- default value for unmatched patterns\n        [\"^vim%.\"] = false,\n        [\"mason-core.functional\"] = false,\n        [\"mason-core.functional.data\"] = false,\n        [\"mason-core.optional\"] = false,\n        [\"which-key.state\"] = false,\n      },\n      filter_fn = {\n        default = true,\n        [\"^.*%._[^%.]*$\"] = false,\n        [\"trouble.filter.is\"] = false,\n        [\"trouble.item.__index\"] = false,\n        [\"which-key.node.__index\"] = false,\n        [\"smear_cursor.draw.wo\"] = false,\n        [\"^ibl%.utils%.\"] = false,\n      },\n      icons = {\n        time    = \" \",\n        pct     = \" \",\n        count   = \" \",\n        require = \"󰋺 \",\n        modname = \"󰆼 \",\n        plugin  = \" \",\n        autocmd = \"⚡\",\n        file    = \" \",\n        fn      = \"󰊕 \",\n        status  = \"󰈸 \",\n      },\n    }\n<\n\n\n==============================================================================\n6. Types                                          *snacks.nvim-profiler-types*\n\n\nTRACES                                     *snacks.nvim-profiler-types-traces*\n\n>lua\n    ---@class snacks.profiler.Trace\n    ---@field name string fully qualified name of the function\n    ---@field time number time in nanoseconds\n    ---@field depth number stack depth\n    ---@field [number] snacks.profiler.Trace child traces\n    ---@field fname string function name\n    ---@field fn function function reference\n    ---@field modname? string module name\n    ---@field require? string special case for require\n    ---@field autocmd? string special case for autocmd\n    ---@field count? number number of calls\n    ---@field def? snacks.profiler.Loc location of the definition\n    ---@field ref? snacks.profiler.Loc location of the reference (caller)\n    ---@field loc? snacks.profiler.Loc normalized location\n<\n\n>lua\n    ---@class snacks.profiler.Loc\n    ---@field file string path to the file\n    ---@field line number line number\n    ---@field loc? string normalized location\n    ---@field modname? string module name\n    ---@field plugin? string plugin name\n<\n\n\nPICK: GROUPING, FILTERING AND SORTING*snacks.nvim-profiler-types-pick:-grouping,-filtering-and-sorting*\n\n>lua\n    ---@class snacks.profiler.Find\n    ---@field structure? boolean show traces as a tree or flat list\n    ---@field sort? \"time\"|\"count\"|false sort by time or count, or keep original order\n    ---@field loc? \"def\"|\"ref\" what location to show in the preview\n    ---@field group? boolean|snacks.profiler.Field group traces by field\n    ---@field filter? snacks.profiler.Filter filter traces by field(s)\n    ---@field min_time? number only show grouped traces with `time >= min_time`\n<\n\n>lua\n    ---@class snacks.profiler.Pick: snacks.profiler.Find\n    ---@field picker? snacks.profiler.Picker\n<\n\n>lua\n    ---@alias snacks.profiler.Picker \"snacks\"|\"trouble\"\n    ---@alias snacks.profiler.Pick.spec snacks.profiler.Pick|{preset?:string}|fun():snacks.profiler.Pick\n<\n\n>lua\n    ---@alias snacks.profiler.Field\n    ---| \"name\" fully qualified name of the function\n    ---| \"def\" definition\n    ---| \"ref\" reference (caller)\n    ---| \"require\" require\n    ---| \"autocmd\" autocmd\n    ---| \"modname\" module name of the called function\n    ---| \"def_file\" file of the definition\n    ---| \"def_modname\" module name of the definition\n    ---| \"def_plugin\" plugin that defines the function\n    ---| \"ref_file\" file of the reference\n    ---| \"ref_modname\" module name of the reference\n    ---| \"ref_plugin\" plugin that references the function\n<\n\n>lua\n    ---@class snacks.profiler.Filter\n    ---@field name? string|boolean fully qualified name of the function\n    ---@field def? string|boolean location of the definition\n    ---@field ref? string|boolean location of the reference (caller)\n    ---@field require? string|boolean special case for require\n    ---@field autocmd? string|boolean special case for autocmd\n    ---@field modname? string|boolean module name\n    ---@field def_file? string|boolean file of the definition\n    ---@field def_modname? string|boolean module name of the definition\n    ---@field def_plugin? string|boolean plugin that defines the function\n    ---@field ref_file? string|boolean file of the reference\n    ---@field ref_modname? string|boolean module name of the reference\n    ---@field ref_plugin? string|boolean plugin that references the function\n<\n\n\nUI                                             *snacks.nvim-profiler-types-ui*\n\n>lua\n    ---@alias snacks.profiler.Badge {icon:string, text:string, padding?:boolean, level?:string}\n    ---@alias snacks.profiler.Badge.type \"time\"|\"pct\"|\"count\"|\"name\"|\"trace\"\n<\n\n>lua\n    ---@class snacks.profiler.Highlights\n    ---@field min_time? number only highlight entries with time >= min_time\n    ---@field max_shade? number -- time in ms for the darkest shade\n    ---@field badges? snacks.profiler.Badge.type[] badges to show\n    ---@field align? \"right\"|\"left\"|number align the badges right, left or at a specific column\n<\n\n\nOTHER                                       *snacks.nvim-profiler-types-other*\n\n>lua\n    ---@class snacks.profiler.Startup\n    ---@field event? string\n    ---@field pattern? string|string[] pattern to match for the autocmd\n<\n\n>lua\n    ---@alias snacks.profiler.GroupFn fun(entry:snacks.profiler.Trace):{key:string, name?:string}?\n<\n\n\n==============================================================================\n7. Module                                        *snacks.nvim-profiler-module*\n\n>lua\n    ---@class snacks.profiler\n    ---@field core snacks.profiler.core\n    ---@field loc snacks.profiler.loc\n    ---@field tracer snacks.profiler.tracer\n    ---@field ui snacks.profiler.ui\n    ---@field picker snacks.profiler.picker\n    Snacks.profiler = {}\n<\n\n\n`Snacks.profiler.find()`                              *Snacks.profiler.find()*\n\nGroup and filter traces\n\n>lua\n    ---@param opts snacks.profiler.Find\n    Snacks.profiler.find(opts)\n<\n\n\n`Snacks.profiler.highlight()`                          *Snacks.profiler.highlight()*\n\nToggle the profiler highlights\n\n>lua\n    ---@param enable? boolean\n    Snacks.profiler.highlight(enable)\n<\n\n\n`Snacks.profiler.pick()`                              *Snacks.profiler.pick()*\n\nGroup and filter traces and open a picker\n\n>lua\n    ---@param opts? snacks.profiler.Pick.spec\n    Snacks.profiler.pick(opts)\n<\n\n\n`Snacks.profiler.running()`                          *Snacks.profiler.running()*\n\nCheck if the profiler is running\n\n>lua\n    Snacks.profiler.running()\n<\n\n\n`Snacks.profiler.scratch()`                          *Snacks.profiler.scratch()*\n\nOpen a scratch buffer with the profiler picker options\n\n>lua\n    Snacks.profiler.scratch()\n<\n\n\n`Snacks.profiler.start()`                            *Snacks.profiler.start()*\n\nStart the profiler\n\n>lua\n    ---@param opts? snacks.profiler.Config\n    Snacks.profiler.start(opts)\n<\n\n\n`Snacks.profiler.startup()`                          *Snacks.profiler.startup()*\n\nStart the profiler on startup, and stop it after the event has been triggered.\n\n>lua\n    ---@param opts snacks.profiler.Config\n    Snacks.profiler.startup(opts)\n<\n\n\n`Snacks.profiler.status()`                          *Snacks.profiler.status()*\n\nStatusline component\n\n>lua\n    Snacks.profiler.status()\n<\n\n\n`Snacks.profiler.stop()`                              *Snacks.profiler.stop()*\n\nStop the profiler\n\n>lua\n    ---@param opts? {highlights?:boolean, pick?:snacks.profiler.Pick.spec}\n    Snacks.profiler.stop(opts)\n<\n\n\n`Snacks.profiler.toggle()`                          *Snacks.profiler.toggle()*\n\nToggle the profiler\n\n>lua\n    Snacks.profiler.toggle()\n<\n\n==============================================================================\n8. Links                                          *snacks.nvim-profiler-links*\n\n1. *image*: https://github.com/user-attachments/assets/cebb1308-077b-4f20-bee3-28644fb121b8\n2. *image*: https://github.com/user-attachments/assets/4ee557c4-a290-4a52-b5c9-64e325bf1094\n3. *image*: https://github.com/user-attachments/assets/ec03e440-6719-4463-a649-a8626dcfe2ec\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-quickfile.txt",
    "content": "*snacks-quickfile*                         snacks quickfile docs\n\n==============================================================================\nTable of Contents                    *snacks.nvim-quickfile-table-of-contents*\n\n1. Setup                                         |snacks.nvim-quickfile-setup|\n2. Config                                       |snacks.nvim-quickfile-config|\nWhen doing `nvim somefile.txt`, it will render the file as quickly as possible,\nbefore loading your plugins.\n\n\n==============================================================================\n1. Setup                                         *snacks.nvim-quickfile-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        quickfile = {\n          -- your quickfile configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                       *snacks.nvim-quickfile-config*\n\n>lua\n    ---@class snacks.quickfile.Config\n    {\n      -- any treesitter langs to exclude\n      exclude = { \"latex\" },\n    }\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-rename.txt",
    "content": "*snacks-rename*                               snacks rename docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-rename-table-of-contents*\n\n1. Usage                                            |snacks.nvim-rename-usage|\n2. mini.files                                  |snacks.nvim-rename-mini.files|\n3. oil.nvim                                      |snacks.nvim-rename-oil.nvim|\n4. fyler.nvim                                  |snacks.nvim-rename-fyler.nvim|\n5. neo-tree.nvim                            |snacks.nvim-rename-neo-tree.nvim|\n6. nvim-tree                                    |snacks.nvim-rename-nvim-tree|\n7. netrw (builtin file explorer)|snacks.nvim-rename-netrw-(builtin-file-explorer)|\n8. Module                                          |snacks.nvim-rename-module|\n  - Snacks.rename.on_rename_file()|snacks.nvim-rename-module-snacks.rename.on_rename_file()|\n  - Snacks.rename.rename_file()|snacks.nvim-rename-module-snacks.rename.rename_file()|\nLSP-integrated file renaming with support for plugins like neo-tree.nvim\n<https://github.com/nvim-neo-tree/neo-tree.nvim> and mini.files\n<https://github.com/nvim-mini/mini.files>.\n\n\n==============================================================================\n1. Usage                                            *snacks.nvim-rename-usage*\n\n\n==============================================================================\n2. mini.files                                  *snacks.nvim-rename-mini.files*\n\n>lua\n    vim.api.nvim_create_autocmd(\"User\", {\n      pattern = \"MiniFilesActionRename\",\n      callback = function(event)\n        Snacks.rename.on_rename_file(event.data.from, event.data.to)\n      end,\n    })\n<\n\n\n==============================================================================\n3. oil.nvim                                      *snacks.nvim-rename-oil.nvim*\n\n>lua\n    vim.api.nvim_create_autocmd(\"User\", {\n      pattern = \"OilActionsPost\",\n      callback = function(event)\n          if event.data.actions[1].type == \"move\" then\n              Snacks.rename.on_rename_file(event.data.actions[1].src_url, event.data.actions[1].dest_url)\n          end\n      end,\n    })\n<\n\n\n==============================================================================\n4. fyler.nvim                                  *snacks.nvim-rename-fyler.nvim*\n\n>lua\n    return {\n      \"A7Lavinraj/fyler.nvim\",\n      dependencies = { \"echasnovski/mini.icons\" },\n      opts = {\n        hooks = {\n          on_rename = function(src_path, destination_path)\n            Snacks.rename.on_rename_file(src_path, destination_path)\n          end,\n        },\n      },\n    }\n<\n\n\n==============================================================================\n5. neo-tree.nvim                            *snacks.nvim-rename-neo-tree.nvim*\n\n>lua\n    {\n      \"nvim-neo-tree/neo-tree.nvim\",\n      opts = function(_, opts)\n        local function on_move(data)\n          Snacks.rename.on_rename_file(data.source, data.destination)\n        end\n        local events = require(\"neo-tree.events\")\n        opts.event_handlers = opts.event_handlers or {}\n        vim.list_extend(opts.event_handlers, {\n          { event = events.FILE_MOVED, handler = on_move },\n          { event = events.FILE_RENAMED, handler = on_move },\n        })\n      end,\n    }\n<\n\n\n==============================================================================\n6. nvim-tree                                    *snacks.nvim-rename-nvim-tree*\n\n>lua\n    local prev = { new_name = \"\", old_name = \"\" } -- Prevents duplicate events\n    vim.api.nvim_create_autocmd(\"User\", {\n      pattern = \"NvimTreeSetup\",\n      callback = function()\n        local events = require(\"nvim-tree.api\").events\n        events.subscribe(events.Event.NodeRenamed, function(data)\n          if prev.new_name ~= data.new_name or prev.old_name ~= data.old_name then\n            data = data\n            Snacks.rename.on_rename_file(data.old_name, data.new_name)\n          end\n        end)\n      end,\n    })\n<\n\n\n==============================================================================\n7. netrw (builtin file explorer)*snacks.nvim-rename-netrw-(builtin-file-explorer)*\n\n>lua\n    vim.api.nvim_create_autocmd({ 'FileType' }, {\n      pattern = { 'netrw' },\n      group = vim.api.nvim_create_augroup('NetrwOnRename', { clear = true }),\n      callback = function()\n        vim.keymap.set(\"n\", \"R\", function()\n          local original_file_path = vim.b.netrw_curdir .. '/' .. vim.fn[\"netrw#Call\"](\"NetrwGetWord\")\n    \n          vim.ui.input({ prompt = 'Move/rename to:', default = original_file_path }, function(target_file_path)\n            if target_file_path and target_file_path ~= \"\" then\n              local file_exists = vim.uv.fs_access(target_file_path, \"W\")\n    \n              if not file_exists then\n                vim.uv.fs_rename(original_file_path, target_file_path)\n    \n                Snacks.rename.on_rename_file(original_file_path, target_file_path)\n              else\n                vim.notify(\"File '\" .. target_file_path .. \"' already exists! Skipping...\", vim.log.levels.ERROR)\n              end\n    \n              -- Refresh netrw\n              vim.cmd(':Ex ' .. vim.b.netrw_curdir)\n            end\n          end)\n        end, { remap = true, buffer = true })\n      end\n    })\n<\n\n\n==============================================================================\n8. Module                                          *snacks.nvim-rename-module*\n\n\n`Snacks.rename.on_rename_file()`                        *Snacks.rename.on_rename_file()*\n\nLets LSP clients know that a file has been renamed\n\n>lua\n    ---@param from string\n    ---@param to string\n    ---@param rename? fun()\n    Snacks.rename.on_rename_file(from, to, rename)\n<\n\n\n`Snacks.rename.rename_file()`                        *Snacks.rename.rename_file()*\n\nRenames the provided file, or the current buffer’s file. Prompt for the new\nfilename if `to` is not provided. do the rename, and trigger LSP handlers\n\n>lua\n    ---@param opts? {from?: string, to?:string, on_rename?: fun(to:string, from:string, ok:boolean)}\n    Snacks.rename.rename_file(opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-scope.txt",
    "content": "*snacks-scope*                                 snacks scope docs\n\n==============================================================================\nTable of Contents                        *snacks.nvim-scope-table-of-contents*\n\n1. Setup                                             |snacks.nvim-scope-setup|\n2. Config                                           |snacks.nvim-scope-config|\n3. Types                                             |snacks.nvim-scope-types|\n4. Module                                           |snacks.nvim-scope-module|\n  - Snacks.scope.attach()     |snacks.nvim-scope-module-snacks.scope.attach()|\n  - Snacks.scope.get()           |snacks.nvim-scope-module-snacks.scope.get()|\n  - Snacks.scope.jump()         |snacks.nvim-scope-module-snacks.scope.jump()|\n  - Snacks.scope.textobject()|snacks.nvim-scope-module-snacks.scope.textobject()|\nScope detection based on treesitter or indent.\n\nThe indent-based algorithm is similar to what is used in mini.indentscope\n<https://github.com/nvim-mini/mini.indentscope>.\n\n\n==============================================================================\n1. Setup                                             *snacks.nvim-scope-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        scope = {\n          -- your scope configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                           *snacks.nvim-scope-config*\n\n>lua\n    ---@class snacks.scope.Config\n    ---@field max_size? number\n    ---@field enabled? boolean\n    {\n      -- absolute minimum size of the scope.\n      -- can be less if the scope is a top-level single line scope\n      min_size = 2,\n      -- try to expand the scope to this size\n      max_size = nil,\n      cursor = true, -- when true, the column of the cursor is used to determine the scope\n      edge = true, -- include the edge of the scope (typically the line above and below with smaller indent)\n      siblings = false, -- expand single line scopes with single line siblings\n      -- what buffers to attach to\n      filter = function(buf)\n        return vim.bo[buf].buftype == \"\" and vim.b[buf].snacks_scope ~= false and vim.g.snacks_scope ~= false\n      end,\n      -- debounce scope detection in ms\n      debounce = 30,\n      treesitter = {\n        -- detect scope based on treesitter.\n        -- falls back to indent based detection if not available\n        enabled = true,\n        injections = true, -- include language injections when detecting scope (useful for languages like `vue`)\n        ---@type string[]|{enabled?:boolean}\n        blocks = {\n          enabled = false, -- enable to use the following blocks\n          \"function_declaration\",\n          \"function_definition\",\n          \"method_declaration\",\n          \"method_definition\",\n          \"class_declaration\",\n          \"class_definition\",\n          \"do_statement\",\n          \"while_statement\",\n          \"repeat_statement\",\n          \"if_statement\",\n          \"for_statement\",\n        },\n        -- these treesitter fields will be considered as blocks\n        field_blocks = {\n          \"local_declaration\",\n        },\n      },\n      -- These keymaps will only be set if the `scope` plugin is enabled.\n      -- Alternatively, you can set them manually in your config,\n      -- using the `Snacks.scope.textobject` and `Snacks.scope.jump` functions.\n      keys = {\n        ---@type table<string, snacks.scope.TextObject|{desc?:string}|false>\n        textobject = {\n          ii = {\n            min_size = 2, -- minimum size of the scope\n            edge = false, -- inner scope\n            cursor = false,\n            treesitter = { blocks = { enabled = false } },\n            desc = \"inner scope\",\n          },\n          ai = {\n            cursor = false,\n            min_size = 2, -- minimum size of the scope\n            treesitter = { blocks = { enabled = false } },\n            desc = \"full scope\",\n          },\n        },\n        ---@type table<string, snacks.scope.Jump|{desc?:string}|false>\n        jump = {\n          [\"[i\"] = {\n            min_size = 1, -- allow single line scopes\n            bottom = false,\n            cursor = false,\n            edge = true,\n            treesitter = { blocks = { enabled = false } },\n            desc = \"jump to top edge of scope\",\n          },\n          [\"]i\"] = {\n            min_size = 1, -- allow single line scopes\n            bottom = true,\n            cursor = false,\n            edge = true,\n            treesitter = { blocks = { enabled = false } },\n            desc = \"jump to bottom edge of scope\",\n          },\n        },\n      },\n    }\n<\n\n\n==============================================================================\n3. Types                                             *snacks.nvim-scope-types*\n\n>lua\n    ---@class snacks.scope.Opts: snacks.scope.Config,{}\n    ---@field buf? number\n    ---@field pos? {[1]:number, [2]:number} -- (1,0) indexed\n    ---@field end_pos? {[1]:number, [2]:number} -- (1,0) indexed\n    ---@field async? boolean run scope detection asynchronously (defaults to true)\n<\n\n>lua\n    ---@class snacks.scope.TextObject: snacks.scope.Opts\n    ---@field linewise? boolean if nil, use visual mode. Defaults to `false` when not in visual mode\n    ---@field notify? boolean show a notification when no scope is found (defaults to true)\n<\n\n>lua\n    ---@class snacks.scope.Jump: snacks.scope.Opts\n    ---@field bottom? boolean if true, jump to the bottom of the scope, otherwise to the top\n    ---@field notify? boolean show a notification when no scope is found (defaults to true)\n<\n\n>lua\n    ---@alias snacks.scope.Attach.cb fun(win: number, buf: number, scope:snacks.scope.Scope?, prev:snacks.scope.Scope?)\n<\n\n>lua\n    ---@alias snacks.scope.scope {buf: number, from: number, to: number, indent?: number}\n<\n\n\n==============================================================================\n4. Module                                           *snacks.nvim-scope-module*\n\n\n`Snacks.scope.attach()`                                *Snacks.scope.attach()*\n\nAttach a scope listener\n\n>lua\n    ---@param cb snacks.scope.Attach.cb\n    ---@param opts? snacks.scope.Config\n    ---@return snacks.scope.Listener\n    Snacks.scope.attach(cb, opts)\n<\n\n\n`Snacks.scope.get()`                                      *Snacks.scope.get()*\n\n>lua\n    ---@param cb fun(scope?: snacks.scope.Scope)\n    ---@param opts? snacks.scope.Opts|{parse?:boolean}\n    Snacks.scope.get(cb, opts)\n<\n\n\n`Snacks.scope.jump()`                                    *Snacks.scope.jump()*\n\nJump to the top or bottom of the scope If the scope is the same as the current\nscope, it will jump to the parent scope instead.\n\n>lua\n    ---@param opts? snacks.scope.Jump\n    Snacks.scope.jump(opts)\n<\n\n\n`Snacks.scope.textobject()`                        *Snacks.scope.textobject()*\n\nText objects for indent scopes. Best to use with Treesitter disabled. When in\nvisual mode, it will select the scope containing the visual selection. When the\nscope is the same as the visual selection, it will select the parent scope\ninstead.\n\n>lua\n    ---@param opts? snacks.scope.TextObject\n    Snacks.scope.textobject(opts)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-scratch.txt",
    "content": "*snacks-scratch*                             snacks scratch docs\n\n==============================================================================\nTable of Contents                      *snacks.nvim-scratch-table-of-contents*\n\n1. Usage                                           |snacks.nvim-scratch-usage|\n2. Setup                                           |snacks.nvim-scratch-setup|\n3. Config                                         |snacks.nvim-scratch-config|\n4. Styles                                         |snacks.nvim-scratch-styles|\n  - scratch                               |snacks.nvim-scratch-styles-scratch|\n5. Types                                           |snacks.nvim-scratch-types|\n6. Module                                         |snacks.nvim-scratch-module|\n  - Snacks.scratch()             |snacks.nvim-scratch-module-snacks.scratch()|\n  - Snacks.scratch.list()   |snacks.nvim-scratch-module-snacks.scratch.list()|\n  - Snacks.scratch.open()   |snacks.nvim-scratch-module-snacks.scratch.open()|\n  - Snacks.scratch.select()|snacks.nvim-scratch-module-snacks.scratch.select()|\n7. Links                                           |snacks.nvim-scratch-links|\nQuickly open scratch buffers for testing code, creating notes or just messing\naround. Scratch buffers are organized by using context like your working\ndirectory, Git branch and `vim.v.count1`.\n\nIt supports templates, custom keymaps, and auto-saves when you hide the buffer.\n\nIn lua buffers, pressing `<cr>` will execute the buffer / selection with\n`Snacks.debug.run()` that will show print output inline and show errors as\ndiagnostics.\n\n\n==============================================================================\n1. Usage                                           *snacks.nvim-scratch-usage*\n\nSuggested config:\n\n>lua\n    {\n      \"folke/snacks.nvim\",\n      keys = {\n        { \"<leader>.\",  function() Snacks.scratch() end, desc = \"Toggle Scratch Buffer\" },\n        { \"<leader>S\",  function() Snacks.scratch.select() end, desc = \"Select Scratch Buffer\" },\n      }\n    }\n<\n\n\n==============================================================================\n2. Setup                                           *snacks.nvim-scratch-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        scratch = {\n          -- your scratch configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n3. Config                                         *snacks.nvim-scratch-config*\n\n>lua\n    ---@class snacks.scratch.Config\n    ---@field win? snacks.win.Config scratch window\n    ---@field template? string template for new buffers\n    ---@field file? string scratch file path. You probably don't need to set this.\n    ---@field ft? string|fun():string the filetype of the scratch buffer\n    {\n      name = \"Scratch\",\n      ft = function()\n        if vim.bo.buftype == \"\" and vim.bo.filetype ~= \"\" then\n          return vim.bo.filetype\n        end\n        return \"markdown\"\n      end,\n      ---@type string|string[]?\n      icon = nil, -- `icon|{icon, icon_hl}`. defaults to the filetype icon\n      root = vim.fn.stdpath(\"data\") .. \"/scratch\",\n      autowrite = true, -- automatically write when the buffer is hidden\n      -- unique key for the scratch file is based on:\n      -- * name\n      -- * ft\n      -- * vim.v.count1 (useful for keymaps)\n      -- * cwd (optional)\n      -- * branch (optional)\n      filekey = {\n        id = nil, ---@type string? unique id used instead of name for the filename hash\n        cwd = true, -- use current working directory\n        branch = true, -- use current branch name\n        count = true, -- use vim.v.count1\n      },\n      win = { style = \"scratch\" },\n      ---@type table<string, snacks.win.Config>\n      win_by_ft = {\n        lua = {\n          keys = {\n            [\"source\"] = {\n              \"<cr>\",\n              function(self)\n                local name = \"scratch.\" .. vim.fn.fnamemodify(vim.api.nvim_buf_get_name(self.buf), \":e\")\n                Snacks.debug.run({ buf = self.buf, name = name })\n              end,\n              desc = \"Source buffer\",\n              mode = { \"n\", \"x\" },\n            },\n          },\n        },\n      },\n    }\n<\n\n\n==============================================================================\n4. Styles                                         *snacks.nvim-scratch-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nSCRATCH                                   *snacks.nvim-scratch-styles-scratch*\n\n>lua\n    {\n      width = 100,\n      height = 30,\n      bo = { buftype = \"\", buflisted = false, bufhidden = \"hide\", swapfile = false },\n      minimal = false,\n      noautocmd = false,\n      -- position = \"right\",\n      zindex = 20,\n      wo = { winhighlight = \"NormalFloat:Normal\" },\n      footer_keys = true,\n      border = true,\n    }\n<\n\n\n==============================================================================\n5. Types                                           *snacks.nvim-scratch-types*\n\n>lua\n    ---@class snacks.scratch.File\n    ---@field file string full path to the scratch buffer\n    ---@field name string name of the scratch buffer\n    ---@field ft string file type\n    ---@field icon? string icon for the file type\n    ---@field icon_hl? string highlight group for the icon\n    ---@field cwd? string current working directory\n    ---@field branch? string Git branch\n    ---@field count? number vim.v.count1 used to open the buffer\n    ---@field id? string unique id used instead of name for the filename hash\n<\n\n\n==============================================================================\n6. Module                                         *snacks.nvim-scratch-module*\n\n\n`Snacks.scratch()`                                          *Snacks.scratch()*\n\n>lua\n    ---@type fun(opts?: snacks.scratch.Config): snacks.win\n    Snacks.scratch()\n<\n\n\n`Snacks.scratch.list()`                                *Snacks.scratch.list()*\n\nReturn a list of scratch buffers sorted by mtime.\n\n>lua\n    ---@return snacks.scratch.File[]\n    Snacks.scratch.list()\n<\n\n\n`Snacks.scratch.open()`                                *Snacks.scratch.open()*\n\nOpen a scratch buffer with the given options. If a window is already open with\nthe same buffer, it will be closed instead.\n\n>lua\n    ---@param opts? snacks.scratch.Config\n    Snacks.scratch.open(opts)\n<\n\n\n`Snacks.scratch.select()`                            *Snacks.scratch.select()*\n\nSelect a scratch buffer from a list of scratch buffers.\n\n>lua\n    Snacks.scratch.select()\n<\n\n==============================================================================\n7. Links                                           *snacks.nvim-scratch-links*\n\n1. *image*: https://github.com/user-attachments/assets/52ac7c1a-908f-4d1d-97a2-ad4642f8dc36\n2. *image*: https://github.com/user-attachments/assets/d3e766e9-e64a-4c22-85b4-3d965f645b59\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-scroll.txt",
    "content": "*snacks-scroll*                               snacks scroll docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-scroll-table-of-contents*\n\n1. Setup                                            |snacks.nvim-scroll-setup|\n2. Config                                          |snacks.nvim-scroll-config|\n3. Types                                            |snacks.nvim-scroll-types|\n4. Module                                          |snacks.nvim-scroll-module|\n  - Snacks.scroll.disable()|snacks.nvim-scroll-module-snacks.scroll.disable()|\n  - Snacks.scroll.enable()  |snacks.nvim-scroll-module-snacks.scroll.enable()|\nSmooth scrolling for Neovim. Properly handles `scrolloff` and mouse scrolling.\n\nSimilar plugins:\n\n- mini.animate <https://github.com/nvim-mini/mini.animate>\n- neoscroll.nvim <https://github.com/karb94/neoscroll.nvim>\n\n\n==============================================================================\n1. Setup                                            *snacks.nvim-scroll-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        scroll = {\n          -- your scroll configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                          *snacks.nvim-scroll-config*\n\n>lua\n    ---@class snacks.scroll.Config\n    ---@field animate snacks.animate.Config|{}\n    ---@field animate_repeat snacks.animate.Config|{}|{delay:number}\n    {\n      animate = {\n        duration = { step = 10, total = 200 },\n        easing = \"linear\",\n      },\n      -- faster animation when repeating scroll after delay\n      animate_repeat = {\n        delay = 100, -- delay in ms before using the repeat animation\n        duration = { step = 5, total = 50 },\n        easing = \"linear\",\n      },\n      -- what buffers to animate\n      filter = function(buf)\n        return vim.g.snacks_scroll ~= false and vim.b[buf].snacks_scroll ~= false and vim.bo[buf].buftype ~= \"terminal\"\n      end,\n    }\n<\n\n\n==============================================================================\n3. Types                                            *snacks.nvim-scroll-types*\n\n>lua\n    ---@alias snacks.scroll.View {topline:number, lnum:number}\n<\n\n\n==============================================================================\n4. Module                                          *snacks.nvim-scroll-module*\n\n\n`Snacks.scroll.disable()`                            *Snacks.scroll.disable()*\n\n>lua\n    Snacks.scroll.disable()\n<\n\n\n`Snacks.scroll.enable()`                              *Snacks.scroll.enable()*\n\n>lua\n    Snacks.scroll.enable()\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-statuscolumn.txt",
    "content": "*snacks-statuscolumn*                   snacks statuscolumn docs\n\n==============================================================================\nTable of Contents                 *snacks.nvim-statuscolumn-table-of-contents*\n\n1. Setup                                      |snacks.nvim-statuscolumn-setup|\n2. Config                                    |snacks.nvim-statuscolumn-config|\n3. Types                                      |snacks.nvim-statuscolumn-types|\n4. Module                                    |snacks.nvim-statuscolumn-module|\n  - Snacks.statuscolumn()|snacks.nvim-statuscolumn-module-snacks.statuscolumn()|\n  - Snacks.statuscolumn.click_fold()|snacks.nvim-statuscolumn-module-snacks.statuscolumn.click_fold()|\n  - Snacks.statuscolumn.get()|snacks.nvim-statuscolumn-module-snacks.statuscolumn.get()|\n\n==============================================================================\n1. Setup                                      *snacks.nvim-statuscolumn-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        statuscolumn = {\n          -- your statuscolumn configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                    *snacks.nvim-statuscolumn-config*\n\n>lua\n    ---@class snacks.statuscolumn.Config\n    ---@field left snacks.statuscolumn.Components\n    ---@field right snacks.statuscolumn.Components\n    ---@field enabled? boolean\n    {\n      left = { \"mark\", \"sign\" }, -- priority of signs on the left (high to low)\n      right = { \"fold\", \"git\" }, -- priority of signs on the right (high to low)\n      folds = {\n        open = false, -- show open fold icons\n        git_hl = false, -- use Git Signs hl for fold icons\n      },\n      git = {\n        -- patterns to match Git signs\n        patterns = { \"GitSign\", \"MiniDiffSign\" },\n      },\n      refresh = 50, -- refresh at most every 50ms\n    }\n<\n\n\n==============================================================================\n3. Types                                      *snacks.nvim-statuscolumn-types*\n\n>lua\n    ---@class snacks.statuscolumn.FoldInfo\n    ---@field start number Line number where deepest fold starts\n    ---@field level number Fold level, when zero other fields are N/A\n    ---@field llevel number Lowest level that starts in v:lnum\n    ---@field lines number Number of lines from v:lnum to end of closed fold\n<\n\n>lua\n    ---@alias snacks.statuscolumn.Component \"mark\"|\"sign\"|\"fold\"|\"git\"\n    ---@alias snacks.statuscolumn.Components snacks.statuscolumn.Component[]|fun(win:number,buf:number,lnum:number):snacks.statuscolumn.Component[]\n    ---@alias snacks.statuscolumn.Wanted table<snacks.statuscolumn.Component, boolean>\n<\n\n\n==============================================================================\n4. Module                                    *snacks.nvim-statuscolumn-module*\n\n\n`Snacks.statuscolumn()`                                *Snacks.statuscolumn()*\n\n>lua\n    ---@type fun(): string\n    Snacks.statuscolumn()\n<\n\n\n`Snacks.statuscolumn.click_fold()`                              *Snacks.statuscolumn.click_fold()*\n\n>lua\n    Snacks.statuscolumn.click_fold()\n<\n\n\n`Snacks.statuscolumn.get()`                              *Snacks.statuscolumn.get()*\n\n>lua\n    Snacks.statuscolumn.get()\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-styles.txt",
    "content": "*snacks-styles*                               snacks styles docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-styles-table-of-contents*\n\n1. Setup                                            |snacks.nvim-styles-setup|\n2. Styles                                          |snacks.nvim-styles-styles|\n  - blame_line                          |snacks.nvim-styles-styles-blame_line|\n  - dashboard                            |snacks.nvim-styles-styles-dashboard|\n  - float                                    |snacks.nvim-styles-styles-float|\n  - help                                      |snacks.nvim-styles-styles-help|\n  - input                                    |snacks.nvim-styles-styles-input|\n  - lazygit                                |snacks.nvim-styles-styles-lazygit|\n  - minimal                                |snacks.nvim-styles-styles-minimal|\n  - notification                      |snacks.nvim-styles-styles-notification|\n  - notification_history      |snacks.nvim-styles-styles-notification_history|\n  - scratch                                |snacks.nvim-styles-styles-scratch|\n  - snacks_image                      |snacks.nvim-styles-styles-snacks_image|\n  - split                                    |snacks.nvim-styles-styles-split|\n  - terminal                              |snacks.nvim-styles-styles-terminal|\n  - zen                                        |snacks.nvim-styles-styles-zen|\n  - zoom_indicator                  |snacks.nvim-styles-styles-zoom_indicator|\nPlugins provide window styles that can be customized with the `opts.styles`\noption of `snacks.nvim`.\n\n\n==============================================================================\n1. Setup                                            *snacks.nvim-styles-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        ---@type table<string, snacks.win.Config>\n        styles = {\n          -- your styles configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Styles                                          *snacks.nvim-styles-styles*\n\nThese are the default styles that Snacks provides. You can customize them by\nadding your own styles to `opts.styles`.\n\n\nBLAME_LINE                              *snacks.nvim-styles-styles-blame_line*\n\n>lua\n    {\n      width = 0.6,\n      height = 0.6,\n      border = true,\n      title = \" Git Blame \",\n      title_pos = \"center\",\n      ft = \"git\",\n    }\n<\n\n\nDASHBOARD                                *snacks.nvim-styles-styles-dashboard*\n\nThe default style for the dashboard. When opening the dashboard during startup,\nonly the `bo` and `wo` options are used. The other options are used with `:lua\nSnacks.dashboard()`\n\n>lua\n    {\n      zindex = 10,\n      height = 0,\n      width = 0,\n      bo = {\n        bufhidden = \"wipe\",\n        buftype = \"nofile\",\n        buflisted = false,\n        filetype = \"snacks_dashboard\",\n        swapfile = false,\n        undofile = false,\n      },\n      wo = {\n        colorcolumn = \"\",\n        cursorcolumn = false,\n        cursorline = false,\n        foldmethod = \"manual\",\n        list = false,\n        number = false,\n        relativenumber = false,\n        sidescrolloff = 0,\n        signcolumn = \"no\",\n        spell = false,\n        statuscolumn = \"\",\n        statusline = \"\",\n        winbar = \"\",\n        winhighlight = \"Normal:SnacksDashboardNormal,NormalFloat:SnacksDashboardNormal\",\n        wrap = false,\n      },\n    }\n<\n\n\nFLOAT                                        *snacks.nvim-styles-styles-float*\n\n>lua\n    {\n      position = \"float\",\n      backdrop = 60,\n      height = 0.9,\n      width = 0.9,\n      zindex = 50,\n    }\n<\n\n\nHELP                                          *snacks.nvim-styles-styles-help*\n\n>lua\n    {\n      position = \"float\",\n      backdrop = false,\n      border = \"top\",\n      row = -1,\n      width = 0,\n      height = 0.3,\n    }\n<\n\n\nINPUT                                        *snacks.nvim-styles-styles-input*\n\n>lua\n    {\n      backdrop = false,\n      position = \"float\",\n      border = true,\n      title_pos = \"center\",\n      height = 1,\n      width = 60,\n      relative = \"editor\",\n      noautocmd = true,\n      row = 2,\n      -- relative = \"cursor\",\n      -- row = -3,\n      -- col = 0,\n      wo = {\n        winhighlight = \"NormalFloat:SnacksInputNormal,FloatBorder:SnacksInputBorder,FloatTitle:SnacksInputTitle\",\n        cursorline = false,\n      },\n      bo = {\n        filetype = \"snacks_input\",\n        buftype = \"prompt\",\n      },\n      --- buffer local variables\n      b = {\n        completion = false, -- disable blink completions in input\n      },\n      keys = {\n        n_esc = { \"<esc>\", { \"cmp_close\", \"cancel\" }, mode = \"n\", expr = true },\n        i_esc = { \"<esc>\", { \"cmp_close\", \"stopinsert\" }, mode = \"i\", expr = true },\n        i_cr = { \"<cr>\", { \"cmp_accept\", \"confirm\" }, mode = { \"i\", \"n\" }, expr = true },\n        i_tab = { \"<tab>\", { \"cmp_select_next\", \"cmp\" }, mode = \"i\", expr = true },\n        i_ctrl_w = { \"<c-w>\", \"<c-s-w>\", mode = \"i\", expr = true },\n        i_up = { \"<up>\", { \"hist_up\" }, mode = { \"i\", \"n\" } },\n        i_down = { \"<down>\", { \"hist_down\" }, mode = { \"i\", \"n\" } },\n        q = \"cancel\",\n      },\n    }\n<\n\n\nLAZYGIT                                    *snacks.nvim-styles-styles-lazygit*\n\n>lua\n    {}\n<\n\n\nMINIMAL                                    *snacks.nvim-styles-styles-minimal*\n\n>lua\n    {\n      wo = {\n        cursorcolumn = false,\n        cursorline = false,\n        cursorlineopt = \"both\",\n        colorcolumn = \"\",\n        fillchars = \"eob: ,lastline:…\",\n        foldcolumn = \"0\",\n        list = false,\n        listchars = \"extends:…,tab:  \",\n        number = false,\n        relativenumber = false,\n        signcolumn = \"no\",\n        spell = false,\n        winbar = \"\",\n        statuscolumn = \"\",\n        wrap = false,\n        sidescrolloff = 0,\n      },\n    }\n<\n\n\nNOTIFICATION                          *snacks.nvim-styles-styles-notification*\n\n>lua\n    {\n      border = true,\n      zindex = 100,\n      ft = \"markdown\",\n      wo = {\n        winblend = 5,\n        wrap = false,\n        conceallevel = 2,\n        colorcolumn = \"\",\n      },\n      bo = { filetype = \"snacks_notif\" },\n    }\n<\n\n\nNOTIFICATION_HISTORY          *snacks.nvim-styles-styles-notification_history*\n\n>lua\n    {\n      border = true,\n      zindex = 100,\n      width = 0.6,\n      height = 0.6,\n      minimal = false,\n      title = \" Notification History \",\n      title_pos = \"center\",\n      ft = \"markdown\",\n      bo = { filetype = \"snacks_notif_history\", modifiable = false },\n      wo = { winhighlight = \"Normal:SnacksNotifierHistory\" },\n      keys = { q = \"close\" },\n    }\n<\n\n\nSCRATCH                                    *snacks.nvim-styles-styles-scratch*\n\n>lua\n    {\n      width = 100,\n      height = 30,\n      bo = { buftype = \"\", buflisted = false, bufhidden = \"hide\", swapfile = false },\n      minimal = false,\n      noautocmd = false,\n      -- position = \"right\",\n      zindex = 20,\n      wo = { winhighlight = \"NormalFloat:Normal\" },\n      footer_keys = true,\n      border = true,\n    }\n<\n\n\nSNACKS_IMAGE                          *snacks.nvim-styles-styles-snacks_image*\n\n>lua\n    {\n      relative = \"cursor\",\n      border = true,\n      focusable = false,\n      backdrop = false,\n      row = 1,\n      col = 1,\n      -- width/height are automatically set by the image size unless specified below\n    }\n<\n\n\nSPLIT                                        *snacks.nvim-styles-styles-split*\n\n>lua\n    {\n      position = \"bottom\",\n      height = 0.4,\n      width = 0.4,\n    }\n<\n\n\nTERMINAL                                  *snacks.nvim-styles-styles-terminal*\n\n>lua\n    {\n      bo = {\n        filetype = \"snacks_terminal\",\n      },\n      wo = {},\n      stack = true, -- when enabled, multiple split windows with the same position will be stacked together (useful for terminals)\n      keys = {\n        q = \"hide\",\n        gf = function(self)\n          local f = vim.fn.findfile(vim.fn.expand(\"<cfile>\"), \"**\")\n          if f == \"\" then\n            Snacks.notify.warn(\"No file under cursor\")\n          else\n            self:hide()\n            vim.schedule(function()\n              vim.cmd(\"e \" .. f)\n            end)\n          end\n        end,\n        term_normal = {\n          \"<esc>\",\n          function(self)\n            self.esc_timer = self.esc_timer or (vim.uv or vim.loop).new_timer()\n            if self.esc_timer:is_active() then\n              self.esc_timer:stop()\n              vim.cmd(\"stopinsert\")\n            else\n              self.esc_timer:start(200, 0, function() end)\n              return \"<esc>\"\n            end\n          end,\n          mode = \"t\",\n          expr = true,\n          desc = \"Double escape to normal mode\",\n        },\n      },\n    }\n<\n\n\nZEN                                            *snacks.nvim-styles-styles-zen*\n\n>lua\n    {\n      enter = true,\n      fixbuf = false,\n      minimal = false,\n      width = 120,\n      height = 0,\n      backdrop = { transparent = true, blend = 40 },\n      keys = { q = false },\n      zindex = 40,\n      wo = {\n        winhighlight = \"NormalFloat:Normal\",\n      },\n      w = {\n        snacks_main = true,\n      },\n    }\n<\n\n\nZOOM_INDICATOR                      *snacks.nvim-styles-styles-zoom_indicator*\n\nfullscreen indicator only shown when the window is maximized\n\n>lua\n    {\n      text = \"▍ zoom  󰊓  \",\n      minimal = true,\n      enter = false,\n      focusable = false,\n      height = 1,\n      row = 0,\n      col = -1,\n      backdrop = false,\n    }\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-terminal.txt",
    "content": "*snacks-terminal*                           snacks terminal docs\n\n==============================================================================\nTable of Contents                     *snacks.nvim-terminal-table-of-contents*\n\n1. Usage                                          |snacks.nvim-terminal-usage|\n  - Edgy Integration             |snacks.nvim-terminal-usage-edgy-integration|\n2. Setup                                          |snacks.nvim-terminal-setup|\n3. Config                                        |snacks.nvim-terminal-config|\n4. Styles                                        |snacks.nvim-terminal-styles|\n  - terminal                            |snacks.nvim-terminal-styles-terminal|\n5. Types                                          |snacks.nvim-terminal-types|\n6. Module                                        |snacks.nvim-terminal-module|\n  - Snacks.terminal()          |snacks.nvim-terminal-module-snacks.terminal()|\n  - Snacks.terminal.colorize()|snacks.nvim-terminal-module-snacks.terminal.colorize()|\n  - Snacks.terminal.focus()|snacks.nvim-terminal-module-snacks.terminal.focus()|\n  - Snacks.terminal.get()  |snacks.nvim-terminal-module-snacks.terminal.get()|\n  - Snacks.terminal.list()|snacks.nvim-terminal-module-snacks.terminal.list()|\n  - Snacks.terminal.open()|snacks.nvim-terminal-module-snacks.terminal.open()|\n  - Snacks.terminal.tid()  |snacks.nvim-terminal-module-snacks.terminal.tid()|\n  - Snacks.terminal.toggle()|snacks.nvim-terminal-module-snacks.terminal.toggle()|\n7. Links                                          |snacks.nvim-terminal-links|\nCreate and toggle terminal windows.\n\nBased on the provided options, some defaults will be set:\n\n- if no `cmd` is provided, the window will be opened in a bottom split\n- if `cmd` is provided, the window will be opened in a floating window\n- for splits, a `winbar` will be added with the terminal title\n\n\n==============================================================================\n1. Usage                                          *snacks.nvim-terminal-usage*\n\n\nEDGY INTEGRATION                 *snacks.nvim-terminal-usage-edgy-integration*\n\n>lua\n    {\n      \"folke/edgy.nvim\",\n      ---@module 'edgy'\n      ---@param opts Edgy.Config\n      opts = function(_, opts)\n        for _, pos in ipairs({ \"top\", \"bottom\", \"left\", \"right\" }) do\n          opts[pos] = opts[pos] or {}\n          table.insert(opts[pos], {\n            ft = \"snacks_terminal\",\n            size = { height = 0.4 },\n            title = \"%{b:snacks_terminal.id}: %{b:term_title}\",\n            filter = function(_buf, win)\n              return vim.w[win].snacks_win\n                and vim.w[win].snacks_win.position == pos\n                and vim.w[win].snacks_win.relative == \"editor\"\n                and not vim.w[win].trouble_preview\n            end,\n          })\n        end\n      end,\n    }\n<\n\n\n==============================================================================\n2. Setup                                          *snacks.nvim-terminal-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        terminal = {\n          -- your terminal configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n3. Config                                        *snacks.nvim-terminal-config*\n\n>lua\n    ---@class snacks.terminal.Config\n    ---@field win? snacks.win.Config|{}\n    ---@field shell? string|string[] The shell to use. Defaults to `vim.o.shell`\n    ---@field override? fun(cmd?: string|string[], opts?: snacks.terminal.Opts) Use this to use a different terminal implementation\n    {\n      win = { style = \"terminal\" },\n    }\n<\n\n\n==============================================================================\n4. Styles                                        *snacks.nvim-terminal-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nTERMINAL                                *snacks.nvim-terminal-styles-terminal*\n\n>lua\n    {\n      bo = {\n        filetype = \"snacks_terminal\",\n      },\n      wo = {},\n      stack = true, -- when enabled, multiple split windows with the same position will be stacked together (useful for terminals)\n      keys = {\n        q = \"hide\",\n        gf = function(self)\n          local f = vim.fn.findfile(vim.fn.expand(\"<cfile>\"), \"**\")\n          if f == \"\" then\n            Snacks.notify.warn(\"No file under cursor\")\n          else\n            self:hide()\n            vim.schedule(function()\n              vim.cmd(\"e \" .. f)\n            end)\n          end\n        end,\n        term_normal = {\n          \"<esc>\",\n          function(self)\n            self.esc_timer = self.esc_timer or (vim.uv or vim.loop).new_timer()\n            if self.esc_timer:is_active() then\n              self.esc_timer:stop()\n              vim.cmd(\"stopinsert\")\n            else\n              self.esc_timer:start(200, 0, function() end)\n              return \"<esc>\"\n            end\n          end,\n          mode = \"t\",\n          expr = true,\n          desc = \"Double escape to normal mode\",\n        },\n      },\n    }\n<\n\n\n==============================================================================\n5. Types                                          *snacks.nvim-terminal-types*\n\n>lua\n    ---@class snacks.terminal.Opts: snacks.terminal.Config\n    ---@field cwd? string\n    ---@field count? integer\n    ---@field env? table<string, string>\n    ---@field start_insert? boolean start insert mode when starting the terminal\n    ---@field auto_insert? boolean start insert mode when entering the terminal buffer\n    ---@field auto_close? boolean close the terminal buffer when the process exits\n    ---@field interactive? boolean shortcut for `start_insert`, `auto_close` and `auto_insert` (default: true)\n<\n\n\n==============================================================================\n6. Module                                        *snacks.nvim-terminal-module*\n\n>lua\n    ---@class snacks.terminal: snacks.win\n    ---@field cmd? string | string[]\n    ---@field opts snacks.terminal.Opts\n    Snacks.terminal = {}\n<\n\n\n`Snacks.terminal()`                                        *Snacks.terminal()*\n\n>lua\n    ---@type fun(cmd?: string|string[], opts?: snacks.terminal.Opts): snacks.terminal\n    Snacks.terminal()\n<\n\n\n`Snacks.terminal.colorize()`                          *Snacks.terminal.colorize()*\n\nColorize the current buffer. Replaces ansii color codes with the actual colors.\n\nExample:\n\n>sh\n    ls -la --color=always | nvim - -c \"lua Snacks.terminal.colorize()\"\n<\n\n>lua\n    Snacks.terminal.colorize()\n<\n\n\n`Snacks.terminal.focus()`                            *Snacks.terminal.focus()*\n\nFocus a terminal window. If already focused, hide it. The terminal id is based\non the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n\n>lua\n    ---@param cmd? string | string[]\n    ---@param opts? snacks.terminal.Opts\n    Snacks.terminal.focus(cmd, opts)\n<\n\n\n`Snacks.terminal.get()`                                *Snacks.terminal.get()*\n\nGet or create a terminal window. The terminal id is based on the `cmd`, `cwd`,\n`env` and `vim.v.count1` options. `opts.create` defaults to `true`.\n\n>lua\n    ---@param cmd? string | string[]\n    ---@param opts? snacks.terminal.Opts| {create?: boolean}\n    ---@return snacks.win? terminal, boolean? created\n    Snacks.terminal.get(cmd, opts)\n<\n\n\n`Snacks.terminal.list()`                              *Snacks.terminal.list()*\n\n>lua\n    ---@return snacks.win[]\n    Snacks.terminal.list()\n<\n\n\n`Snacks.terminal.open()`                              *Snacks.terminal.open()*\n\nOpen a new terminal window.\n\n>lua\n    ---@param cmd? string | string[]\n    ---@param opts? snacks.terminal.Opts\n    Snacks.terminal.open(cmd, opts)\n<\n\n\n`Snacks.terminal.tid()`                                *Snacks.terminal.tid()*\n\nGet a terminal id based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n\n>lua\n    ---@param cmd? string | string[]\n    ---@param opts? snacks.terminal.Opts\n    Snacks.terminal.tid(cmd, opts)\n<\n\n\n`Snacks.terminal.toggle()`                          *Snacks.terminal.toggle()*\n\nToggle a terminal window. The terminal id is based on the `cmd`, `cwd`, `env`\nand `vim.v.count1` options.\n\n>lua\n    ---@param cmd? string | string[]\n    ---@param opts? snacks.terminal.Opts\n    Snacks.terminal.toggle(cmd, opts)\n<\n\n==============================================================================\n7. Links                                          *snacks.nvim-terminal-links*\n\n1. *image*: https://github.com/user-attachments/assets/afcc9989-57d7-4518-a390-cc7d6f0cec13\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-toggle.txt",
    "content": "*snacks-toggle*                               snacks toggle docs\n\n==============================================================================\nTable of Contents                       *snacks.nvim-toggle-table-of-contents*\n\n1. Setup                                            |snacks.nvim-toggle-setup|\n2. Config                                          |snacks.nvim-toggle-config|\n3. Types                                            |snacks.nvim-toggle-types|\n4. Module                                          |snacks.nvim-toggle-module|\n  - Snacks.toggle()                |snacks.nvim-toggle-module-snacks.toggle()|\n  - Snacks.toggle.animate()|snacks.nvim-toggle-module-snacks.toggle.animate()|\n  - Snacks.toggle.diagnostics()|snacks.nvim-toggle-module-snacks.toggle.diagnostics()|\n  - Snacks.toggle.dim()        |snacks.nvim-toggle-module-snacks.toggle.dim()|\n  - Snacks.toggle.get()        |snacks.nvim-toggle-module-snacks.toggle.get()|\n  - Snacks.toggle.indent()  |snacks.nvim-toggle-module-snacks.toggle.indent()|\n  - Snacks.toggle.inlay_hints()|snacks.nvim-toggle-module-snacks.toggle.inlay_hints()|\n  - Snacks.toggle.line_number()|snacks.nvim-toggle-module-snacks.toggle.line_number()|\n  - Snacks.toggle.new()        |snacks.nvim-toggle-module-snacks.toggle.new()|\n  - Snacks.toggle.option()  |snacks.nvim-toggle-module-snacks.toggle.option()|\n  - Snacks.toggle.profiler()|snacks.nvim-toggle-module-snacks.toggle.profiler()|\n  - Snacks.toggle.profiler_highlights()|snacks.nvim-toggle-module-snacks.toggle.profiler_highlights()|\n  - Snacks.toggle.scroll()  |snacks.nvim-toggle-module-snacks.toggle.scroll()|\n  - Snacks.toggle.treesitter()|snacks.nvim-toggle-module-snacks.toggle.treesitter()|\n  - Snacks.toggle.words()    |snacks.nvim-toggle-module-snacks.toggle.words()|\n  - Snacks.toggle.zen()        |snacks.nvim-toggle-module-snacks.toggle.zen()|\n  - Snacks.toggle.zoom()      |snacks.nvim-toggle-module-snacks.toggle.zoom()|\n5. Links                                            |snacks.nvim-toggle-links|\nToggle keymaps integrated with which-key icons / colors\n\n\n==============================================================================\n1. Setup                                            *snacks.nvim-toggle-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        toggle = {\n          -- your toggle configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                          *snacks.nvim-toggle-config*\n\n>lua\n    ---@class snacks.toggle.Config\n    ---@field icon? string|{ enabled: string, disabled: string }\n    ---@field color? string|{ enabled: string, disabled: string }\n    ---@field wk_desc? string|{ enabled: string, disabled: string }\n    ---@field map? fun(mode: string|string[], lhs: string, rhs: string|fun(), opts?: vim.keymap.set.Opts)\n    ---@field which_key? boolean\n    ---@field notify? boolean|fun(state:boolean, opts: snacks.toggle.Opts)\n    {\n      map = vim.keymap.set, -- keymap.set function to use\n      which_key = true, -- integrate with which-key to show enabled/disabled icons and colors\n      notify = true, -- show a notification when toggling\n      -- icons for enabled/disabled states\n      icon = {\n        enabled = \" \",\n        disabled = \" \",\n      },\n      -- colors for enabled/disabled states\n      color = {\n        enabled = \"green\",\n        disabled = \"yellow\",\n      },\n      wk_desc = {\n        enabled = \"Disable \",\n        disabled = \"Enable \",\n      },\n    }\n<\n\n\n==============================================================================\n3. Types                                            *snacks.nvim-toggle-types*\n\n>lua\n    ---@class snacks.toggle.Opts: snacks.toggle.Config\n    ---@field id? string\n    ---@field name string\n    ---@field get fun():boolean\n    ---@field set fun(state:boolean)\n<\n\n\n==============================================================================\n4. Module                                          *snacks.nvim-toggle-module*\n\n\n`Snacks.toggle()`                                            *Snacks.toggle()*\n\n>lua\n    ---@type fun(... :snacks.toggle.Opts): snacks.toggle.Class\n    Snacks.toggle()\n<\n\n\n`Snacks.toggle.animate()`                            *Snacks.toggle.animate()*\n\n>lua\n    Snacks.toggle.animate()\n<\n\n\n`Snacks.toggle.diagnostics()`                        *Snacks.toggle.diagnostics()*\n\n>lua\n    ---@param opts? snacks.toggle.Config\n    Snacks.toggle.diagnostics(opts)\n<\n\n\n`Snacks.toggle.dim()`                                    *Snacks.toggle.dim()*\n\n>lua\n    Snacks.toggle.dim()\n<\n\n\n`Snacks.toggle.get()`                                    *Snacks.toggle.get()*\n\n>lua\n    ---@param id string\n    ---@return snacks.toggle.Class?\n    Snacks.toggle.get(id)\n<\n\n\n`Snacks.toggle.indent()`                              *Snacks.toggle.indent()*\n\n>lua\n    Snacks.toggle.indent()\n<\n\n\n`Snacks.toggle.inlay_hints()`                        *Snacks.toggle.inlay_hints()*\n\n>lua\n    ---@param opts? snacks.toggle.Config\n    Snacks.toggle.inlay_hints(opts)\n<\n\n\n`Snacks.toggle.line_number()`                        *Snacks.toggle.line_number()*\n\n>lua\n    ---@param opts? snacks.toggle.Config\n    Snacks.toggle.line_number(opts)\n<\n\n\n`Snacks.toggle.new()`                                    *Snacks.toggle.new()*\n\n>lua\n    ---@param ... snacks.toggle.Opts\n    Snacks.toggle.new(...)\n<\n\n\n`Snacks.toggle.option()`                              *Snacks.toggle.option()*\n\n>lua\n    ---@param option string\n    ---@param opts? snacks.toggle.Config | {on?: unknown, off?: unknown, global?: boolean}\n    Snacks.toggle.option(option, opts)\n<\n\n\n`Snacks.toggle.profiler()`                          *Snacks.toggle.profiler()*\n\n>lua\n    Snacks.toggle.profiler()\n<\n\n\n`Snacks.toggle.profiler_highlights()`                        *Snacks.toggle.profiler_highlights()*\n\n>lua\n    Snacks.toggle.profiler_highlights()\n<\n\n\n`Snacks.toggle.scroll()`                              *Snacks.toggle.scroll()*\n\n>lua\n    Snacks.toggle.scroll()\n<\n\n\n`Snacks.toggle.treesitter()`                        *Snacks.toggle.treesitter()*\n\n>lua\n    ---@param opts? snacks.toggle.Config\n    Snacks.toggle.treesitter(opts)\n<\n\n\n`Snacks.toggle.words()`                                *Snacks.toggle.words()*\n\n>lua\n    Snacks.toggle.words()\n<\n\n\n`Snacks.toggle.zen()`                                    *Snacks.toggle.zen()*\n\n>lua\n    Snacks.toggle.zen()\n<\n\n\n`Snacks.toggle.zoom()`                                  *Snacks.toggle.zoom()*\n\n>lua\n    Snacks.toggle.zoom()\n<\n\n==============================================================================\n5. Links                                            *snacks.nvim-toggle-links*\n\n1. *image*: https://github.com/user-attachments/assets/6d843acd-1ac1-44fd-b318-58b4c17de2d5\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-util.txt",
    "content": "*snacks-util*                                   snacks util docs\n\n==============================================================================\nTable of Contents                         *snacks.nvim-util-table-of-contents*\n\n1. Types                                              |snacks.nvim-util-types|\n2. Module                                            |snacks.nvim-util-module|\n  - Snacks.util.blend()          |snacks.nvim-util-module-snacks.util.blend()|\n  - Snacks.util.bo()                |snacks.nvim-util-module-snacks.util.bo()|\n  - Snacks.util.color()          |snacks.nvim-util-module-snacks.util.color()|\n  - Snacks.util.debounce()    |snacks.nvim-util-module-snacks.util.debounce()|\n  - Snacks.util.file_decode()|snacks.nvim-util-module-snacks.util.file_decode()|\n  - Snacks.util.file_encode()|snacks.nvim-util-module-snacks.util.file_encode()|\n  - Snacks.util.get_lang()    |snacks.nvim-util-module-snacks.util.get_lang()|\n  - Snacks.util.icon()            |snacks.nvim-util-module-snacks.util.icon()|\n  - Snacks.util.is_float()    |snacks.nvim-util-module-snacks.util.is_float()|\n  - Snacks.util.is_transparent()|snacks.nvim-util-module-snacks.util.is_transparent()|\n  - Snacks.util.keycode()      |snacks.nvim-util-module-snacks.util.keycode()|\n  - Snacks.util.normkey()      |snacks.nvim-util-module-snacks.util.normkey()|\n  - Snacks.util.on_key()        |snacks.nvim-util-module-snacks.util.on_key()|\n  - Snacks.util.on_module()  |snacks.nvim-util-module-snacks.util.on_module()|\n  - Snacks.util.parse()          |snacks.nvim-util-module-snacks.util.parse()|\n  - Snacks.util.path_type()  |snacks.nvim-util-module-snacks.util.path_type()|\n  - Snacks.util.redraw()        |snacks.nvim-util-module-snacks.util.redraw()|\n  - Snacks.util.redraw_range()|snacks.nvim-util-module-snacks.util.redraw_range()|\n  - Snacks.util.ref()              |snacks.nvim-util-module-snacks.util.ref()|\n  - Snacks.util.set_hl()        |snacks.nvim-util-module-snacks.util.set_hl()|\n  - Snacks.util.spinner()      |snacks.nvim-util-module-snacks.util.spinner()|\n  - Snacks.util.stop()            |snacks.nvim-util-module-snacks.util.stop()|\n  - Snacks.util.throttle()    |snacks.nvim-util-module-snacks.util.throttle()|\n  - Snacks.util.var()              |snacks.nvim-util-module-snacks.util.var()|\n  - Snacks.util.winhl()          |snacks.nvim-util-module-snacks.util.winhl()|\n  - Snacks.util.wo()                |snacks.nvim-util-module-snacks.util.wo()|\n\n==============================================================================\n1. Types                                              *snacks.nvim-util-types*\n\n>lua\n    ---@alias snacks.util.hl table<string, string|vim.api.keyset.highlight>\n<\n\n\n==============================================================================\n2. Module                                            *snacks.nvim-util-module*\n\n>lua\n    ---@class snacks.util\n    ---@field spawn snacks.spawn\n    ---@field lsp snacks.lsp\n    Snacks.util = {}\n<\n\n\n`Snacks.util.blend()`                                    *Snacks.util.blend()*\n\n>lua\n    ---@param fg string foreground color\n    ---@param bg string background color\n    ---@param alpha number number between 0 and 1. 0 results in bg, 1 results in fg\n    Snacks.util.blend(fg, bg, alpha)\n<\n\n\n`Snacks.util.bo()`                                          *Snacks.util.bo()*\n\nSet buffer-local options.\n\n>lua\n    ---@param buf number\n    ---@param bo vim.bo|{}\n    Snacks.util.bo(buf, bo)\n<\n\n\n`Snacks.util.color()`                                    *Snacks.util.color()*\n\n>lua\n    ---@param group string|string[] hl group to get color from\n    ---@param prop? string property to get. Defaults to \"fg\"\n    Snacks.util.color(group, prop)\n<\n\n\n`Snacks.util.debounce()`                              *Snacks.util.debounce()*\n\n>lua\n    ---@generic T\n    ---@param fn T\n    ---@param opts? {ms?:number}\n    ---@return T\n    Snacks.util.debounce(fn, opts)\n<\n\n\n`Snacks.util.file_decode()`                        *Snacks.util.file_decode()*\n\nDecodes a file name to a string.\n\n>lua\n    ---@param str string\n    Snacks.util.file_decode(str)\n<\n\n\n`Snacks.util.file_encode()`                        *Snacks.util.file_encode()*\n\nEncodes a string to be used as a file name.\n\n>lua\n    ---@param str string\n    Snacks.util.file_encode(str)\n<\n\n\n`Snacks.util.get_lang()`                              *Snacks.util.get_lang()*\n\n>lua\n    ---@param lang string|number|nil\n    ---@overload fun(buf:number):string?\n    ---@overload fun(ft:string):string?\n    ---@return string?\n    Snacks.util.get_lang(lang)\n<\n\n\n`Snacks.util.icon()`                                      *Snacks.util.icon()*\n\nGet an icon from `mini.icons` or `nvim-web-devicons`.\n\n>lua\n    ---@param name string\n    ---@param cat? string \"file\"|\"filetype\"|\"extension\"|\"directory\"\n    ---@param opts? { fallback?: {dir?:string, file?:string} }\n    ---@return string, string?\n    Snacks.util.icon(name, cat, opts)\n<\n\n\n`Snacks.util.is_float()`                              *Snacks.util.is_float()*\n\n>lua\n    ---@param win? number\n    Snacks.util.is_float(win)\n<\n\n\n`Snacks.util.is_transparent()`                      *Snacks.util.is_transparent()*\n\nCheck if the colorscheme is transparent.\n\n>lua\n    Snacks.util.is_transparent()\n<\n\n\n`Snacks.util.keycode()`                                *Snacks.util.keycode()*\n\n>lua\n    ---@param str string\n    Snacks.util.keycode(str)\n<\n\n\n`Snacks.util.normkey()`                                *Snacks.util.normkey()*\n\n>lua\n    ---@param key string\n    Snacks.util.normkey(key)\n<\n\n\n`Snacks.util.on_key()`                                  *Snacks.util.on_key()*\n\n>lua\n    ---@param key string\n    ---@param cb fun(key:string)\n    Snacks.util.on_key(key, cb)\n<\n\n\n`Snacks.util.on_module()`                            *Snacks.util.on_module()*\n\nCall a function when a module is loaded. The callback is called immediately if\nthe module is already loaded. Otherwise, it is called when the module is\nloaded.\n\n>lua\n    ---@param modname string\n    ---@param cb fun(modname:string)\n    Snacks.util.on_module(modname, cb)\n<\n\n\n`Snacks.util.parse()`                                    *Snacks.util.parse()*\n\nParse async when available.\n\n>lua\n    ---@param parser vim.treesitter.LanguageTree\n    ---@param range boolean|Range|nil: Parse this range in the parser's source.\n    ---@param on_parse fun(err?: string, trees?: table<integer, TSTree>) Function invoked when parsing completes.\n    Snacks.util.parse(parser, range, on_parse)\n<\n\n\n`Snacks.util.path_type()`                            *Snacks.util.path_type()*\n\nBetter validation to check if path is a dir or a file\n\n>lua\n    ---@param path string\n    ---@return \"directory\"|\"file\"\n    Snacks.util.path_type(path)\n<\n\n\n`Snacks.util.redraw()`                                  *Snacks.util.redraw()*\n\nRedraw the window. Optimized for Neovim >= 0.10\n\n>lua\n    ---@param win number\n    Snacks.util.redraw(win)\n<\n\n\n`Snacks.util.redraw_range()`                      *Snacks.util.redraw_range()*\n\nRedraw the range of lines in the window. Optimized for Neovim >= 0.10\n\n>lua\n    ---@param win number\n    ---@param from number -- 1-indexed, inclusive\n    ---@param to number -- 1-indexed, inclusive\n    Snacks.util.redraw_range(win, from, to)\n<\n\n\n`Snacks.util.ref()`                                        *Snacks.util.ref()*\n\n>lua\n    ---@generic T\n    ---@param t T\n    ---@return { value?:T }|fun():T?\n    Snacks.util.ref(t)\n<\n\n\n`Snacks.util.set_hl()`                                  *Snacks.util.set_hl()*\n\nEnsures the hl groups are always set, even after a colorscheme change.\n\n>lua\n    ---@param groups snacks.util.hl\n    ---@param opts? { prefix?:string, default?:boolean, managed?:boolean }\n    Snacks.util.set_hl(groups, opts)\n<\n\n\n`Snacks.util.spinner()`                                *Snacks.util.spinner()*\n\n>lua\n    Snacks.util.spinner()\n<\n\n\n`Snacks.util.stop()`                                      *Snacks.util.stop()*\n\n>lua\n    ---@param handle? uv.uv_handle_t|uv.uv_timer_t\n    Snacks.util.stop(handle)\n<\n\n\n`Snacks.util.throttle()`                              *Snacks.util.throttle()*\n\n>lua\n    ---@generic T\n    ---@param fn T\n    ---@param opts? {ms?:number}\n    ---@return T\n    Snacks.util.throttle(fn, opts)\n<\n\n\n`Snacks.util.var()`                                        *Snacks.util.var()*\n\nGet a buffer or global variable.\n\n>lua\n    ---@generic T\n    ---@param buf? number\n    ---@param name string\n    ---@param default? T\n    ---@return T\n    Snacks.util.var(buf, name, default)\n<\n\n\n`Snacks.util.winhl()`                                    *Snacks.util.winhl()*\n\nMerges vim.wo.winhighlight options. Option values can be a string or a\ndictionary.\n\n>lua\n    ---@param ... string|table<string, string>\n    Snacks.util.winhl(...)\n<\n\n\n`Snacks.util.wo()`                                          *Snacks.util.wo()*\n\nSet window-local options.\n\n>lua\n    ---@param win number\n    ---@param wo vim.wo|{}|{winhighlight: string|table<string, string>}\n    Snacks.util.wo(win, wo)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-win.txt",
    "content": "*snacks-win*                                     snacks win docs\n\n==============================================================================\nTable of Contents                          *snacks.nvim-win-table-of-contents*\n\n1. Usage                                               |snacks.nvim-win-usage|\n2. Setup                                               |snacks.nvim-win-setup|\n3. Config                                             |snacks.nvim-win-config|\n4. Styles                                             |snacks.nvim-win-styles|\n  - float                                       |snacks.nvim-win-styles-float|\n  - help                                         |snacks.nvim-win-styles-help|\n  - minimal                                   |snacks.nvim-win-styles-minimal|\n  - split                                       |snacks.nvim-win-styles-split|\n5. Types                                               |snacks.nvim-win-types|\n6. Module                                             |snacks.nvim-win-module|\n  - Snacks.win()                         |snacks.nvim-win-module-snacks.win()|\n  - Snacks.win.is_border()     |snacks.nvim-win-module-snacks.win.is_border()|\n  - Snacks.win.new()                 |snacks.nvim-win-module-snacks.win.new()|\n  - Snacks.win.zindex()           |snacks.nvim-win-module-snacks.win.zindex()|\n  - win:action()                         |snacks.nvim-win-module-win:action()|\n  - win:add_padding()               |snacks.nvim-win-module-win:add_padding()|\n  - win:border()                         |snacks.nvim-win-module-win:border()|\n  - win:border_size()               |snacks.nvim-win-module-win:border_size()|\n  - win:border_text_width()   |snacks.nvim-win-module-win:border_text_width()|\n  - win:buf_valid()                   |snacks.nvim-win-module-win:buf_valid()|\n  - win:close()                           |snacks.nvim-win-module-win:close()|\n  - win:destroy()                       |snacks.nvim-win-module-win:destroy()|\n  - win:dim()                               |snacks.nvim-win-module-win:dim()|\n  - win:execute()                       |snacks.nvim-win-module-win:execute()|\n  - win:fixbuf()                         |snacks.nvim-win-module-win:fixbuf()|\n  - win:focus()                           |snacks.nvim-win-module-win:focus()|\n  - win:has_border()                 |snacks.nvim-win-module-win:has_border()|\n  - win:hide()                             |snacks.nvim-win-module-win:hide()|\n  - win:hscroll()                       |snacks.nvim-win-module-win:hscroll()|\n  - win:is_floating()               |snacks.nvim-win-module-win:is_floating()|\n  - win:line()                             |snacks.nvim-win-module-win:line()|\n  - win:lines()                           |snacks.nvim-win-module-win:lines()|\n  - win:map()                               |snacks.nvim-win-module-win:map()|\n  - win:on()                                 |snacks.nvim-win-module-win:on()|\n  - win:on_current_tab()         |snacks.nvim-win-module-win:on_current_tab()|\n  - win:on_resize()                   |snacks.nvim-win-module-win:on_resize()|\n  - win:parent_size()               |snacks.nvim-win-module-win:parent_size()|\n  - win:redraw()                         |snacks.nvim-win-module-win:redraw()|\n  - win:scratch()                       |snacks.nvim-win-module-win:scratch()|\n  - win:scroll()                         |snacks.nvim-win-module-win:scroll()|\n  - win:set_buf()                       |snacks.nvim-win-module-win:set_buf()|\n  - win:set_title()                   |snacks.nvim-win-module-win:set_title()|\n  - win:show()                             |snacks.nvim-win-module-win:show()|\n  - win:size()                             |snacks.nvim-win-module-win:size()|\n  - win:text()                             |snacks.nvim-win-module-win:text()|\n  - win:toggle()                         |snacks.nvim-win-module-win:toggle()|\n  - win:toggle_help()               |snacks.nvim-win-module-win:toggle_help()|\n  - win:update()                         |snacks.nvim-win-module-win:update()|\n  - win:valid()                           |snacks.nvim-win-module-win:valid()|\n  - win:win_valid()                   |snacks.nvim-win-module-win:win_valid()|\n7. Links                                               |snacks.nvim-win-links|\nEasily create and manage floating windows or splits\n\n\n==============================================================================\n1. Usage                                               *snacks.nvim-win-usage*\n\n>lua\n    Snacks.win({\n      file = vim.api.nvim_get_runtime_file(\"doc/news.txt\", false)[1],\n      width = 0.6,\n      height = 0.6,\n      wo = {\n        spell = false,\n        wrap = false,\n        signcolumn = \"yes\",\n        statuscolumn = \" \",\n        conceallevel = 3,\n      },\n    })\n<\n\n\n==============================================================================\n2. Setup                                               *snacks.nvim-win-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        win = {\n          -- your win configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n3. Config                                             *snacks.nvim-win-config*\n\n>lua\n    ---@class snacks.win.Config: vim.api.keyset.win_config\n    ---@field style? string merges with config from `Snacks.config.styles[style]`\n    ---@field show? boolean Show the window immediately (default: true)\n    ---@field footer_keys? boolean|string[] Show keys footer. When string[], only show those keys with lhs (default: false)\n    ---@field height? number|fun(self:snacks.win):number Height of the window. Use <1 for relative height. 0 means full height. (default: 0.9)\n    ---@field width? number|fun(self:snacks.win):number Width of the window. Use <1 for relative width. 0 means full width. (default: 0.9)\n    ---@field min_height? number Minimum height of the window\n    ---@field max_height? number Maximum height of the window\n    ---@field min_width? number Minimum width of the window\n    ---@field max_width? number Maximum width of the window\n    ---@field col? number|fun(self:snacks.win):number Column of the window. Use <1 for relative column. (default: center)\n    ---@field row? number|fun(self:snacks.win):number Row of the window. Use <1 for relative row. (default: center)\n    ---@field minimal? boolean Disable a bunch of options to make the window minimal (default: true)\n    ---@field position? \"float\"|\"bottom\"|\"top\"|\"left\"|\"right\"|\"current\"\n    ---@field border? \"none\"|\"top\"|\"right\"|\"bottom\"|\"left\"|\"top_bottom\"|\"hpad\"|\"vpad\"|\"rounded\"|\"single\"|\"double\"|\"solid\"|\"shadow\"|\"bold\"|string[]|false|true\n    ---@field buf? number If set, use this buffer instead of creating a new one\n    ---@field file? string If set, use this file instead of creating a new buffer\n    ---@field enter? boolean Enter the window after opening (default: false)\n    ---@field backdrop? number|false|snacks.win.Backdrop Opacity of the backdrop (default: 60)\n    ---@field wo? vim.wo|{} window options\n    ---@field bo? vim.bo|{} buffer options\n    ---@field b? table<string, any> buffer local variables\n    ---@field w? table<string, any> window local variables\n    ---@field ft? string filetype to use for treesitter/syntax highlighting. Won't override existing filetype\n    ---@field scratch_ft? string filetype to use for scratch buffers\n    ---@field keys? table<string, false|string|fun(self: snacks.win)|snacks.win.Keys> Key mappings\n    ---@field on_buf? fun(self: snacks.win) Callback after opening the buffer\n    ---@field on_win? fun(self: snacks.win) Callback after opening the window\n    ---@field on_close? fun(self: snacks.win) Callback after closing the window\n    ---@field fixbuf? boolean don't allow other buffers to be opened in this window\n    ---@field text? string|string[]|fun():(string[]|string) Initial lines to set in the buffer\n    ---@field actions? table<string, snacks.win.Action.spec> Actions that can be used in key mappings\n    ---@field resize? boolean Automatically resize the window when the editor is resized\n    ---@field stack? boolean When enabled, multiple split windows with the same position will be stacked together (useful for terminals)\n    {\n      show = true,\n      fixbuf = true,\n      relative = \"editor\",\n      position = \"float\",\n      minimal = true,\n      wo = {\n        winhighlight = \"Normal:SnacksNormal,NormalNC:SnacksNormalNC,WinBar:SnacksWinBar,WinBarNC:SnacksWinBarNC,FloatTitle:SnacksTitle,FloatFooter:SnacksFooter,WinSeparator:SnacksWinSeparator\",\n      },\n      bo = {},\n      title_pos = \"center\",\n      keys = {\n        q = \"close\",\n      },\n      footer_pos = \"center\",\n      footer_keys = false,\n    }\n<\n\n\n==============================================================================\n4. Styles                                             *snacks.nvim-win-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nFLOAT                                           *snacks.nvim-win-styles-float*\n\n>lua\n    {\n      position = \"float\",\n      backdrop = 60,\n      height = 0.9,\n      width = 0.9,\n      zindex = 50,\n    }\n<\n\n\nHELP                                             *snacks.nvim-win-styles-help*\n\n>lua\n    {\n      position = \"float\",\n      backdrop = false,\n      border = \"top\",\n      row = -1,\n      width = 0,\n      height = 0.3,\n    }\n<\n\n\nMINIMAL                                       *snacks.nvim-win-styles-minimal*\n\n>lua\n    {\n      wo = {\n        cursorcolumn = false,\n        cursorline = false,\n        cursorlineopt = \"both\",\n        colorcolumn = \"\",\n        fillchars = \"eob: ,lastline:…\",\n        foldcolumn = \"0\",\n        list = false,\n        listchars = \"extends:…,tab:  \",\n        number = false,\n        relativenumber = false,\n        signcolumn = \"no\",\n        spell = false,\n        winbar = \"\",\n        statuscolumn = \"\",\n        wrap = false,\n        sidescrolloff = 0,\n      },\n    }\n<\n\n\nSPLIT                                           *snacks.nvim-win-styles-split*\n\n>lua\n    {\n      position = \"bottom\",\n      height = 0.4,\n      width = 0.4,\n    }\n<\n\n\n==============================================================================\n5. Types                                               *snacks.nvim-win-types*\n\n>lua\n    ---@class snacks.win.Keys: vim.api.keyset.keymap\n    ---@field [1]? string\n    ---@field [2]? string|string[]|fun(self: snacks.win): string?\n    ---@field mode? string|string[]\n<\n\n>lua\n    ---@class snacks.win.Event: vim.api.keyset.create_autocmd\n    ---@field buf? true\n    ---@field win? true\n    ---@field callback? fun(self: snacks.win, ev:vim.api.keyset.create_autocmd.callback_args):boolean?\n<\n\n>lua\n    ---@class snacks.win.Backdrop\n    ---@field bg? string\n    ---@field blend? number\n    ---@field transparent? boolean defaults to true\n    ---@field win? snacks.win.Config overrides the backdrop window config\n<\n\n>lua\n    ---@class snacks.win.Dim\n    ---@field width number width of the window, without borders\n    ---@field height number height of the window, without borders\n    ---@field row number row of the window (0-indexed)\n    ---@field col number column of the window (0-indexed)\n    ---@field border? boolean whether the window has a border\n<\n\n>lua\n    ---@alias snacks.win.Action.fn fun(self: snacks.win):(boolean|string?)\n    ---@alias snacks.win.Action.spec snacks.win.Action|snacks.win.Action.fn\n    ---@class snacks.win.Action\n    ---@field action snacks.win.Action.fn\n    ---@field desc? string\n<\n\n\n==============================================================================\n6. Module                                             *snacks.nvim-win-module*\n\n>lua\n    ---@class snacks.win\n    ---@field id number\n    ---@field buf? number\n    ---@field scratch_buf? number\n    ---@field win? number\n    ---@field opts snacks.win.Config\n    ---@field augroup? number\n    ---@field backdrop? snacks.win\n    ---@field keys snacks.win.Keys[]\n    ---@field events (snacks.win.Event|{event:string|string[]})[]\n    ---@field meta table<string, any>\n    ---@field closed? boolean\n    Snacks.win = {}\n<\n\n\n`Snacks.win()`                                                  *Snacks.win()*\n\n>lua\n    ---@type fun(opts? :snacks.win.Config|{}): snacks.win\n    Snacks.win()\n<\n\n\n`Snacks.win.is_border()`                              *Snacks.win.is_border()*\n\n>lua\n    Snacks.win.is_border(border)\n<\n\n\n`Snacks.win.new()`                                          *Snacks.win.new()*\n\n>lua\n    ---@param opts? snacks.win.Config|{}\n    ---@return snacks.win\n    Snacks.win.new(opts)\n<\n\n\n`Snacks.win.zindex()`                                    *Snacks.win.zindex()*\n\nCalculate the next available zindex for snacks windows. New windows open on top\nof existing ones.\n\n>lua\n    ---@param opts? { zindex?: number, tab?: number|boolean, all?: boolean, max?: number }\n    ---@overload fun(zindex: number): number\n    Snacks.win.zindex(opts)\n<\n\n\nWIN:ACTION()                             *snacks.nvim-win-module-win:action()*\n\n>lua\n    ---@param actions string|string[]\n    ---@return (fun(): boolean|string?) action, string? desc\n    win:action(actions)\n<\n\n\nWIN:ADD_PADDING()                   *snacks.nvim-win-module-win:add_padding()*\n\n>lua\n    win:add_padding()\n<\n\n\nWIN:BORDER()                             *snacks.nvim-win-module-win:border()*\n\n>lua\n    win:border()\n<\n\n\nWIN:BORDER_SIZE()                   *snacks.nvim-win-module-win:border_size()*\n\nCalculate the size of the border\n\n>lua\n    win:border_size()\n<\n\n\nWIN:BORDER_TEXT_WIDTH()       *snacks.nvim-win-module-win:border_text_width()*\n\n>lua\n    win:border_text_width()\n<\n\n\nWIN:BUF_VALID()                       *snacks.nvim-win-module-win:buf_valid()*\n\n>lua\n    win:buf_valid()\n<\n\n\nWIN:CLOSE()                               *snacks.nvim-win-module-win:close()*\n\n>lua\n    ---@param opts? { buf: boolean }\n    win:close(opts)\n<\n\n\nWIN:DESTROY()                           *snacks.nvim-win-module-win:destroy()*\n\n>lua\n    win:destroy()\n<\n\n\nWIN:DIM()                                   *snacks.nvim-win-module-win:dim()*\n\n>lua\n    ---@param parent? snacks.win.Dim\n    win:dim(parent)\n<\n\n\nWIN:EXECUTE()                           *snacks.nvim-win-module-win:execute()*\n\n>lua\n    ---@param actions string|string[]\n    win:execute(actions)\n<\n\n\nWIN:FIXBUF()                             *snacks.nvim-win-module-win:fixbuf()*\n\n>lua\n    win:fixbuf()\n<\n\n\nWIN:FOCUS()                               *snacks.nvim-win-module-win:focus()*\n\n>lua\n    win:focus()\n<\n\n\nWIN:HAS_BORDER()                     *snacks.nvim-win-module-win:has_border()*\n\n>lua\n    win:has_border()\n<\n\n\nWIN:HIDE()                                 *snacks.nvim-win-module-win:hide()*\n\n>lua\n    win:hide()\n<\n\n\nWIN:HSCROLL()                           *snacks.nvim-win-module-win:hscroll()*\n\n>lua\n    ---@param left? boolean\n    win:hscroll(left)\n<\n\n\nWIN:IS_FLOATING()                   *snacks.nvim-win-module-win:is_floating()*\n\n>lua\n    win:is_floating()\n<\n\n\nWIN:LINE()                                 *snacks.nvim-win-module-win:line()*\n\n>lua\n    win:line(line)\n<\n\n\nWIN:LINES()                               *snacks.nvim-win-module-win:lines()*\n\n>lua\n    ---@param from? number 1-indexed, inclusive\n    ---@param to? number 1-indexed, inclusive\n    win:lines(from, to)\n<\n\n\nWIN:MAP()                                   *snacks.nvim-win-module-win:map()*\n\n>lua\n    win:map()\n<\n\n\nWIN:ON()                                     *snacks.nvim-win-module-win:on()*\n\n>lua\n    ---@param event string|string[]\n    ---@param cb fun(self: snacks.win, ev:vim.api.keyset.create_autocmd.callback_args):boolean?\n    ---@param opts? snacks.win.Event\n    win:on(event, cb, opts)\n<\n\n\nWIN:ON_CURRENT_TAB()             *snacks.nvim-win-module-win:on_current_tab()*\n\n>lua\n    win:on_current_tab()\n<\n\n\nWIN:ON_RESIZE()                       *snacks.nvim-win-module-win:on_resize()*\n\n>lua\n    win:on_resize()\n<\n\n\nWIN:PARENT_SIZE()                   *snacks.nvim-win-module-win:parent_size()*\n\n>lua\n    ---@return { height: number, width: number }\n    win:parent_size()\n<\n\n\nWIN:REDRAW()                             *snacks.nvim-win-module-win:redraw()*\n\n>lua\n    win:redraw()\n<\n\n\nWIN:SCRATCH()                           *snacks.nvim-win-module-win:scratch()*\n\n>lua\n    win:scratch()\n<\n\n\nWIN:SCROLL()                             *snacks.nvim-win-module-win:scroll()*\n\n>lua\n    ---@param up? boolean\n    win:scroll(up)\n<\n\n\nWIN:SET_BUF()                           *snacks.nvim-win-module-win:set_buf()*\n\n>lua\n    ---@param buf number\n    win:set_buf(buf)\n<\n\n\nWIN:SET_TITLE()                       *snacks.nvim-win-module-win:set_title()*\n\n>lua\n    ---@param title string|{[1]:string, [2]:string}[]\n    ---@param pos? \"center\"|\"left\"|\"right\"\n    win:set_title(title, pos)\n<\n\n\nWIN:SHOW()                                 *snacks.nvim-win-module-win:show()*\n\n>lua\n    win:show()\n<\n\n\nWIN:SIZE()                                 *snacks.nvim-win-module-win:size()*\n\n>lua\n    ---@return { height: number, width: number }\n    win:size()\n<\n\n\nWIN:TEXT()                                 *snacks.nvim-win-module-win:text()*\n\n>lua\n    ---@param from? number 1-indexed, inclusive\n    ---@param to? number 1-indexed, inclusive\n    win:text(from, to)\n<\n\n\nWIN:TOGGLE()                             *snacks.nvim-win-module-win:toggle()*\n\n>lua\n    win:toggle()\n<\n\n\nWIN:TOGGLE_HELP()                   *snacks.nvim-win-module-win:toggle_help()*\n\n>lua\n    ---@param opts? {col_width?: number, key_width?: number, win?: snacks.win.Config}\n    win:toggle_help(opts)\n<\n\n\nWIN:UPDATE()                             *snacks.nvim-win-module-win:update()*\n\n>lua\n    win:update()\n<\n\n\nWIN:VALID()                               *snacks.nvim-win-module-win:valid()*\n\n>lua\n    win:valid()\n<\n\n\nWIN:WIN_VALID()                       *snacks.nvim-win-module-win:win_valid()*\n\n>lua\n    win:win_valid()\n<\n\n==============================================================================\n7. Links                                               *snacks.nvim-win-links*\n\n1. *image*: https://github.com/user-attachments/assets/250acfbd-a624-4f42-a36b-9aab316ebf64\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-words.txt",
    "content": "*snacks-words*                                 snacks words docs\n\n==============================================================================\nTable of Contents                        *snacks.nvim-words-table-of-contents*\n\n1. Setup                                             |snacks.nvim-words-setup|\n2. Config                                           |snacks.nvim-words-config|\n3. Module                                           |snacks.nvim-words-module|\n  - Snacks.words.clear()       |snacks.nvim-words-module-snacks.words.clear()|\n  - Snacks.words.disable()   |snacks.nvim-words-module-snacks.words.disable()|\n  - Snacks.words.enable()     |snacks.nvim-words-module-snacks.words.enable()|\n  - Snacks.words.is_enabled()|snacks.nvim-words-module-snacks.words.is_enabled()|\n  - Snacks.words.jump()         |snacks.nvim-words-module-snacks.words.jump()|\nAuto-show LSP references and quickly navigate between them\n\n\n==============================================================================\n1. Setup                                             *snacks.nvim-words-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        words = {\n          -- your words configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                           *snacks.nvim-words-config*\n\n>lua\n    ---@class snacks.words.Config\n    ---@field enabled? boolean\n    {\n      debounce = 200, -- time in ms to wait before updating\n      notify_jump = false, -- show a notification when jumping\n      notify_end = true, -- show a notification when reaching the end\n      foldopen = true, -- open folds after jumping\n      jumplist = true, -- set jump point before jumping\n      modes = { \"n\", \"i\", \"c\" }, -- modes to show references\n      filter = function(buf) -- what buffers to enable `snacks.words`\n        return vim.g.snacks_words ~= false and vim.b[buf].snacks_words ~= false\n      end,\n    }\n<\n\n\n==============================================================================\n3. Module                                           *snacks.nvim-words-module*\n\n\n`Snacks.words.clear()`                                  *Snacks.words.clear()*\n\n>lua\n    Snacks.words.clear()\n<\n\n\n`Snacks.words.disable()`                              *Snacks.words.disable()*\n\n>lua\n    Snacks.words.disable()\n<\n\n\n`Snacks.words.enable()`                                *Snacks.words.enable()*\n\n>lua\n    Snacks.words.enable()\n<\n\n\n`Snacks.words.is_enabled()`                        *Snacks.words.is_enabled()*\n\n>lua\n    ---@param opts? number|{buf?:number, modes:boolean} if modes is true, also check if the current mode is enabled\n    Snacks.words.is_enabled(opts)\n<\n\n\n`Snacks.words.jump()`                                    *Snacks.words.jump()*\n\n>lua\n    ---@param count? number\n    ---@param cycle? boolean\n    Snacks.words.jump(count, cycle)\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim-zen.txt",
    "content": "*snacks-zen*                                     snacks zen docs\n\n==============================================================================\nTable of Contents                          *snacks.nvim-zen-table-of-contents*\n\n1. Setup                                               |snacks.nvim-zen-setup|\n2. Config                                             |snacks.nvim-zen-config|\n3. Styles                                             |snacks.nvim-zen-styles|\n  - zen                                           |snacks.nvim-zen-styles-zen|\n  - zoom_indicator                     |snacks.nvim-zen-styles-zoom_indicator|\n4. Module                                             |snacks.nvim-zen-module|\n  - Snacks.zen()                         |snacks.nvim-zen-module-snacks.zen()|\n  - Snacks.zen.zen()                 |snacks.nvim-zen-module-snacks.zen.zen()|\n  - Snacks.zen.zoom()               |snacks.nvim-zen-module-snacks.zen.zoom()|\n5. Links                                               |snacks.nvim-zen-links|\nZen mode • distraction-free coding. Integrates with `Snacks.toggle` to toggle\nvarious UI elements and with `Snacks.dim` to dim code out of scope.\n\nSimilar plugins:\n\n- zen-mode.nvim <https://github.com/folke/zen-mode.nvim>\n- true-zen.nvim <https://github.com/pocco81/true-zen.nvim>\n\n\n==============================================================================\n1. Setup                                               *snacks.nvim-zen-setup*\n\n>lua\n    -- lazy.nvim\n    {\n      \"folke/snacks.nvim\",\n      ---@type snacks.Config\n      opts = {\n        zen = {\n          -- your zen configuration comes here\n          -- or leave it empty to use the default settings\n          -- refer to the configuration section below\n        }\n      }\n    }\n<\n\n\n==============================================================================\n2. Config                                             *snacks.nvim-zen-config*\n\n>lua\n    ---@class snacks.zen.Config\n    {\n      -- You can add any `Snacks.toggle` id here.\n      -- Toggle state is restored when the window is closed.\n      -- Toggle config options are NOT merged.\n      ---@type table<string, boolean>\n      toggles = {\n        dim = true,\n        git_signs = false,\n        mini_diff_signs = false,\n        -- diagnostics = false,\n        -- inlay_hints = false,\n      },\n      center = true, -- center the window\n      show = {\n        statusline = false, -- can only be shown when using the global statusline\n        tabline = false,\n      },\n      ---@type snacks.win.Config\n      win = { style = \"zen\" },\n      --- Callback when the window is opened.\n      ---@param win snacks.win\n      on_open = function(win) end,\n      --- Callback when the window is closed.\n      ---@param win snacks.win\n      on_close = function(win) end,\n      --- Options for the `Snacks.zen.zoom()`\n      ---@type snacks.zen.Config\n      zoom = {\n        toggles = {},\n        center = false,\n        show = { statusline = true, tabline = true },\n        win = {\n          backdrop = false,\n          width = 0, -- full width\n        },\n      },\n    }\n<\n\n\n==============================================================================\n3. Styles                                             *snacks.nvim-zen-styles*\n\nCheck the styles\n<https://github.com/folke/snacks.nvim/blob/main/docs/styles.md> docs for more\ninformation on how to customize these styles\n\n\nZEN                                               *snacks.nvim-zen-styles-zen*\n\n>lua\n    {\n      enter = true,\n      fixbuf = false,\n      minimal = false,\n      width = 120,\n      height = 0,\n      backdrop = { transparent = true, blend = 40 },\n      keys = { q = false },\n      zindex = 40,\n      wo = {\n        winhighlight = \"NormalFloat:Normal\",\n      },\n      w = {\n        snacks_main = true,\n      },\n    }\n<\n\n\nZOOM_INDICATOR                         *snacks.nvim-zen-styles-zoom_indicator*\n\nfullscreen indicator only shown when the window is maximized\n\n>lua\n    {\n      text = \"▍ zoom  󰊓  \",\n      minimal = true,\n      enter = false,\n      focusable = false,\n      height = 1,\n      row = 0,\n      col = -1,\n      backdrop = false,\n    }\n<\n\n\n==============================================================================\n4. Module                                             *snacks.nvim-zen-module*\n\n\n`Snacks.zen()`                                                  *Snacks.zen()*\n\n>lua\n    ---@type fun(opts: snacks.zen.Config): snacks.win\n    Snacks.zen()\n<\n\n\n`Snacks.zen.zen()`                                          *Snacks.zen.zen()*\n\n>lua\n    ---@param opts? snacks.zen.Config\n    Snacks.zen.zen(opts)\n<\n\n\n`Snacks.zen.zoom()`                                        *Snacks.zen.zoom()*\n\n>lua\n    ---@param opts? snacks.zen.Config\n    Snacks.zen.zoom(opts)\n<\n\n==============================================================================\n5. Links                                               *snacks.nvim-zen-links*\n\n1. *image*: https://github.com/user-attachments/assets/77c607ec-c354-4e17-bcd1-fdcd4b4c0057\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/snacks.nvim.txt",
    "content": "*snacks*                                             snacks docs\n\n==============================================================================\nTable of Contents                              *snacks.nvim-table-of-contents*\n\n1. snacks.nvim                                       |snacks.nvim-snacks.nvim|\n  - Features                                |snacks.nvim-snacks.nvim-features|\n  - Requirements                        |snacks.nvim-snacks.nvim-requirements|\n  - Installation                        |snacks.nvim-snacks.nvim-installation|\n  - Configuration                      |snacks.nvim-snacks.nvim-configuration|\n  - Usage                                      |snacks.nvim-snacks.nvim-usage|\n  - Highlight Groups                |snacks.nvim-snacks.nvim-highlight-groups|\n\n==============================================================================\n1. snacks.nvim                                       *snacks.nvim-snacks.nvim*\n\nA collection of small QoL plugins for Neovim.\n\n\nFEATURES                                    *snacks.nvim-snacks.nvim-features*\n\n  -----------------------------------------------------------------------\n  Snack            Description                                Setup\n  ---------------- ------------------------------------- ----------------\n  animate          Efficient animations including over   \n                   45 easing functions (library)         \n\n  bigfile          Deal with big files                   \n\n  bufdelete        Deletebuffers without disrupting      \n                   window layout                         \n\n  dashboard        Beautiful declarative dashboards      \n\n  debug            Prettyinspect & backtraces for        \n                   debugging                             \n\n  dim              Focus on the active scope by dimming  \n                   the rest                              \n\n  explorer         A file explorer (picker in disguise)  \n\n  gh               GitHubCLI integration                 \n\n  git              Git utilities                         \n\n  gitbrowse        Open the current file, branch,        \n                   commit, or repo in a browser          \n                   (e.g. GitHub, GitLab, Bitbucket)      \n\n  image            Image viewer using Kitty Graphics     \n                   Protocol, supported by kitty, wezterm \n                   and ghostty                           \n\n  indent           Indentguides and scopes               \n\n  input            Better vim.ui.input                   \n\n  keymap           Bettervim.keymap with support for     \n                   filetypes and LSP clients             \n\n  layout           Window layouts                        \n\n  lazygit          Open LazyGit in a float,              \n                   auto-configure colorscheme and        \n                   integration with Neovim               \n\n  notifier         Pretty vim.notify                     \n\n  notify           Utilityfunctions to work with         \n                   Neovim’s vim.notify                   \n\n  picker           Picker for selecting items            \n\n  profiler         Neovimlua profiler                    \n\n  quickfile        When doing nvim somefile.txt, it will \n                   render the file as quickly as         \n                   possible, before loading your         \n                   plugins.                              \n\n  rename           LSP-integratedfile renaming with      \n                   support for plugins like              \n                   neo-tree.nvim and mini.files.         \n\n  scope            Scope detection, text objects and     \n                   jumping based on treesitter or indent \n\n  scratch          Scratchbuffers with a persistent file \n\n  scroll           Smooth scrolling                      \n\n  statuscolumn     Prettystatus column                   \n\n  terminal         Createand toggle floating/split       \n                   terminals                             \n\n  toggle           Toggle keymaps integrated with        \n                   which-key icons / colors              \n\n  util             Utility functions for Snacks          \n                   (library)                             \n\n  win              Create and manage floating windows or \n                   splits                                \n\n  words            Auto-show LSP references and quickly  \n                   navigate between them                 \n\n  zen              Zenmode • distraction-free coding     \n  -----------------------------------------------------------------------\n\nREQUIREMENTS                            *snacks.nvim-snacks.nvim-requirements*\n\n- **Neovim** >= 0.9.4\n- for proper icons support:\n    - mini.icons <https://github.com/nvim-mini/mini.icons> _(optional)_\n    - nvim-web-devicons <https://github.com/nvim-tree/nvim-web-devicons> _(optional)_\n    - a Nerd Font <https://www.nerdfonts.com/> **(optional)**\n\n\nINSTALLATION                            *snacks.nvim-snacks.nvim-installation*\n\nInstall the plugin with your package manager:\n\n\nLAZY.NVIM ~\n\n\n  [!important] A couple of plugins **require** `snacks.nvim` to be set-up early.\n  Setup creates some autocmds and does not load any plugins. Check the code\n  <https://github.com/folke/snacks.nvim/blob/main/lua/snacks/init.lua> to see\n  what it does.\n\n  [!caution] You need to explicitly pass options for a plugin or set `enabled =\n  true` to enable it.\n\n  [!tip] It’s a good idea to run `:checkhealth snacks` to see if everything is\n  set up correctly.\n>lua\n    {\n      \"folke/snacks.nvim\",\n      priority = 1000,\n      lazy = false,\n      ---@type snacks.Config\n      opts = {\n        -- your configuration comes here\n        -- or leave it empty to use the default settings\n        -- refer to the configuration section below\n        bigfile = { enabled = true },\n        dashboard = { enabled = true },\n        explorer = { enabled = true },\n        indent = { enabled = true },\n        input = { enabled = true },\n        picker = { enabled = true },\n        notifier = { enabled = true },\n        quickfile = { enabled = true },\n        scope = { enabled = true },\n        scroll = { enabled = true },\n        statuscolumn = { enabled = true },\n        words = { enabled = true },\n      },\n    }\n<\n\nFor an in-depth setup of `snacks.nvim` with `lazy.nvim`, check the example\n<https://github.com/folke/snacks.nvim?tab=readme-ov-file#-usage> below.\n\n\nCONFIGURATION                          *snacks.nvim-snacks.nvim-configuration*\n\nPlease refer to the readme of each plugin for their specific configuration.\n\nDefault Options ~\n\n>lua\n    ---@class snacks.Config\n    ---@field animate? snacks.animate.Config\n    ---@field bigfile? snacks.bigfile.Config\n    ---@field dashboard? snacks.dashboard.Config\n    ---@field dim? snacks.dim.Config\n    ---@field explorer? snacks.explorer.Config\n    ---@field gh? snacks.gh.Config\n    ---@field gitbrowse? snacks.gitbrowse.Config\n    ---@field image? snacks.image.Config\n    ---@field indent? snacks.indent.Config\n    ---@field input? snacks.input.Config\n    ---@field layout? snacks.layout.Config\n    ---@field lazygit? snacks.lazygit.Config\n    ---@field notifier? snacks.notifier.Config\n    ---@field picker? snacks.picker.Config\n    ---@field profiler? snacks.profiler.Config\n    ---@field quickfile? snacks.quickfile.Config\n    ---@field scope? snacks.scope.Config\n    ---@field scratch? snacks.scratch.Config\n    ---@field scroll? snacks.scroll.Config\n    ---@field statuscolumn? snacks.statuscolumn.Config\n    ---@field terminal? snacks.terminal.Config\n    ---@field toggle? snacks.toggle.Config\n    ---@field win? snacks.win.Config\n    ---@field words? snacks.words.Config\n    ---@field zen? snacks.zen.Config\n    ---@field styles? table<string, snacks.win.Config>\n    ---@field image? snacks.image.Config|{}\n    {\n      image = {\n        -- define these here, so that we don't need to load the image module\n        formats = {\n          \"png\",\n          \"jpg\",\n          \"jpeg\",\n          \"gif\",\n          \"bmp\",\n          \"webp\",\n          \"tiff\",\n          \"heic\",\n          \"avif\",\n          \"mp4\",\n          \"mov\",\n          \"avi\",\n          \"mkv\",\n          \"webm\",\n          \"pdf\",\n          \"icns\",\n        },\n      },\n    }\n<\n\nSome plugins have examples in their documentation. You can include them in your\nconfig like this:\n\n>lua\n    {\n      dashboard = { example = \"github\" }\n    }\n<\n\nIf you want to customize options for a plugin after they have been resolved,\nyou can use the `config` function:\n\n>lua\n    {\n      gitbrowse = {\n        config = function(opts, defaults)\n          table.insert(opts.remote_patterns, { \"my\", \"custom pattern\" })\n        end\n      },\n    }\n<\n\n\nUSAGE                                          *snacks.nvim-snacks.nvim-usage*\n\nSee the example below for how to configure `snacks.nvim`.\n\n>lua\n    {\n      \"folke/snacks.nvim\",\n      priority = 1000,\n      lazy = false,\n      ---@type snacks.Config\n      opts = {\n        bigfile = { enabled = true },\n        dashboard = { enabled = true },\n        explorer = { enabled = true },\n        indent = { enabled = true },\n        input = { enabled = true },\n        notifier = {\n          enabled = true,\n          timeout = 3000,\n        },\n        picker = { enabled = true },\n        quickfile = { enabled = true },\n        scope = { enabled = true },\n        scroll = { enabled = true },\n        statuscolumn = { enabled = true },\n        words = { enabled = true },\n        styles = {\n          notification = {\n            -- wo = { wrap = true } -- Wrap notifications\n          }\n        }\n      },\n      keys = {\n        -- Top Pickers & Explorer\n        { \"<leader><space>\", function() Snacks.picker.smart() end, desc = \"Smart Find Files\" },\n        { \"<leader>,\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n        { \"<leader>/\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n        { \"<leader>:\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n        { \"<leader>n\", function() Snacks.picker.notifications() end, desc = \"Notification History\" },\n        { \"<leader>e\", function() Snacks.explorer() end, desc = \"File Explorer\" },\n        -- find\n        { \"<leader>fb\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n        { \"<leader>fc\", function() Snacks.picker.files({ cwd = vim.fn.stdpath(\"config\") }) end, desc = \"Find Config File\" },\n        { \"<leader>ff\", function() Snacks.picker.files() end, desc = \"Find Files\" },\n        { \"<leader>fg\", function() Snacks.picker.git_files() end, desc = \"Find Git Files\" },\n        { \"<leader>fp\", function() Snacks.picker.projects() end, desc = \"Projects\" },\n        { \"<leader>fr\", function() Snacks.picker.recent() end, desc = \"Recent\" },\n        -- git\n        { \"<leader>gb\", function() Snacks.picker.git_branches() end, desc = \"Git Branches\" },\n        { \"<leader>gl\", function() Snacks.picker.git_log() end, desc = \"Git Log\" },\n        { \"<leader>gL\", function() Snacks.picker.git_log_line() end, desc = \"Git Log Line\" },\n        { \"<leader>gs\", function() Snacks.picker.git_status() end, desc = \"Git Status\" },\n        { \"<leader>gS\", function() Snacks.picker.git_stash() end, desc = \"Git Stash\" },\n        { \"<leader>gd\", function() Snacks.picker.git_diff() end, desc = \"Git Diff (Hunks)\" },\n        { \"<leader>gf\", function() Snacks.picker.git_log_file() end, desc = \"Git Log File\" },\n        -- gh\n        { \"<leader>gi\", function() Snacks.picker.gh_issue() end, desc = \"GitHub Issues (open)\" },\n        { \"<leader>gI\", function() Snacks.picker.gh_issue({ state = \"all\" }) end, desc = \"GitHub Issues (all)\" },\n        { \"<leader>gp\", function() Snacks.picker.gh_pr() end, desc = \"GitHub Pull Requests (open)\" },\n        { \"<leader>gP\", function() Snacks.picker.gh_pr({ state = \"all\" }) end, desc = \"GitHub Pull Requests (all)\" },\n        -- Grep\n        { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n        { \"<leader>sB\", function() Snacks.picker.grep_buffers() end, desc = \"Grep Open Buffers\" },\n        { \"<leader>sg\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n        { \"<leader>sw\", function() Snacks.picker.grep_word() end, desc = \"Visual selection or word\", mode = { \"n\", \"x\" } },\n        -- search\n        { '<leader>s\"', function() Snacks.picker.registers() end, desc = \"Registers\" },\n        { '<leader>s/', function() Snacks.picker.search_history() end, desc = \"Search History\" },\n        { \"<leader>sa\", function() Snacks.picker.autocmds() end, desc = \"Autocmds\" },\n        { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n        { \"<leader>sc\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n        { \"<leader>sC\", function() Snacks.picker.commands() end, desc = \"Commands\" },\n        { \"<leader>sd\", function() Snacks.picker.diagnostics() end, desc = \"Diagnostics\" },\n        { \"<leader>sD\", function() Snacks.picker.diagnostics_buffer() end, desc = \"Buffer Diagnostics\" },\n        { \"<leader>sh\", function() Snacks.picker.help() end, desc = \"Help Pages\" },\n        { \"<leader>sH\", function() Snacks.picker.highlights() end, desc = \"Highlights\" },\n        { \"<leader>si\", function() Snacks.picker.icons() end, desc = \"Icons\" },\n        { \"<leader>sj\", function() Snacks.picker.jumps() end, desc = \"Jumps\" },\n        { \"<leader>sk\", function() Snacks.picker.keymaps() end, desc = \"Keymaps\" },\n        { \"<leader>sl\", function() Snacks.picker.loclist() end, desc = \"Location List\" },\n        { \"<leader>sm\", function() Snacks.picker.marks() end, desc = \"Marks\" },\n        { \"<leader>sM\", function() Snacks.picker.man() end, desc = \"Man Pages\" },\n        { \"<leader>sp\", function() Snacks.picker.lazy() end, desc = \"Search for Plugin Spec\" },\n        { \"<leader>sq\", function() Snacks.picker.qflist() end, desc = \"Quickfix List\" },\n        { \"<leader>sR\", function() Snacks.picker.resume() end, desc = \"Resume\" },\n        { \"<leader>su\", function() Snacks.picker.undo() end, desc = \"Undo History\" },\n        { \"<leader>uC\", function() Snacks.picker.colorschemes() end, desc = \"Colorschemes\" },\n        -- LSP\n        { \"gd\", function() Snacks.picker.lsp_definitions() end, desc = \"Goto Definition\" },\n        { \"gD\", function() Snacks.picker.lsp_declarations() end, desc = \"Goto Declaration\" },\n        { \"gr\", function() Snacks.picker.lsp_references() end, nowait = true, desc = \"References\" },\n        { \"gI\", function() Snacks.picker.lsp_implementations() end, desc = \"Goto Implementation\" },\n        { \"gy\", function() Snacks.picker.lsp_type_definitions() end, desc = \"Goto T[y]pe Definition\" },\n        { \"gai\", function() Snacks.picker.lsp_incoming_calls() end, desc = \"C[a]lls Incoming\" },\n        { \"gao\", function() Snacks.picker.lsp_outgoing_calls() end, desc = \"C[a]lls Outgoing\" },\n        { \"<leader>ss\", function() Snacks.picker.lsp_symbols() end, desc = \"LSP Symbols\" },\n        { \"<leader>sS\", function() Snacks.picker.lsp_workspace_symbols() end, desc = \"LSP Workspace Symbols\" },\n        -- Other\n        { \"<leader>z\",  function() Snacks.zen() end, desc = \"Toggle Zen Mode\" },\n        { \"<leader>Z\",  function() Snacks.zen.zoom() end, desc = \"Toggle Zoom\" },\n        { \"<leader>.\",  function() Snacks.scratch() end, desc = \"Toggle Scratch Buffer\" },\n        { \"<leader>S\",  function() Snacks.scratch.select() end, desc = \"Select Scratch Buffer\" },\n        { \"<leader>n\",  function() Snacks.notifier.show_history() end, desc = \"Notification History\" },\n        { \"<leader>bd\", function() Snacks.bufdelete() end, desc = \"Delete Buffer\" },\n        { \"<leader>cR\", function() Snacks.rename.rename_file() end, desc = \"Rename File\" },\n        { \"<leader>gB\", function() Snacks.gitbrowse() end, desc = \"Git Browse\", mode = { \"n\", \"v\" } },\n        { \"<leader>gg\", function() Snacks.lazygit() end, desc = \"Lazygit\" },\n        { \"<leader>un\", function() Snacks.notifier.hide() end, desc = \"Dismiss All Notifications\" },\n        { \"<c-/>\",      function() Snacks.terminal() end, desc = \"Toggle Terminal\" },\n        { \"<c-_>\",      function() Snacks.terminal() end, desc = \"which_key_ignore\" },\n        { \"]]\",         function() Snacks.words.jump(vim.v.count1) end, desc = \"Next Reference\", mode = { \"n\", \"t\" } },\n        { \"[[\",         function() Snacks.words.jump(-vim.v.count1) end, desc = \"Prev Reference\", mode = { \"n\", \"t\" } },\n        {\n          \"<leader>N\",\n          desc = \"Neovim News\",\n          function()\n            Snacks.win({\n              file = vim.api.nvim_get_runtime_file(\"doc/news.txt\", false)[1],\n              width = 0.6,\n              height = 0.6,\n              wo = {\n                spell = false,\n                wrap = false,\n                signcolumn = \"yes\",\n                statuscolumn = \" \",\n                conceallevel = 3,\n              },\n            })\n          end,\n        }\n      },\n      init = function()\n        vim.api.nvim_create_autocmd(\"User\", {\n          pattern = \"VeryLazy\",\n          callback = function()\n            -- Setup some globals for debugging (lazy-loaded)\n            _G.dd = function(...)\n              Snacks.debug.inspect(...)\n            end\n            _G.bt = function()\n              Snacks.debug.backtrace()\n            end\n    \n            -- Override print to use snacks for `:=` command\n            if vim.fn.has(\"nvim-0.11\") == 1 then\n              vim._print = function(_, ...)\n                dd(...)\n              end\n            else\n              vim.print = _G.dd \n            end\n    \n            -- Create some toggle mappings\n            Snacks.toggle.option(\"spell\", { name = \"Spelling\" }):map(\"<leader>us\")\n            Snacks.toggle.option(\"wrap\", { name = \"Wrap\" }):map(\"<leader>uw\")\n            Snacks.toggle.option(\"relativenumber\", { name = \"Relative Number\" }):map(\"<leader>uL\")\n            Snacks.toggle.diagnostics():map(\"<leader>ud\")\n            Snacks.toggle.line_number():map(\"<leader>ul\")\n            Snacks.toggle.option(\"conceallevel\", { off = 0, on = vim.o.conceallevel > 0 and vim.o.conceallevel or 2 }):map(\"<leader>uc\")\n            Snacks.toggle.treesitter():map(\"<leader>uT\")\n            Snacks.toggle.option(\"background\", { off = \"light\", on = \"dark\", name = \"Dark Background\" }):map(\"<leader>ub\")\n            Snacks.toggle.inlay_hints():map(\"<leader>uh\")\n            Snacks.toggle.indent():map(\"<leader>ug\")\n            Snacks.toggle.dim():map(\"<leader>uD\")\n          end,\n        })\n      end,\n    }\n<\n\n\nHIGHLIGHT GROUPS                    *snacks.nvim-snacks.nvim-highlight-groups*\n\nSnacks defines **a lot** of highlight groups and it’s impossible to document\nthem all.\n\nInstead, you can use the picker to see all the highlight groups.\n\n>lua\n    Snacks.picker.highlights({pattern = \"hl_group:^Snacks\"})\n<\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "docs/animate.md",
    "content": "# 🍿 animate\n\nEfficient animation library including over 45 easing functions:\n\n- [Emmanuel Oga's easing functions](https://github.com/EmmanuelOga/easing)\n- [Easing functions overview](https://github.com/kikito/tween.lua?tab=readme-ov-file#easing-functions)\n\nThere's at any given time at most one timer running, that takes\ncare of all active animations, controlled by the `fps` setting.\n\nYou can at any time disable all animations with:\n\n- `vim.g.snacks_animate = false` globally\n- `vim.b.snacks_animate = false` locally for the buffer\n\nDoing this, will disable `scroll`, `indent`, `dim` and all other animations.\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    animate = {\n      -- your animate configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.animate.Config\n---@field easing? snacks.animate.easing|snacks.animate.easing.Fn\n{\n  ---@type snacks.animate.Duration|number\n  duration = 20, -- ms per step\n  easing = \"linear\",\n  fps = 120, -- frames per second. Global setting for all animations\n}\n```\n\n## 📚 Types\n\nAll easing functions take these parameters:\n\n* `t` _(time)_: should go from 0 to duration\n* `b` _(begin)_: starting value of the property\n* `c` _(change)_: ending value of the property - starting value\n* `d` _(duration)_: total duration of the animation\n\nSome functions allow additional modifiers, like the elastic functions\nwhich also can receive an amplitud and a period parameters (defaults\nare included)\n\n```lua\n---@alias snacks.animate.easing.Fn fun(t: number, b: number, c: number, d: number): number\n```\n\nDuration can be specified as the total duration or the duration per step.\nWhen both are specified, the minimum of both is used.\n\n```lua\n---@class snacks.animate.Duration\n---@field step? number duration per step in ms\n---@field total? number total duration in ms\n```\n\n```lua\n---@class snacks.animate.Opts: snacks.animate.Config\n---@field buf? number optional buffer to check if animations should be enabled\n---@field int? boolean interpolate the value to an integer\n---@field id? number|string unique identifier for the animation\n```\n\n```lua\n---@class snacks.animate.ctx\n---@field anim snacks.animate.Animation\n---@field prev number\n---@field done boolean\n```\n\n```lua\n---@alias snacks.animate.cb fun(value:number, ctx: snacks.animate.ctx)\n```\n\n## 📦 Module\n\n### `Snacks.animate()`\n\n```lua\n---@type fun(from: number, to: number, cb: snacks.animate.cb, opts?: snacks.animate.Opts): snacks.animate.Animation\nSnacks.animate()\n```\n\n### `Snacks.animate.add()`\n\nAdd an animation\n\n```lua\n---@param from number\n---@param to number\n---@param cb snacks.animate.cb\n---@param opts? snacks.animate.Opts\nSnacks.animate.add(from, to, cb, opts)\n```\n\n### `Snacks.animate.del()`\n\nDelete an animation\n\n```lua\n---@param id number|string\nSnacks.animate.del(id)\n```\n\n### `Snacks.animate.enabled()`\n\nCheck if animations are enabled.\nWill return false if `snacks_animate` is set to false or if the buffer\nlocal variable `snacks_animate` is set to false.\n\n```lua\n---@param opts? {buf?: number, name?: string}\nSnacks.animate.enabled(opts)\n```\n"
  },
  {
    "path": "docs/bigfile.md",
    "content": "# 🍿 bigfile\n\n`bigfile` adds a new filetype `bigfile` to Neovim that triggers when the file is\nlarger than the configured size. This automatically prevents things like LSP\nand Treesitter attaching to the buffer.\n\nUse the `setup` config function to further make changes to a `bigfile` buffer.\nThe context provides the actual filetype.\n\nThe default implementation enables `syntax` for the buffer and disables\n[mini.animate](https://github.com/nvim-mini/mini.animate) (if used)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    bigfile = {\n      -- your bigfile configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.bigfile.Config\n---@field enabled? boolean\n{\n  notify = true, -- show notification when big file detected\n  size = 1.5 * 1024 * 1024, -- 1.5MB\n  line_length = 1000, -- average line length (useful for minified files)\n  -- Enable or disable features when big file detected\n  ---@param ctx {buf: number, ft:string}\n  setup = function(ctx)\n    if vim.fn.exists(\":NoMatchParen\") ~= 0 then\n      vim.cmd([[NoMatchParen]])\n    end\n    Snacks.util.wo(0, { foldmethod = \"manual\", statuscolumn = \"\", conceallevel = 0 })\n    vim.b.completion = false\n    vim.b.minianimate_disable = true\n    vim.b.minihipatterns_disable = true\n    vim.schedule(function()\n      if vim.api.nvim_buf_is_valid(ctx.buf) then\n        vim.bo[ctx.buf].syntax = ctx.ft\n      end\n    end)\n  end,\n}\n```\n"
  },
  {
    "path": "docs/bufdelete.md",
    "content": "# 🍿 bufdelete\n\nDelete buffers without disrupting window layout.\n\nIf the buffer you want to close has changes,\na prompt will be shown to save or discard.\n\n<!-- docgen -->\n\n## 📚 Types\n\n```lua\n---@class snacks.bufdelete.Opts\n---@field buf? number Buffer to delete. Defaults to the current buffer\n---@field file? string Delete buffer by file name. If provided, `buf` is ignored\n---@field force? boolean Delete the buffer even if it is modified\n---@field filter? fun(buf: number): boolean Filter buffers to delete\n---@field wipe? boolean Wipe the buffer instead of deleting it (see `:h :bwipeout`)\n```\n\n## 📦 Module\n\n### `Snacks.bufdelete()`\n\n```lua\n---@type fun(buf?: number|snacks.bufdelete.Opts)\nSnacks.bufdelete()\n```\n\n### `Snacks.bufdelete.all()`\n\nDelete all buffers\n\n```lua\n---@param opts? snacks.bufdelete.Opts\nSnacks.bufdelete.all(opts)\n```\n\n### `Snacks.bufdelete.delete()`\n\nDelete a buffer:\n- either the current buffer if `buf` is not provided\n- or the buffer `buf` if it is a number\n- or every buffer for which `buf` returns true if it is a function\n\n```lua\n---@param opts? number|snacks.bufdelete.Opts\nSnacks.bufdelete.delete(opts)\n```\n\n### `Snacks.bufdelete.other()`\n\nDelete all buffers except the current one\n\n```lua\n---@param opts? snacks.bufdelete.Opts\nSnacks.bufdelete.other(opts)\n```\n"
  },
  {
    "path": "docs/dashboard.md",
    "content": "# 🍿 dashboard\n\n## ✨ Features\n\n- declarative configuration\n- flexible layouts\n- multiple vertical panes\n- built-in sections:\n  - **header**: show a header\n  - **keys**: show keymaps\n  - **projects**: show recent projects\n  - **recent_files**: show recent files\n  - **session**: session support\n  - **startup**: startup time (lazy.nvim)\n  - **terminal**: colored terminal output\n- super fast `terminal` sections with automatic caching\n\n## 🚀 Usage\n\nThe dashboard comes with a set of default sections, that\ncan be customized with `opts.preset` or\nfully replaced with `opts.sections`.\n\nThe default preset comes with support for:\n\n- pickers:\n  - [fzf-lua](https://github.com/ibhagwan/fzf-lua)\n  - [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)\n  - [mini.pick](https://github.com/nvim-mini/mini.pick)\n- session managers: (only works with [lazy.nvim](https://github.com/folke/lazy.nvim))\n  - [persistence.nvim](https://github.com/folke/persistence.nvim)\n  - [persisted.nvim](https://github.com/olimorris/persisted.nvim)\n  - [neovim-session-manager](https://github.com/Shatur/neovim-session-manager)\n  - [posession.nvim](https://github.com/jedrzejboczar/possession.nvim)\n  - [mini.sessions](https://github.com/nvim-mini/mini.sessions)\n\n### Section actions\n\nA section can have an `action` property that will be executed as:\n\n- a command if it starts with `:`\n- a keymap if it's a string not starting with `:`\n- a function if it's a function\n\n```lua\n-- command\n{\n  action = \":Telescope find_files\",\n  key = \"f\",\n},\n```\n\n```lua\n-- keymap\n{\n  action = \"<leader>ff\",\n  key = \"f\",\n},\n```\n\n```lua\n-- function\n{\n  action = function()\n    require(\"telescope.builtin\").find_files()\n  end,\n  key = \"h\",\n},\n```\n\n### Item text\n\nEvery item should have a `text` property with an array of `snacks.dashboard.Text` objects.\nIf the `text` property is not provided, the `snacks.dashboard.Config.formats`\nwill be used to generate the text.\n\nIn the example below, both sections are equivalent.\n\n```lua\n{\n  text = {\n    { \"  \", hl = \"SnacksDashboardIcon\" },\n    { \"Find File\", hl = \"SnacksDashboardDesc\", width = 50 },\n    { \"[f]\", hl = \"SnacksDashboardKey\" },\n  },\n  action = \":Telescope find_files\",\n  key = \"f\",\n},\n```\n\n```lua\n{\n  action = \":Telescope find_files\",\n  key = \"f\",\n  desc = \"Find File\",\n  icon = \" \",\n},\n```\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    dashboard = {\n      -- your dashboard configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.dashboard.Config\n---@field enabled? boolean\n---@field sections snacks.dashboard.Section\n---@field formats table<string, snacks.dashboard.Text|fun(item:snacks.dashboard.Item, ctx:snacks.dashboard.Format.ctx):snacks.dashboard.Text>\n{\n  width = 60,\n  row = nil, -- dashboard position. nil for center\n  col = nil, -- dashboard position. nil for center\n  pane_gap = 4, -- empty columns between vertical panes\n  autokeys = \"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\", -- autokey sequence\n  -- These settings are used by some built-in sections\n  preset = {\n    -- Defaults to a picker that supports `fzf-lua`, `telescope.nvim` and `mini.pick`\n    ---@type fun(cmd:string, opts:table)|nil\n    pick = nil,\n    -- Used by the `keys` section to show keymaps.\n    -- Set your custom keymaps here.\n    -- When using a function, the `items` argument are the default keymaps.\n    ---@type snacks.dashboard.Item[]\n    keys = {\n      { icon = \" \", key = \"f\", desc = \"Find File\", action = \":lua Snacks.dashboard.pick('files')\" },\n      { icon = \" \", key = \"n\", desc = \"New File\", action = \":ene | startinsert\" },\n      { icon = \" \", key = \"g\", desc = \"Find Text\", action = \":lua Snacks.dashboard.pick('live_grep')\" },\n      { icon = \" \", key = \"r\", desc = \"Recent Files\", action = \":lua Snacks.dashboard.pick('oldfiles')\" },\n      { icon = \" \", key = \"c\", desc = \"Config\", action = \":lua Snacks.dashboard.pick('files', {cwd = vim.fn.stdpath('config')})\" },\n      { icon = \" \", key = \"s\", desc = \"Restore Session\", section = \"session\" },\n      { icon = \"󰒲 \", key = \"L\", desc = \"Lazy\", action = \":Lazy\", enabled = package.loaded.lazy ~= nil },\n      { icon = \" \", key = \"q\", desc = \"Quit\", action = \":qa\" },\n    },\n    -- Used by the `header` section\n    header = [[\n███╗   ██╗███████╗ ██████╗ ██╗   ██╗██╗███╗   ███╗\n████╗  ██║██╔════╝██╔═══██╗██║   ██║██║████╗ ████║\n██╔██╗ ██║█████╗  ██║   ██║██║   ██║██║██╔████╔██║\n██║╚██╗██║██╔══╝  ██║   ██║╚██╗ ██╔╝██║██║╚██╔╝██║\n██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║\n╚═╝  ╚═══╝╚══════╝ ╚═════╝   ╚═══╝  ╚═╝╚═╝     ╚═╝]],\n  },\n  -- item field formatters\n  formats = {\n    icon = function(item)\n      if item.file and item.icon == \"file\" or item.icon == \"directory\" then\n        return Snacks.dashboard.icon(item.file, item.icon)\n      end\n      return { item.icon, width = 2, hl = \"icon\" }\n    end,\n    footer = { \"%s\", align = \"center\" },\n    header = { \"%s\", align = \"center\" },\n    file = function(item, ctx)\n      local fname = vim.fn.fnamemodify(item.file, \":~\")\n      fname = ctx.width and #fname > ctx.width and vim.fn.pathshorten(fname) or fname\n      if #fname > ctx.width then\n        local dir = vim.fn.fnamemodify(fname, \":h\")\n        local file = vim.fn.fnamemodify(fname, \":t\")\n        if dir and file then\n          file = file:sub(-(ctx.width - #dir - 2))\n          fname = dir .. \"/…\" .. file\n        end\n      end\n      local dir, file = fname:match(\"^(.*)/(.+)$\")\n      return dir and { { dir .. \"/\", hl = \"dir\" }, { file, hl = \"file\" } } or { { fname, hl = \"file\" } }\n    end,\n  },\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { section = \"startup\" },\n  },\n}\n```\n\n## 🚀 Examples\n\n### `advanced`\n\nA more advanced example using multiple panes\n![image](https://github.com/user-attachments/assets/bbf4d2cd-6fc5-4122-a462-0ca59ba89545)\n\n```lua\n{\n  sections = {\n    { section = \"header\" },\n    {\n      pane = 2,\n      section = \"terminal\",\n      cmd = \"colorscript -e square\",\n      height = 5,\n      padding = 1,\n    },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { pane = 2, icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = 1 },\n    { pane = 2, icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 1 },\n    {\n      pane = 2,\n      icon = \" \",\n      title = \"Git Status\",\n      section = \"terminal\",\n      enabled = function()\n        return Snacks.git.get_root() ~= nil\n      end,\n      cmd = \"git status --short --branch --renames\",\n      height = 5,\n      padding = 1,\n      ttl = 5 * 60,\n      indent = 3,\n    },\n    { section = \"startup\" },\n  },\n}\n```\n\n### `chafa`\n\nAn example using the `chafa` command to display an image\n![image](https://github.com/user-attachments/assets/e498ef8f-83ce-4917-a720-8cb31d98ecec)\n\n```lua\n{\n  sections = {\n    {\n      section = \"terminal\",\n      cmd = \"chafa ~/.config/wall.png --format symbols --symbols vhalf --size 60x17 --stretch; sleep .1\",\n      height = 17,\n      padding = 1,\n    },\n    {\n      pane = 2,\n      { section = \"keys\", gap = 1, padding = 1 },\n      { section = \"startup\" },\n    },\n  },\n}\n```\n\n### `compact_files`\n\nA more compact version of the `files` example\n![image](https://github.com/user-attachments/assets/772e84fe-b220-4841-bbe9-6e28780dc30a)\n\n```lua\n{\n  sections = {\n    { section = \"header\" },\n    { icon = \" \", title = \"Keymaps\", section = \"keys\", indent = 2, padding = 1 },\n    { icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = 1 },\n    { icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 1 },\n    { section = \"startup\" },\n  },\n}\n```\n\n### `doom`\n\nSimilar to the Emacs Doom dashboard\n![image](https://github.com/user-attachments/assets/823f702d-e5d0-449a-afd2-684e1fb97622)\n\n```lua\n{\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { section = \"startup\" },\n  },\n}\n```\n\n### `files`\n\nA simple example with a header, keys, recent files, and projects\n![image](https://github.com/user-attachments/assets/e98997b6-07d3-4162-bc06-2768b78fe353)\n\n```lua\n{\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1 },\n    { icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = { 2, 2 } },\n    { icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 2 },\n    { section = \"startup\" },\n  },\n}\n```\n\n### `github`\n\nAdvanced example using the GitHub CLI.\n![image](https://github.com/user-attachments/assets/747d7386-ef05-487f-9550-3e5ef94869fc)\n\n```lua\n{\n  sections = {\n    { section = \"header\" },\n    {\n      pane = 2,\n      section = \"terminal\",\n      cmd = \"colorscript -e square\",\n      height = 5,\n      padding = 1,\n    },\n    { section = \"keys\", gap = 1, padding = 1 },\n    {\n      pane = 2,\n      icon = \" \",\n      desc = \"Browse Repo\",\n      padding = 1,\n      key = \"b\",\n      action = function()\n        Snacks.gitbrowse()\n      end,\n    },\n    function()\n      local in_git = Snacks.git.get_root() ~= nil\n      local cmds = {\n        {\n          title = \"Notifications\",\n          cmd = \"gh notify -s -a -n5\",\n          action = function()\n            vim.ui.open(\"https://github.com/notifications\")\n          end,\n          key = \"n\",\n          icon = \" \",\n          height = 5,\n          enabled = true,\n        },\n        {\n          title = \"Open Issues\",\n          cmd = \"gh issue list -L 3\",\n          key = \"i\",\n          action = function()\n            vim.fn.jobstart(\"gh issue list --web\", { detach = true })\n          end,\n          icon = \" \",\n          height = 7,\n        },\n        {\n          icon = \" \",\n          title = \"Open PRs\",\n          cmd = \"gh pr list -L 3\",\n          key = \"P\",\n          action = function()\n            vim.fn.jobstart(\"gh pr list --web\", { detach = true })\n          end,\n          height = 7,\n        },\n        {\n          icon = \" \",\n          title = \"Git Status\",\n          cmd = \"git --no-pager diff --stat -B -M -C\",\n          height = 10,\n        },\n      }\n      return vim.tbl_map(function(cmd)\n        return vim.tbl_extend(\"force\", {\n          pane = 2,\n          section = \"terminal\",\n          enabled = in_git,\n          padding = 1,\n          ttl = 5 * 60,\n          indent = 3,\n        }, cmd)\n      end, cmds)\n    end,\n    { section = \"startup\" },\n  },\n}\n```\n\n### `pokemon`\n\nPokemons, because why not?\n![image](https://github.com/user-attachments/assets/2fb17ecc-8bc0-48d3-a023-aa8dfc70247e)\n\n```lua\n{\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { section = \"startup\" },\n    {\n      section = \"terminal\",\n      cmd = \"pokemon-colorscripts -r --no-title; sleep .1\",\n      random = 10,\n      pane = 2,\n      indent = 4,\n      height = 30,\n    },\n  },\n}\n```\n\n### `startify`\n\nSimilar to the Vim Startify dashboard\n![image](https://github.com/user-attachments/assets/561eff8c-ddf0-4de9-8485-e6be18a19c0b)\n\n```lua\n{\n  formats = {\n    key = function(item)\n      return { { \"[\", hl = \"special\" }, { item.key, hl = \"key\" }, { \"]\", hl = \"special\" } }\n    end,\n  },\n  sections = {\n    { section = \"terminal\", cmd = \"fortune -s | cowsay\", hl = \"header\", padding = 1, indent = 8 },\n    { title = \"MRU\", padding = 1 },\n    { section = \"recent_files\", limit = 8, padding = 1 },\n    { title = \"MRU \", file = vim.fn.fnamemodify(\".\", \":~\"), padding = 1 },\n    { section = \"recent_files\", cwd = true, limit = 8, padding = 1 },\n    { title = \"Sessions\", padding = 1 },\n    { section = \"projects\", padding = 1 },\n    { title = \"Bookmarks\", padding = 1 },\n    { section = \"keys\" },\n  },\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `dashboard`\n\nThe default style for the dashboard.\nWhen opening the dashboard during startup, only the `bo` and `wo` options are used.\nThe other options are used with `:lua Snacks.dashboard()`\n\n```lua\n{\n  zindex = 10,\n  height = 0,\n  width = 0,\n  bo = {\n    bufhidden = \"wipe\",\n    buftype = \"nofile\",\n    buflisted = false,\n    filetype = \"snacks_dashboard\",\n    swapfile = false,\n    undofile = false,\n  },\n  wo = {\n    colorcolumn = \"\",\n    cursorcolumn = false,\n    cursorline = false,\n    foldmethod = \"manual\",\n    list = false,\n    number = false,\n    relativenumber = false,\n    sidescrolloff = 0,\n    signcolumn = \"no\",\n    spell = false,\n    statuscolumn = \"\",\n    statusline = \"\",\n    winbar = \"\",\n    winhighlight = \"Normal:SnacksDashboardNormal,NormalFloat:SnacksDashboardNormal\",\n    wrap = false,\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.dashboard.Item\n---@field indent? number\n---@field align? \"left\" | \"center\" | \"right\"\n---@field gap? number the number of empty lines between child items\n---@field padding? number | {[1]:number, [2]:number} bottom or {bottom, top} padding\n--- The action to run when the section is selected or the key is pressed.\n--- * if it's a string starting with `:`, it will be run as a command\n--- * if it's a string, it will be executed as a keymap\n--- * if it's a function, it will be called\n---@field action? snacks.dashboard.Action\n---@field enabled? boolean|fun(opts:snacks.dashboard.Opts):boolean if false, the section will be disabled\n---@field section? string the name of a section to include. See `Snacks.dashboard.sections`\n---@field [string] any section options\n---@field key? string shortcut key\n---@field hidden? boolean when `true`, the item will not be shown, but the key will still be assigned\n---@field autokey? boolean automatically assign a numerical key\n---@field label? string\n---@field desc? string\n---@field file? string\n---@field footer? string\n---@field header? string\n---@field icon? string\n---@field title? string\n---@field text? string|snacks.dashboard.Text[]\n```\n\n```lua\n---@alias snacks.dashboard.Format.ctx {width?:number}\n---@alias snacks.dashboard.Action string|fun(self:snacks.dashboard.Class)\n---@alias snacks.dashboard.Gen fun(self:snacks.dashboard.Class):snacks.dashboard.Section?\n---@alias snacks.dashboard.Section snacks.dashboard.Item|snacks.dashboard.Gen|snacks.dashboard.Section[]\n```\n\n```lua\n---@class snacks.dashboard.Text\n---@field [1] string the text\n---@field hl? string the highlight group\n---@field width? number the width used for alignment\n---@field align? \"left\" | \"center\" | \"right\"\n```\n\n```lua\n---@class snacks.dashboard.Opts: snacks.dashboard.Config\n---@field buf? number the buffer to use. If not provided, a new buffer will be created\n---@field win? number the window to use. If not provided, a new floating window will be created\n```\n\n## 📦 Module\n\n### `Snacks.dashboard()`\n\n```lua\n---@type fun(opts?: snacks.dashboard.Opts): snacks.dashboard.Class\nSnacks.dashboard()\n```\n\n### `Snacks.dashboard.have_plugin()`\n\nChecks if the plugin is installed.\nOnly works with [lazy.nvim](https://github.com/folke/lazy.nvim)\n\n```lua\n---@param name string\nSnacks.dashboard.have_plugin(name)\n```\n\n### `Snacks.dashboard.health()`\n\n```lua\nSnacks.dashboard.health()\n```\n\n### `Snacks.dashboard.icon()`\n\nGet an icon\n\n```lua\n---@param name string\n---@param cat? string\n---@return snacks.dashboard.Text\nSnacks.dashboard.icon(name, cat)\n```\n\n### `Snacks.dashboard.oldfiles()`\n\n```lua\n---@param opts? {filter?: table<string, boolean>}\n---@return fun():string?\nSnacks.dashboard.oldfiles(opts)\n```\n\n### `Snacks.dashboard.open()`\n\n```lua\n---@param opts? snacks.dashboard.Opts\n---@return snacks.dashboard.Class\nSnacks.dashboard.open(opts)\n```\n\n### `Snacks.dashboard.pick()`\n\nUsed by the default preset to pick something\n\n```lua\n---@param cmd? string\nSnacks.dashboard.pick(cmd, opts)\n```\n\n### `Snacks.dashboard.sections.header()`\n\n```lua\n---@return snacks.dashboard.Gen\nSnacks.dashboard.sections.header()\n```\n\n### `Snacks.dashboard.sections.keys()`\n\n```lua\n---@return snacks.dashboard.Gen\nSnacks.dashboard.sections.keys()\n```\n\n### `Snacks.dashboard.sections.projects()`\n\nGet the most recent projects based on git roots of recent files.\nThe default action will change the directory to the project root,\ntry to restore the session and open the picker if the session is not restored.\nYou can customize the behavior by providing a custom action.\nUse `opts.dirs` to provide a list of directories to use instead of the git roots.\n\n```lua\n---@param opts? {limit?:number, dirs?:(string[]|fun():string[]), pick?:boolean, session?:boolean, action?:fun(dir), filter?:fun(dir:string):boolean?}\nSnacks.dashboard.sections.projects(opts)\n```\n\n### `Snacks.dashboard.sections.recent_files()`\n\nGet the most recent files, optionally filtered by the\ncurrent working directory or a custom directory.\n\n```lua\n---@param opts? {limit?:number, cwd?:string|boolean, filter?:fun(file:string):boolean?}\n---@return snacks.dashboard.Gen\nSnacks.dashboard.sections.recent_files(opts)\n```\n\n### `Snacks.dashboard.sections.session()`\n\nAdds a section to restore the session if any of the supported plugins are installed.\n\n```lua\n---@param item? snacks.dashboard.Item\n---@return snacks.dashboard.Item?\nSnacks.dashboard.sections.session(item)\n```\n\n### `Snacks.dashboard.sections.startup()`\n\nAdd the startup section\n\n```lua\n---@param opts? {icon?:string}\n---@return snacks.dashboard.Section?\nSnacks.dashboard.sections.startup(opts)\n```\n\n### `Snacks.dashboard.sections.terminal()`\n\n```lua\n---@param opts {cmd:string|string[], ttl?:number, height?:number, width?:number, random?:number}|snacks.dashboard.Item\n---@return snacks.dashboard.Gen\nSnacks.dashboard.sections.terminal(opts)\n```\n\n### `Snacks.dashboard.setup()`\n\nCheck if the dashboard should be opened\n\n```lua\nSnacks.dashboard.setup()\n```\n\n### `Snacks.dashboard.update()`\n\nUpdate the dashboard\n\n```lua\nSnacks.dashboard.update()\n```\n"
  },
  {
    "path": "docs/debug.md",
    "content": "# 🍿 debug\n\nUtility functions you can use in your code.\n\nPersonally, I have the code below at the top of my `init.lua`:\n\n```lua\n_G.dd = function(...)\n  Snacks.debug.inspect(...)\nend\n_G.bt = function()\n  Snacks.debug.backtrace()\nend\nif vim.fn.has(\"nvim-0.11\") == 1 then\n  vim._print = function(_, ...)\n    dd(...)\n  end\nelse\n  vim.print = dd\nend\n```\n\nWhat this does:\n\n- Add a global `dd(...)` you can use anywhere to quickly show a\n  notification with a pretty printed dump of the object(s)\n  with lua treesitter highlighting\n- Add a global `bt()` to show a notification with a pretty\n  backtrace.\n- Override Neovim's `vim.print`, which is also used by `:= {something = 123}`\n\n![image](https://github.com/user-attachments/assets/0517aed7-fbd0-42ee-8058-c213410d80a7)\n\n<!-- docgen -->\n\n## 📚 Types\n\n```lua\n---@class snacks.debug.cmd\n---@field cmd string|string[]\n---@field level? snacks.notifier.level|vim.log.levels\n---@field title? string\n---@field args? string[]\n---@field cwd? string\n---@field group? boolean\n---@field notify? boolean\n---@field footer? string\n---@field header? string\n---@field props? table<string, string|boolean|number|nil>\n```\n\n```lua\n---@alias snacks.debug.Trace {name: string, time: number, [number]:snacks.debug.Trace}\n---@alias snacks.debug.Stat {name:string, time:number, count?:number, depth?:number}\n```\n\n## 📦 Module\n\n### `Snacks.debug()`\n\n```lua\n---@type fun(...)\nSnacks.debug()\n```\n\n### `Snacks.debug.backtrace()`\n\nShow a notification with a pretty backtrace\n\n```lua\n---@param msg? string|string[]\n---@param opts? snacks.notify.Opts\nSnacks.debug.backtrace(msg, opts)\n```\n\n### `Snacks.debug.cmd()`\n\n```lua\n---@param opts snacks.debug.cmd\nSnacks.debug.cmd(opts)\n```\n\n### `Snacks.debug.inspect()`\n\nShow a notification with a pretty printed dump of the object(s)\nwith lua treesitter highlighting and the location of the caller\n\n```lua\nSnacks.debug.inspect(...)\n```\n\n### `Snacks.debug.log()`\n\nLog a message to the file `./debug.log`.\n- a timestamp will be added to every message.\n- accepts multiple arguments and pretty prints them.\n- if the argument is not a string, it will be printed using `vim.inspect`.\n- if the message is smaller than 120 characters, it will be printed on a single line.\n\n```lua\nSnacks.debug.log(\"Hello\", { foo = \"bar\" }, 42)\n-- 2024-11-08 08:56:52 Hello { foo = \"bar\" } 42\n```\n\n```lua\nSnacks.debug.log(...)\n```\n\n### `Snacks.debug.metrics()`\n\n```lua\nSnacks.debug.metrics()\n```\n\n### `Snacks.debug.profile()`\n\nVery simple function to profile a lua function.\n* **flush**: set to `true` to use `jit.flush` in every iteration.\n* **count**: defaults to 100\n\n```lua\n---@param fn fun()\n---@param opts? {count?: number, flush?: boolean, title?: string}\nSnacks.debug.profile(fn, opts)\n```\n\n### `Snacks.debug.run()`\n\nRun the current buffer or a range of lines.\nShows the output of `print` inlined with the code.\nAny error will be shown as a diagnostic.\n\n```lua\n---@param opts? {name?:string, buf?:number, print?:boolean}\nSnacks.debug.run(opts)\n```\n\n### `Snacks.debug.size()`\n\n```lua\nSnacks.debug.size(bytes)\n```\n\n### `Snacks.debug.stats()`\n\n```lua\n---@param opts? {min?: number, show?:boolean}\n---@return {summary:table<string, snacks.debug.Stat>, trace:snacks.debug.Stat[], traces:snacks.debug.Trace[]}\nSnacks.debug.stats(opts)\n```\n\n### `Snacks.debug.trace()`\n\n```lua\n---@param name string?\nSnacks.debug.trace(name)\n```\n\n### `Snacks.debug.tracemod()`\n\n```lua\n---@param modname string\n---@param mod? table\n---@param suffix? string\nSnacks.debug.tracemod(modname, mod, suffix)\n```\n"
  },
  {
    "path": "docs/dim.md",
    "content": "# 🍿 dim\n\nFocus on the active scope by dimming the rest.\n\nSimilar plugins:\n\n- [twilight.nvim](https://github.com/folke/twilight.nvim)\n- [limelight.vim](https://github.com/junegunn/limelight.vim)\n- [goyo.vim](https://github.com/junegunn/goyo.vim)\n\n![image](https://github.com/user-attachments/assets/c0c5ffda-aaeb-4578-8a18-abee2e443a93)\n\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    dim = {\n      -- your dim configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.dim.Config\n{\n  ---@type snacks.scope.Config\n  scope = {\n    min_size = 5,\n    max_size = 20,\n    siblings = true,\n  },\n  -- animate scopes. Enabled by default for Neovim >= 0.10\n  -- Works on older versions but has to trigger redraws during animation.\n  ---@type snacks.animate.Config|{enabled?: boolean}\n  animate = {\n    enabled = vim.fn.has(\"nvim-0.10\") == 1,\n    easing = \"outQuad\",\n    duration = {\n      step = 20, -- ms per step\n      total = 300, -- maximum duration\n    },\n  },\n  -- what buffers to dim\n  filter = function(buf)\n    return vim.g.snacks_dim ~= false and vim.b[buf].snacks_dim ~= false and vim.bo[buf].buftype == \"\"\n  end,\n}\n```\n\n## 📦 Module\n\n### `Snacks.dim()`\n\n```lua\n---@type fun(opts: snacks.dim.Config)\nSnacks.dim()\n```\n\n### `Snacks.dim.disable()`\n\nDisable dimming\n\n```lua\nSnacks.dim.disable()\n```\n\n### `Snacks.dim.enable()`\n\n```lua\n---@param opts? snacks.dim.Config\nSnacks.dim.enable(opts)\n```\n"
  },
  {
    "path": "docs/examples/dashboard.lua",
    "content": "local M = {}\n\n---@type table<string, snacks.dashboard.Section>\nM.examples = {}\n\n-- Similar to the Emacs Doom dashboard\n-- ![image](https://github.com/user-attachments/assets/823f702d-e5d0-449a-afd2-684e1fb97622)\nM.examples.doom = {\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { section = \"startup\" },\n  },\n}\n\n-- Similar to the Vim Startify dashboard\n-- ![image](https://github.com/user-attachments/assets/561eff8c-ddf0-4de9-8485-e6be18a19c0b)\nM.examples.startify = {\n  formats = {\n    key = function(item)\n      return { { \"[\", hl = \"special\" }, { item.key, hl = \"key\" }, { \"]\", hl = \"special\" } }\n    end,\n  },\n  sections = {\n    { section = \"terminal\", cmd = \"fortune -s | cowsay\", hl = \"header\", padding = 1, indent = 8 },\n    { title = \"MRU\", padding = 1 },\n    { section = \"recent_files\", limit = 8, padding = 1 },\n    { title = \"MRU \", file = vim.fn.fnamemodify(\".\", \":~\"), padding = 1 },\n    { section = \"recent_files\", cwd = true, limit = 8, padding = 1 },\n    { title = \"Sessions\", padding = 1 },\n    { section = \"projects\", padding = 1 },\n    { title = \"Bookmarks\", padding = 1 },\n    { section = \"keys\" },\n  },\n}\n\n-- A more advanced example using multiple panes\n-- ![image](https://github.com/user-attachments/assets/bbf4d2cd-6fc5-4122-a462-0ca59ba89545)\nM.examples.advanced = {\n  sections = {\n    { section = \"header\" },\n    {\n      pane = 2,\n      section = \"terminal\",\n      cmd = \"colorscript -e square\",\n      height = 5,\n      padding = 1,\n    },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { pane = 2, icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = 1 },\n    { pane = 2, icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 1 },\n    {\n      pane = 2,\n      icon = \" \",\n      title = \"Git Status\",\n      section = \"terminal\",\n      enabled = function()\n        return Snacks.git.get_root() ~= nil\n      end,\n      cmd = \"git status --short --branch --renames\",\n      height = 5,\n      padding = 1,\n      ttl = 5 * 60,\n      indent = 3,\n    },\n    { section = \"startup\" },\n  },\n}\n\n-- Advanced example using the GitHub CLI.\n-- ![image](https://github.com/user-attachments/assets/747d7386-ef05-487f-9550-3e5ef94869fc)\nM.examples.github = {\n  sections = {\n    { section = \"header\" },\n    {\n      pane = 2,\n      section = \"terminal\",\n      cmd = \"colorscript -e square\",\n      height = 5,\n      padding = 1,\n    },\n    { section = \"keys\", gap = 1, padding = 1 },\n    {\n      pane = 2,\n      icon = \" \",\n      desc = \"Browse Repo\",\n      padding = 1,\n      key = \"b\",\n      action = function()\n        Snacks.gitbrowse()\n      end,\n    },\n    function()\n      local in_git = Snacks.git.get_root() ~= nil\n      local cmds = {\n        {\n          title = \"Notifications\",\n          cmd = \"gh notify -s -a -n5\",\n          action = function()\n            vim.ui.open(\"https://github.com/notifications\")\n          end,\n          key = \"n\",\n          icon = \" \",\n          height = 5,\n          enabled = true,\n        },\n        {\n          title = \"Open Issues\",\n          cmd = \"gh issue list -L 3\",\n          key = \"i\",\n          action = function()\n            vim.fn.jobstart(\"gh issue list --web\", { detach = true })\n          end,\n          icon = \" \",\n          height = 7,\n        },\n        {\n          icon = \" \",\n          title = \"Open PRs\",\n          cmd = \"gh pr list -L 3\",\n          key = \"P\",\n          action = function()\n            vim.fn.jobstart(\"gh pr list --web\", { detach = true })\n          end,\n          height = 7,\n        },\n        {\n          icon = \" \",\n          title = \"Git Status\",\n          cmd = \"git --no-pager diff --stat -B -M -C\",\n          height = 10,\n        },\n      }\n      return vim.tbl_map(function(cmd)\n        return vim.tbl_extend(\"force\", {\n          pane = 2,\n          section = \"terminal\",\n          enabled = in_git,\n          padding = 1,\n          ttl = 5 * 60,\n          indent = 3,\n        }, cmd)\n      end, cmds)\n    end,\n    { section = \"startup\" },\n  },\n}\n\n-- A simple example with a header, keys, recent files, and projects\n-- ![image](https://github.com/user-attachments/assets/e98997b6-07d3-4162-bc06-2768b78fe353)\nM.examples.files = {\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1 },\n    { icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = { 2, 2 } },\n    { icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 2 },\n    { section = \"startup\" },\n  },\n}\n\n-- A more compact version of the `files` example\n-- ![image](https://github.com/user-attachments/assets/772e84fe-b220-4841-bbe9-6e28780dc30a)\nM.examples.compact_files = {\n  sections = {\n    { section = \"header\" },\n    { icon = \" \", title = \"Keymaps\", section = \"keys\", indent = 2, padding = 1 },\n    { icon = \" \", title = \"Recent Files\", section = \"recent_files\", indent = 2, padding = 1 },\n    { icon = \" \", title = \"Projects\", section = \"projects\", indent = 2, padding = 1 },\n    { section = \"startup\" },\n  },\n}\n\n-- An example using the `chafa` command to display an image\n-- ![image](https://github.com/user-attachments/assets/e498ef8f-83ce-4917-a720-8cb31d98ecec)\nM.examples.chafa = {\n  sections = {\n    {\n      section = \"terminal\",\n      cmd = \"chafa ~/.config/wall.png --format symbols --symbols vhalf --size 60x17 --stretch; sleep .1\",\n      height = 17,\n      padding = 1,\n    },\n    {\n      pane = 2,\n      { section = \"keys\", gap = 1, padding = 1 },\n      { section = \"startup\" },\n    },\n  },\n}\n\n-- Pokemons, because why not?\n-- ![image](https://github.com/user-attachments/assets/2fb17ecc-8bc0-48d3-a023-aa8dfc70247e)\nM.examples.pokemon = {\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { section = \"startup\" },\n    {\n      section = \"terminal\",\n      cmd = \"pokemon-colorscripts -r --no-title; sleep .1\",\n      random = 10,\n      pane = 2,\n      indent = 4,\n      height = 30,\n    },\n  },\n}\n\nreturn M\n"
  },
  {
    "path": "docs/examples/init.lua",
    "content": "-- stylua: ignore\nreturn {\n  \"folke/snacks.nvim\",\n  priority = 1000,\n  lazy = false,\n  ---@type snacks.Config\n  opts = {\n    bigfile = { enabled = true },\n    dashboard = { enabled = true },\n    explorer = { enabled = true },\n    indent = { enabled = true },\n    input = { enabled = true },\n    notifier = {\n      enabled = true,\n      timeout = 3000,\n    },\n    picker = { enabled = true },\n    quickfile = { enabled = true },\n    scope = { enabled = true },\n    scroll = { enabled = true },\n    statuscolumn = { enabled = true },\n    words = { enabled = true },\n    styles = {\n      notification = {\n        -- wo = { wrap = true } -- Wrap notifications\n      }\n    }\n  },\n  keys = {\n    -- EXTRA_KEYS\n    -- Other\n    { \"<leader>z\",  function() Snacks.zen() end, desc = \"Toggle Zen Mode\" },\n    { \"<leader>Z\",  function() Snacks.zen.zoom() end, desc = \"Toggle Zoom\" },\n    { \"<leader>.\",  function() Snacks.scratch() end, desc = \"Toggle Scratch Buffer\" },\n    { \"<leader>S\",  function() Snacks.scratch.select() end, desc = \"Select Scratch Buffer\" },\n    { \"<leader>n\",  function() Snacks.notifier.show_history() end, desc = \"Notification History\" },\n    { \"<leader>bd\", function() Snacks.bufdelete() end, desc = \"Delete Buffer\" },\n    { \"<leader>cR\", function() Snacks.rename.rename_file() end, desc = \"Rename File\" },\n    { \"<leader>gB\", function() Snacks.gitbrowse() end, desc = \"Git Browse\", mode = { \"n\", \"v\" } },\n    { \"<leader>gg\", function() Snacks.lazygit() end, desc = \"Lazygit\" },\n    { \"<leader>un\", function() Snacks.notifier.hide() end, desc = \"Dismiss All Notifications\" },\n    { \"<c-/>\",      function() Snacks.terminal() end, desc = \"Toggle Terminal\" },\n    { \"<c-_>\",      function() Snacks.terminal() end, desc = \"which_key_ignore\" },\n    { \"]]\",         function() Snacks.words.jump(vim.v.count1) end, desc = \"Next Reference\", mode = { \"n\", \"t\" } },\n    { \"[[\",         function() Snacks.words.jump(-vim.v.count1) end, desc = \"Prev Reference\", mode = { \"n\", \"t\" } },\n    {\n      \"<leader>N\",\n      desc = \"Neovim News\",\n      function()\n        Snacks.win({\n          file = vim.api.nvim_get_runtime_file(\"doc/news.txt\", false)[1],\n          width = 0.6,\n          height = 0.6,\n          wo = {\n            spell = false,\n            wrap = false,\n            signcolumn = \"yes\",\n            statuscolumn = \" \",\n            conceallevel = 3,\n          },\n        })\n      end,\n    }\n  },\n  init = function()\n    vim.api.nvim_create_autocmd(\"User\", {\n      pattern = \"VeryLazy\",\n      callback = function()\n        -- Setup some globals for debugging (lazy-loaded)\n        _G.dd = function(...)\n          Snacks.debug.inspect(...)\n        end\n        _G.bt = function()\n          Snacks.debug.backtrace()\n        end\n\n        -- Override print to use snacks for `:=` command\n        if vim.fn.has(\"nvim-0.11\") == 1 then\n          vim._print = function(_, ...)\n            dd(...)\n          end\n        else\n          vim.print = _G.dd \n        end\n\n        -- Create some toggle mappings\n        Snacks.toggle.option(\"spell\", { name = \"Spelling\" }):map(\"<leader>us\")\n        Snacks.toggle.option(\"wrap\", { name = \"Wrap\" }):map(\"<leader>uw\")\n        Snacks.toggle.option(\"relativenumber\", { name = \"Relative Number\" }):map(\"<leader>uL\")\n        Snacks.toggle.diagnostics():map(\"<leader>ud\")\n        Snacks.toggle.line_number():map(\"<leader>ul\")\n        Snacks.toggle.option(\"conceallevel\", { off = 0, on = vim.o.conceallevel > 0 and vim.o.conceallevel or 2 }):map(\"<leader>uc\")\n        Snacks.toggle.treesitter():map(\"<leader>uT\")\n        Snacks.toggle.option(\"background\", { off = \"light\", on = \"dark\", name = \"Dark Background\" }):map(\"<leader>ub\")\n        Snacks.toggle.inlay_hints():map(\"<leader>uh\")\n        Snacks.toggle.indent():map(\"<leader>ug\")\n        Snacks.toggle.dim():map(\"<leader>uD\")\n      end,\n    })\n  end,\n}\n"
  },
  {
    "path": "docs/examples/picker.lua",
    "content": "local M = {}\n\nM.examples = {}\n\nM.examples.general = {\n  \"folke/snacks.nvim\",\n  opts = {\n    picker = {},\n    explorer = {},\n  },\n  -- stylua: ignore\n  keys = {\n    -- Top Pickers & Explorer\n    { \"<leader><space>\", function() Snacks.picker.smart() end, desc = \"Smart Find Files\" },\n    { \"<leader>,\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n    { \"<leader>/\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n    { \"<leader>:\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n    { \"<leader>n\", function() Snacks.picker.notifications() end, desc = \"Notification History\" },\n    { \"<leader>e\", function() Snacks.explorer() end, desc = \"File Explorer\" },\n    -- find\n    { \"<leader>fb\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n    { \"<leader>fc\", function() Snacks.picker.files({ cwd = vim.fn.stdpath(\"config\") }) end, desc = \"Find Config File\" },\n    { \"<leader>ff\", function() Snacks.picker.files() end, desc = \"Find Files\" },\n    { \"<leader>fg\", function() Snacks.picker.git_files() end, desc = \"Find Git Files\" },\n    { \"<leader>fp\", function() Snacks.picker.projects() end, desc = \"Projects\" },\n    { \"<leader>fr\", function() Snacks.picker.recent() end, desc = \"Recent\" },\n    -- git\n    { \"<leader>gb\", function() Snacks.picker.git_branches() end, desc = \"Git Branches\" },\n    { \"<leader>gl\", function() Snacks.picker.git_log() end, desc = \"Git Log\" },\n    { \"<leader>gL\", function() Snacks.picker.git_log_line() end, desc = \"Git Log Line\" },\n    { \"<leader>gs\", function() Snacks.picker.git_status() end, desc = \"Git Status\" },\n    { \"<leader>gS\", function() Snacks.picker.git_stash() end, desc = \"Git Stash\" },\n    { \"<leader>gd\", function() Snacks.picker.git_diff() end, desc = \"Git Diff (Hunks)\" },\n    { \"<leader>gf\", function() Snacks.picker.git_log_file() end, desc = \"Git Log File\" },\n    -- gh\n    { \"<leader>gi\", function() Snacks.picker.gh_issue() end, desc = \"GitHub Issues (open)\" },\n    { \"<leader>gI\", function() Snacks.picker.gh_issue({ state = \"all\" }) end, desc = \"GitHub Issues (all)\" },\n    { \"<leader>gp\", function() Snacks.picker.gh_pr() end, desc = \"GitHub Pull Requests (open)\" },\n    { \"<leader>gP\", function() Snacks.picker.gh_pr({ state = \"all\" }) end, desc = \"GitHub Pull Requests (all)\" },\n    -- Grep\n    { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n    { \"<leader>sB\", function() Snacks.picker.grep_buffers() end, desc = \"Grep Open Buffers\" },\n    { \"<leader>sg\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n    { \"<leader>sw\", function() Snacks.picker.grep_word() end, desc = \"Visual selection or word\", mode = { \"n\", \"x\" } },\n    -- search\n    { '<leader>s\"', function() Snacks.picker.registers() end, desc = \"Registers\" },\n    { '<leader>s/', function() Snacks.picker.search_history() end, desc = \"Search History\" },\n    { \"<leader>sa\", function() Snacks.picker.autocmds() end, desc = \"Autocmds\" },\n    { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n    { \"<leader>sc\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n    { \"<leader>sC\", function() Snacks.picker.commands() end, desc = \"Commands\" },\n    { \"<leader>sd\", function() Snacks.picker.diagnostics() end, desc = \"Diagnostics\" },\n    { \"<leader>sD\", function() Snacks.picker.diagnostics_buffer() end, desc = \"Buffer Diagnostics\" },\n    { \"<leader>sh\", function() Snacks.picker.help() end, desc = \"Help Pages\" },\n    { \"<leader>sH\", function() Snacks.picker.highlights() end, desc = \"Highlights\" },\n    { \"<leader>si\", function() Snacks.picker.icons() end, desc = \"Icons\" },\n    { \"<leader>sj\", function() Snacks.picker.jumps() end, desc = \"Jumps\" },\n    { \"<leader>sk\", function() Snacks.picker.keymaps() end, desc = \"Keymaps\" },\n    { \"<leader>sl\", function() Snacks.picker.loclist() end, desc = \"Location List\" },\n    { \"<leader>sm\", function() Snacks.picker.marks() end, desc = \"Marks\" },\n    { \"<leader>sM\", function() Snacks.picker.man() end, desc = \"Man Pages\" },\n    { \"<leader>sp\", function() Snacks.picker.lazy() end, desc = \"Search for Plugin Spec\" },\n    { \"<leader>sq\", function() Snacks.picker.qflist() end, desc = \"Quickfix List\" },\n    { \"<leader>sR\", function() Snacks.picker.resume() end, desc = \"Resume\" },\n    { \"<leader>su\", function() Snacks.picker.undo() end, desc = \"Undo History\" },\n    { \"<leader>uC\", function() Snacks.picker.colorschemes() end, desc = \"Colorschemes\" },\n    -- LSP\n    { \"gd\", function() Snacks.picker.lsp_definitions() end, desc = \"Goto Definition\" },\n    { \"gD\", function() Snacks.picker.lsp_declarations() end, desc = \"Goto Declaration\" },\n    { \"gr\", function() Snacks.picker.lsp_references() end, nowait = true, desc = \"References\" },\n    { \"gI\", function() Snacks.picker.lsp_implementations() end, desc = \"Goto Implementation\" },\n    { \"gy\", function() Snacks.picker.lsp_type_definitions() end, desc = \"Goto T[y]pe Definition\" },\n    { \"gai\", function() Snacks.picker.lsp_incoming_calls() end, desc = \"C[a]lls Incoming\" },\n    { \"gao\", function() Snacks.picker.lsp_outgoing_calls() end, desc = \"C[a]lls Outgoing\" },\n    { \"<leader>ss\", function() Snacks.picker.lsp_symbols() end, desc = \"LSP Symbols\" },\n    { \"<leader>sS\", function() Snacks.picker.lsp_workspace_symbols() end, desc = \"LSP Workspace Symbols\" },\n  },\n}\n\nM.examples.trouble = {\n  \"folke/trouble.nvim\",\n  optional = true,\n  specs = {\n    \"folke/snacks.nvim\",\n    opts = function(_, opts)\n      return vim.tbl_deep_extend(\"force\", opts or {}, {\n        picker = {\n          actions = require(\"trouble.sources.snacks\").actions,\n          win = {\n            input = {\n              keys = {\n                [\"<c-t>\"] = {\n                  \"trouble_open\",\n                  mode = { \"n\", \"i\" },\n                },\n              },\n            },\n          },\n        },\n      })\n    end,\n  },\n}\n\nM.examples.todo_comments = {\n  \"folke/todo-comments.nvim\",\n  optional = true,\n  -- stylua: ignore\n  keys = {\n    { \"<leader>st\", function() Snacks.picker.todo_comments() end, desc = \"Todo\" },\n    { \"<leader>sT\", function () Snacks.picker.todo_comments({ keywords = { \"TODO\", \"FIX\", \"FIXME\" } }) end, desc = \"Todo/Fix/Fixme\" },\n  },\n}\n\nM.examples.flash = {\n  \"folke/flash.nvim\",\n  optional = true,\n  specs = {\n    {\n      \"folke/snacks.nvim\",\n      opts = {\n        picker = {\n          win = {\n            input = {\n              keys = {\n                [\"<a-s>\"] = { \"flash\", mode = { \"n\", \"i\" } },\n                [\"s\"] = { \"flash\" },\n              },\n            },\n          },\n          actions = {\n            flash = function(picker)\n              require(\"flash\").jump({\n                pattern = \"^\",\n                label = { after = { 0, 0 } },\n                search = {\n                  mode = \"search\",\n                  exclude = {\n                    function(win)\n                      return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= \"snacks_picker_list\"\n                    end,\n                  },\n                },\n                action = function(match)\n                  local idx = picker.list:row2idx(match.pos[1])\n                  picker.list:_move(idx, true, true)\n                end,\n              })\n            end,\n          },\n        },\n      },\n    },\n  },\n}\n\nreturn M\n"
  },
  {
    "path": "docs/explorer.md",
    "content": "# 🍿 explorer\n\nA file explorer for snacks. This is actually a [picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#explorer) in disguise.\n\nThis module provide a shortcut to open the explorer picker and\na setup function to replace netrw with the explorer.\n\nWhen the explorer and `replace_netrw` is enabled, the explorer will be opened:\n\n- when you start `nvim` with a directory\n- when you open a directory in vim\n\nConfiguring the explorer picker is done with the [picker options](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#explorer).\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    explorer = {\n      -- your explorer configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    },\n    picker = {\n      sources = {\n        explorer = {\n          -- your explorer picker configuration comes here\n          -- or leave it empty to use the default settings\n        }\n      }\n    }\n  }\n}\n```\n\n![image](https://github.com/user-attachments/assets/e09d25f8-8559-441c-a0f7-576d2aa57097)\n\n## 🚀 Usage\n\n### File Operations\n\nThe explorer provides powerful file operations with an intuitive selection-based workflow.\n\n#### Moving and Copying Files\n\nThe most efficient way to move or copy multiple files:\n\n1. **Select files** with `<Tab>` (works on multiple files)\n2. **Navigate** to the target directory\n3. **Execute** the operation:\n   - Press `m` to **move** selected files to the current directory\n   - Press `c` to **copy** selected files to the current directory\n\n```\nExample workflow:\n1. Navigate to source files\n2. Press <Tab> on file1.txt\n3. Press <Tab> on file2.txt (both now selected)\n4. Navigate to target directory\n5. Press 'm' → files are moved!\n```\n\n**Single file operations:**\n\n- `m` on a single file (no selection) → renames the file\n- `c` on a single file (no selection) → prompts for new name to copy to\n- `r` → rename current file\n- `d` → delete current/selected files\n\n#### Copy/Paste with Registers\n\nAlternative workflow using yank and paste:\n\n1. **Select files** with `<Tab>` or visual mode\n2. Press `y` to **yank** file paths to register\n3. Navigate to target directory\n4. Press `p` to **paste** (copies files from register)\n\nThis works across different explorer instances and even after closing/reopening!\n\n#### Other File Operations\n\n- `a` → **Add** new file or directory (directories end with `/`)\n- `d` → **Delete** files (uses system trash if available, see `:checkhealth snacks`)\n- `o` → **Open** file with system application\n- `u` → **Update/refresh** the file tree\n\n### Navigation\n\n- `<CR>` or `l` → Open file or toggle directory\n- `h` → Close directory\n- `<BS>` → Go up one directory\n- `.` → Focus on current directory (set as cwd)\n- `H` → Toggle hidden files\n- `I` → Toggle ignored files (from gitignore)\n- `Z` → Close all directories\n\n### Quick Actions\n\n- `<leader>/` → Grep in current directory\n- `<c-t>` → Open terminal in current directory\n- `<c-c>` → Change tab directory to current directory\n- `P` → Toggle preview\n\n### Git Integration\n\nWhen `git_status = true` (default), files show git status indicators:\n\n- `]g` / `[g` → Jump to next/previous git change\n- Directories show aggregate status of contained files\n\n### Diagnostics\n\nWhen `diagnostics = true` (default), files show diagnostic indicators:\n\n- `]d` / `[d` → Jump to next/previous diagnostic\n- `]e` / `[e` → Jump to next/previous error\n- `]w` / `[w` → Jump to next/previous warning\n\n### Visual Mode\n\nYou can use visual mode (`v` or `V`) to select multiple files, then:\n\n- `y` → Yank selected file paths\n- Any other operation works on visual selection\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    explorer = {\n      -- your explorer configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\nThese are just the general explorer settings.\nTo configure the explorer picker, see `snacks.picker.explorer.Config`\n\n```lua\n---@class snacks.explorer.Config\n{\n  replace_netrw = true, -- Replace netrw with the snacks explorer\n  trash = true, -- Use the system trash when deleting files\n}\n```\n\n## 📦 Module\n\n### `Snacks.explorer()`\n\n```lua\n---@type fun(opts?: snacks.picker.explorer.Config): snacks.Picker\nSnacks.explorer()\n```\n\n### `Snacks.explorer.health()`\n\n```lua\nSnacks.explorer.health()\n```\n\n### `Snacks.explorer.open()`\n\nShortcut to open the explorer picker\n\n```lua\n---@param opts? snacks.picker.explorer.Config|{}\nSnacks.explorer.open(opts)\n```\n\n### `Snacks.explorer.reveal()`\n\nReveals the given file/buffer or the current buffer in the explorer\n\n```lua\n---@param opts? {file?:string, buf?:number}\nSnacks.explorer.reveal(opts)\n```\n"
  },
  {
    "path": "docs/gh.md",
    "content": "# 🍿 gh\n\nA modern GitHub CLI integration for Neovim that brings GitHub issues and pull requests directly into your editor.\n\n<img width=\"1827\" height=\"1053\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/24b90163-7403-4f42-80b4-9a44758c81b5\" />\n\n## ✨ Features\n\n- 📋 Browse and search **GitHub issues** and **pull requests** with fuzzy finding\n- 🔍 View full issue/PR details including **comments**, **reactions**, and **status checks**\n- 📝 Perform GitHub actions directly from Neovim:\n  - Comment on issues and PRs\n  - Close, reopen, edit, and merge PRs\n  - Add reactions and labels\n  - Review PRs (approve, request changes, comment)\n  - Checkout PR branches locally\n  - View PR diffs with syntax highlighting\n- ⌨️ Customizable **keymaps** for common GitHub operations\n- 🎨 Beautiful **syntax highlighting** using Treesitter\n- 🔗 Open issues/PRs in your web browser\n- 📎 Yank URLs to clipboard\n- 🌲 Built on top of the powerful [Snacks picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md)\n\n## ⚡️ Requirements\n\n- [GitHub CLI (`gh`)](https://cli.github.com/) - must be installed and authenticated\n- Snacks [picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) enabled\n\n## 🚀 Recommended Setup\n\n```lua\n{\n  \"folke/snacks.nvim\",\n  opts = {\n    gh = {\n      -- your gh configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    },\n    picker = {\n      sources = {\n        gh_issue = {\n          -- your gh_issue picker configuration comes here\n          -- or leave it empty to use the default settings\n        },\n        gh_pr = {\n          -- your gh_pr picker configuration comes here\n          -- or leave it empty to use the default settings\n        }\n      }\n    },\n  },\n  keys = {\n    { \"<leader>gi\", function() Snacks.picker.gh_issue() end, desc = \"GitHub Issues (open)\" },\n    { \"<leader>gI\", function() Snacks.picker.gh_issue({ state = \"all\" }) end, desc = \"GitHub Issues (all)\" },\n    { \"<leader>gp\", function() Snacks.picker.gh_pr() end, desc = \"GitHub Pull Requests (open)\" },\n    { \"<leader>gP\", function() Snacks.picker.gh_pr({ state = \"all\" }) end, desc = \"GitHub Pull Requests (all)\" },\n  },\n}\n```\n\n## 📚 Usage\n\n```lua\n-- Browse open issues\nSnacks.picker.gh_issue()\n\n-- Browse all issues (including closed)\nSnacks.picker.gh_issue({ state = \"all\" })\n\n-- Browse open pull requests\nSnacks.picker.gh_pr()\n\n-- Browse all pull requests\nSnacks.picker.gh_pr({ state = \"all\" })\n\n-- View PR diff\nSnacks.picker.gh_diff({ pr = 123 })\n\n-- Open issue/PR in buffer\nSnacks.gh.open({ type = \"issue\", number = 123, repo = \"owner/repo\" })\n```\n\n### Available Actions\n\nWhen viewing an issue or PR in the picker, press `<cr>` to show available actions:\n\n<img width=\"1827\" height=\"1053\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/ec6cdb00-2738-4442-b4e5-3f733e551265\" />\n\n`Snacks.gh` makes extensive use of `Snacks.scratch` for editing comments and descriptions.\n\n<img width=\"1250\" height=\"831\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/37f20d3f-a944-49fa-9572-b78cec386158\" />\n\n**Common Actions:**\n\n- **Open in buffer** - View full details with comments\n- **Open in browser** - Open in GitHub web UI\n- **Add comment** - Add a new comment\n- **Add reaction** - React with emoji\n- **Add/Remove labels** - Manage labels\n- **Close/Reopen** - Change issue/PR state\n- **Edit** - Edit title and body\n- **Yank URL** - Copy URL to clipboard\n\n**Pull Request/Issue Specific:**\n\n- **View diff** - Show changed files with syntax highlighting\n- **Checkout** - Checkout PR branch locally\n- **Merge** - Merge, squash, or rebase and merge\n- **Review** - Approve, request changes, or comment\n- **Mark as draft/ready** - Change draft status\n- and more...\n\n<img width=\"1827\" height=\"1053\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/04aff0f5-3676-4555-a9e7-9b6fb21a9321\" />\n\n### GitHub Buffers\n\nWhen you open an issue or PR in a buffer, you get a beautiful rendered view with:\n\n- **Metadata** - Status, author, dates, labels, reactions, and assignees\n- **Description** - Full issue/PR body with markdown rendering\n- **Comments** - All comments with author info and timestamps\n- **Status Checks** - PR status checks and CI results (for PRs)\n- **Syntax Highlighting** - Full Treesitter support for markdown\n- **Folding** - Foldable sections for comments and metadata\n\n**Default Keymaps in GitHub Buffers:**\n\n| Key    | Action        | Description                  |\n| ------ | ------------- | ---------------------------- |\n| `<cr>` | Select Action | Show available actions menu  |\n| `i`    | Edit          | Edit issue/PR title and body |\n| `a`    | Add Comment   | Add a new comment            |\n| `c`    | Close         | Close the issue/PR           |\n| `o`    | Reopen        | Reopen a closed issue/PR     |\n\nSee the [config section](#%EF%B8%8F-config) to customize these keymaps.\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    gh = {\n      -- your gh configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.gh.Config\n{\n  --- Keymaps for GitHub buffers\n  ---@type table<string, snacks.gh.Keymap|false>?\n  keys = {\n    select  = { \"<cr>\", \"gh_actions\", desc = \"Select Action\" },\n    edit    = { \"i\"   , \"gh_edit\"   , desc = \"Edit\" },\n    comment = { \"a\"   , \"gh_comment\", desc = \"Add Comment\" },\n    close   = { \"c\"   , \"gh_close\"  , desc = \"Close\" },\n    reopen  = { \"o\"   , \"gh_reopen\" , desc = \"Reopen\" },\n  },\n  ---@type vim.wo|{}\n  wo = {\n    breakindent = true,\n    wrap = true,\n    showbreak = \"\",\n    linebreak = true,\n    number = false,\n    relativenumber = false,\n    foldexpr = \"v:lua.vim.treesitter.foldexpr()\",\n    foldmethod = \"expr\",\n    concealcursor = \"n\",\n    conceallevel = 2,\n    list = false,\n    winhighlight = Snacks.util.winhl({\n      Normal = \"SnacksGhNormal\",\n      NormalFloat = \"SnacksGhNormalFloat\",\n      FloatBorder = \"SnacksGhBorder\",\n      FloatTitle = \"SnacksGhTitle\",\n      FloatFooter = \"SnacksGhFooter\",\n    }),\n  },\n  ---@type vim.bo|{}\n  bo = {},\n  diff = {\n    min = 4, -- minimum number of lines changed to show diff\n    wrap = 80, -- wrap diff lines at this length\n  },\n  scratch = {\n    height = 15, -- height of scratch window\n  },\n  icons = {\n    logo = \" \",\n    user= \" \",\n    checkmark = \" \",\n    crossmark = \" \",\n    block = \"■\",\n    file = \" \",\n    checks = {\n      pending = \" \",\n      success = \" \",\n      failure = \"\",\n      skipped = \" \",\n    },\n    issue = {\n      open      = \" \",\n      completed = \" \",\n      other     = \" \"\n    },\n    pr = {\n      open   = \" \",\n      closed = \" \",\n      merged = \" \",\n      draft  = \" \",\n      other  = \" \",\n    },\n    review = {\n      approved           = \" \",\n      changes_requested  = \" \",\n      commented          = \" \",\n      dismissed          = \" \",\n      pending            = \" \",\n    },\n    merge_status = {\n      clean    = \" \",\n      dirty    = \" \",\n      blocked  = \" \",\n      unstable = \" \"\n    },\n    reactions = {\n      thumbs_up   = \"👍\",\n      thumbs_down = \"👎\",\n      eyes        = \"👀\",\n      confused    = \"😕\",\n      heart       = \"❤️\",\n      hooray      = \"🎉\",\n      laugh       = \"😄\",\n      rocket      = \"🚀\",\n    },\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@alias snacks.gh.Keymap.fn fun(item:snacks.picker.gh.Item, buf:snacks.gh.Buf)\n---@class snacks.gh.Keymap: vim.keymap.set.Opts\n---@field [1] string lhs\n---@field [2] string|snacks.gh.Keymap.fn rhs\n---@field mode? string|string[] defaults to `n`\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.gh\n---@field api snacks.gh.api\n---@field item snacks.picker.gh.Item\nSnacks.gh = {}\n```\n\n### `Snacks.gh.issue()`\n\n```lua\n---@param opts? snacks.picker.gh.issue.Config\nSnacks.gh.issue(opts)\n```\n\n### `Snacks.gh.pr()`\n\n```lua\n---@param opts? snacks.picker.gh.pr.Config\nSnacks.gh.pr(opts)\n```\n"
  },
  {
    "path": "docs/git.md",
    "content": "# 🍿 git\n\n<!-- docgen -->\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `blame_line`\n\n```lua\n{\n  width = 0.6,\n  height = 0.6,\n  border = true,\n  title = \" Git Blame \",\n  title_pos = \"center\",\n  ft = \"git\",\n}\n```\n\n## 📦 Module\n\n### `Snacks.git.blame_line()`\n\nShow git log for the current line.\n\n```lua\n---@param opts? snacks.terminal.Opts | {count?: number}\nSnacks.git.blame_line(opts)\n```\n\n### `Snacks.git.get_root()`\n\nGets the git root for a buffer or path.\nDefaults to the current buffer.\n\n```lua\n---@param path? number|string buffer or path\n---@return string?\nSnacks.git.get_root(path)\n```\n"
  },
  {
    "path": "docs/gitbrowse.md",
    "content": "# 🍿 gitbrowse\n\nOpen the repo of the active file in the browser (e.g., GitHub)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    gitbrowse = {\n      -- your gitbrowse configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.gitbrowse.Config\n---@field url_patterns? table<string, table<string, string|fun(fields:snacks.gitbrowse.Fields):string>>\n{\n  notify = true, -- show notification on open\n  -- Handler to open the url in a browser\n  ---@param url string\n  open = function(url)\n    if vim.fn.has(\"nvim-0.10\") == 0 then\n      require(\"lazy.util\").open(url, { system = true })\n      return\n    end\n    vim.ui.open(url)\n  end,\n  ---@type \"repo\" | \"branch\" | \"file\" | \"commit\" | \"permalink\"\n  what = \"commit\", -- what to open. not all remotes support all types\n  commit = nil, ---@type string?\n  branch = nil, ---@type string?\n  line_start = nil, ---@type number?\n  line_end = nil, ---@type number?\n  -- patterns to transform remotes to an actual URL\n  remote_patterns = {\n    { \"^(https?://.*)%.git$\"              , \"%1\" },\n    { \"^git@(.+):(.+)%.git$\"              , \"https://%1/%2\" },\n    { \"^git@(.+):(.+)$\"                   , \"https://%1/%2\" },\n    { \"^git@(.+)/(.+)$\"                   , \"https://%1/%2\" },\n    { \"^org%-%d+@(.+):(.+)%.git$\"         , \"https://%1/%2\" },\n    { \"^ssh://git@(.*)$\"                  , \"https://%1\" },\n    { \"^ssh://([^:/]+)(:%d+)/(.*)$\"       , \"https://%1/%3\" },\n    { \"^ssh://([^/]+)/(.*)$\"              , \"https://%1/%2\" },\n    { \"ssh%.dev%.azure%.com/v3/(.*)/(.*)$\", \"dev.azure.com/%1/_git/%2\" },\n    { \"^https://%w*@(.*)\"                 , \"https://%1\" },\n    { \"^git@(.*)\"                         , \"https://%1\" },\n    { \":%d+\"                              , \"\" },\n    { \"%.git$\"                            , \"\" },\n  },\n  url_patterns = {\n    [\"github%.com\"] = {\n      branch = \"/tree/{branch}\",\n      file = \"/blob/{branch}/{file}#L{line_start}-L{line_end}\",\n      permalink = \"/blob/{commit}/{file}#L{line_start}-L{line_end}\",\n      commit = \"/commit/{commit}\",\n    },\n    [\"gitlab%.com\"] = {\n      branch = \"/-/tree/{branch}\",\n      file = \"/-/blob/{branch}/{file}#L{line_start}-{line_end}\",\n      permalink = \"/-/blob/{commit}/{file}#L{line_start}-{line_end}\",\n      commit = \"/-/commit/{commit}\",\n    },\n    [\"bitbucket%.org\"] = {\n      branch = \"/src/{branch}\",\n      file = \"/src/{branch}/{file}#lines-{line_start}-L{line_end}\",\n      permalink = \"/src/{commit}/{file}#lines-{line_start}-L{line_end}\",\n      commit = \"/commits/{commit}\",\n    },\n    [\"git.sr.ht\"] = {\n      branch = \"/tree/{branch}\",\n      file = \"/tree/{branch}/item/{file}\",\n      permalink = \"/tree/{commit}/item/{file}#L{line_start}\",\n      commit = \"/commit/{commit}\",\n    },\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.gitbrowse.Fields\n---@field branch? string\n---@field file? string\n---@field line_start? number\n---@field line_end? number\n---@field commit? string\n---@field line_count? number\n```\n\n## 📦 Module\n\n### `Snacks.gitbrowse()`\n\n```lua\n---@type fun(opts?: snacks.gitbrowse.Config)\nSnacks.gitbrowse()\n```\n\n### `Snacks.gitbrowse.get_url()`\n\n```lua\n---@param repo string\n---@param fields snacks.gitbrowse.Fields\n---@param opts? snacks.gitbrowse.Config\nSnacks.gitbrowse.get_url(repo, fields, opts)\n```\n\n### `Snacks.gitbrowse.open()`\n\n```lua\n---@param opts? snacks.gitbrowse.Config\nSnacks.gitbrowse.open(opts)\n```\n"
  },
  {
    "path": "docs/health.md",
    "content": "# 🍿 health\n\n<!-- docgen -->\n\n## 📚 Types\n\n```lua\n---@class snacks.health.Tool\n---@field cmd string|string[]\n---@field version? string|false\n---@field enabled? boolean\n```\n\n```lua\n---@alias snacks.health.Tool.spec (string|snacks.health.Tool)[]|snacks.health.Tool|string\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.health\n---@field ok fun(msg: string)\n---@field warn fun(msg: string)\n---@field error fun(msg: string)\n---@field info fun(msg: string)\n---@field start fun(msg: string)\nSnacks.health = {}\n```\n\n### `Snacks.health.check()`\n\n```lua\nSnacks.health.check()\n```\n\n### `Snacks.health.has_lang()`\n\nCheck if the given languages are available in treesitter\n\n```lua\n---@param langs string[]|string\nSnacks.health.has_lang(langs)\n```\n\n### `Snacks.health.have_tool()`\n\nCheck if any of the tools are available, with an optional version check\n\n```lua\n---@param tools snacks.health.Tool.spec\nSnacks.health.have_tool(tools)\n```\n"
  },
  {
    "path": "docs/image.md",
    "content": "# 🍿 image\n\n![Image](https://github.com/user-attachments/assets/4e8a686c-bf41-4989-9d74-1641ecf2835f)\n\n## ✨ Features\n\n- Image viewer using the [Kitty Graphics Protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/).\n- open images in a wide range of formats:\n  `pdf`, `png`, `jpg`, `jpeg`, `gif`, `bmp`, `webp`, `tiff`, `heic`, `avif`, `mp4`, `mov`, `avi`, `mkv`, `webm`\n- Supports inline image rendering in:\n  `markdown`, `html`, `norg`, `tsx`, `javascript`, `css`, `vue`, `svelte`, `scss`, `latex`, `typst`\n- LaTex math expressions in `markdown` and `latex` documents\n\nTerminal support:\n\n- [kitty](https://sw.kovidgoyal.net/kitty/)\n- [ghostty](https://ghostty.org/)\n- [wezterm](https://wezfurlong.org/wezterm/)\n  Wezterm has only limited support for the kitty graphics protocol.\n  Inline image rendering is not supported.\n- [tmux](https://github.com/tmux/tmux)\n  Snacks automatically tries to enable `allow-passthrough=on` for tmux,\n  but you may need to enable it manually in your tmux configuration.\n- [zellij](https://github.com/zellij-org/zellij) is **not** supported,\n  since they don't have any support for passthrough\n\nImage will be transferred to the terminal by filename or by sending the image\ndate in case `ssh` is detected.\n\nIn some cases you may need to force snacks to detect or not detect a certain\nenvironment. You can do this by setting `SNACKS_${ENV_NAME}` to `true` or `false`.\n\nFor example, to force detection of **ghostty** you can set `SNACKS_GHOSTTY=true`.\n\nIn order to automatically display the image when opening an image file,\nor to have imaged displayed in supported document formats like `markdown` or `html`,\nyou need to enable the `image` plugin in your `snacks` config.\n\n[ImageMagick](https://imagemagick.org/index.php) is required to convert images\nto the supported formats (all except PNG).\n\nIn case of issues, make sure to run `:checkhealth snacks`.\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    image = {\n      -- your image configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.image.Config\n---@field enabled? boolean enable image viewer\n---@field wo? vim.wo|{} options for windows showing the image\n---@field bo? vim.bo|{} options for the image buffer\n---@field formats? string[]\n--- Resolves a reference to an image with src in a file (currently markdown only).\n--- Return the absolute path or url to the image.\n--- When `nil`, the path is resolved relative to the file.\n---@field resolve? fun(file: string, src: string): string?\n---@field convert? snacks.image.convert.Config\n{\n  formats = {\n    \"png\",\n    \"jpg\",\n    \"jpeg\",\n    \"gif\",\n    \"bmp\",\n    \"webp\",\n    \"tiff\",\n    \"heic\",\n    \"avif\",\n    \"mp4\",\n    \"mov\",\n    \"avi\",\n    \"mkv\",\n    \"webm\",\n    \"pdf\",\n    \"icns\",\n  },\n  force = false, -- try displaying the image, even if the terminal does not support it\n  doc = {\n    -- enable image viewer for documents\n    -- a treesitter parser must be available for the enabled languages.\n    enabled = true,\n    -- render the image inline in the buffer\n    -- if your env doesn't support unicode placeholders, this will be disabled\n    -- takes precedence over `opts.float` on supported terminals\n    inline = true,\n    -- render the image in a floating window\n    -- only used if `opts.inline` is disabled\n    float = true,\n    max_width = 80,\n    max_height = 40,\n    -- Set to `true`, to conceal the image text when rendering inline.\n    -- (experimental)\n    ---@param lang string tree-sitter language\n    ---@param type snacks.image.Type image type\n    conceal = function(lang, type)\n      -- only conceal math expressions\n      return type == \"math\"\n    end,\n  },\n  img_dirs = { \"img\", \"images\", \"assets\", \"static\", \"public\", \"media\", \"attachments\" },\n  -- window options applied to windows displaying image buffers\n  -- an image buffer is a buffer with `filetype=image`\n  wo = {\n    wrap = false,\n    number = false,\n    relativenumber = false,\n    cursorcolumn = false,\n    signcolumn = \"no\",\n    foldcolumn = \"0\",\n    list = false,\n    spell = false,\n    statuscolumn = \"\",\n  },\n  cache = vim.fn.stdpath(\"cache\") .. \"/snacks/image\",\n  debug = {\n    request = false,\n    convert = false,\n    placement = false,\n  },\n  env = {},\n  -- icons used to show where an inline image is located that is\n  -- rendered below the text.\n  icons = {\n    math = \"󰪚 \",\n    chart = \"󰄧 \",\n    image = \" \",\n  },\n  ---@class snacks.image.convert.Config\n  convert = {\n    notify = false, -- show a notification on error\n    ---@type snacks.image.args\n    mermaid = function()\n      local theme = vim.o.background == \"light\" and \"neutral\" or \"dark\"\n      return { \"-i\", \"{src}\", \"-o\", \"{file}\", \"-b\", \"transparent\", \"-t\", theme, \"-s\", \"{scale}\" }\n    end,\n    ---@type table<string,snacks.image.args>\n    magick = {\n      default = { \"{src}[0]\", \"-scale\", \"1920x1080>\" }, -- default for raster images\n      vector = { \"-density\", 192, \"{src}[{page}]\" }, -- used by vector images like svg\n      math = { \"-density\", 192, \"{src}[{page}]\", \"-trim\" },\n      pdf = { \"-density\", 192, \"{src}[{page}]\", \"-background\", \"white\", \"-alpha\", \"remove\", \"-trim\" },\n    },\n  },\n  math = {\n    enabled = true, -- enable math expression rendering\n    -- in the templates below, `${header}` comes from any section in your document,\n    -- between a start/end header comment. Comment syntax is language-specific.\n    -- * start comment: `// snacks: header start`\n    -- * end comment:   `// snacks: header end`\n    typst = {\n      tpl = [[\n        #set page(width: auto, height: auto, margin: (x: 2pt, y: 2pt))\n        #show math.equation.where(block: false): set text(top-edge: \"bounds\", bottom-edge: \"bounds\")\n        #set text(size: 12pt, fill: rgb(\"${color}\"))\n        ${header}\n        ${content}]],\n    },\n    latex = {\n      font_size = \"Large\", -- see https://www.sascha-frank.com/latex-font-size.html\n      -- for latex documents, the doc packages are included automatically,\n      -- but you can add more packages here. Useful for markdown documents.\n      packages = { \"amsmath\", \"amssymb\", \"amsfonts\", \"amscd\", \"mathtools\" },\n      tpl = [[\n        \\documentclass[preview,border=0pt,varwidth,12pt]{standalone}\n        \\usepackage{${packages}}\n        \\begin{document}\n        ${header}\n        { \\${font_size} \\selectfont\n          \\color[HTML]{${color}}\n        ${content}}\n        \\end{document}]],\n    },\n  },\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `snacks_image`\n\n```lua\n{\n  relative = \"cursor\",\n  border = true,\n  focusable = false,\n  backdrop = false,\n  row = 1,\n  col = 1,\n  -- width/height are automatically set by the image size unless specified below\n}\n```\n\n## 📚 Types\n\n```lua\n---@alias snacks.image.Size {width: number, height: number}\n---@alias snacks.image.Pos {[1]: number, [2]: number}\n---@alias snacks.image.Loc snacks.image.Pos|snacks.image.Size|{zindex?: number}\n---@alias snacks.image.Type \"image\"|\"math\"|\"chart\"\n```\n\n```lua\n---@class snacks.image.Env\n---@field name string\n---@field env? table<string, string|true>\n---@field terminal? string\n---@field supported? boolean default: false\n---@field placeholders? boolean default: false\n---@field setup? fun(): boolean?\n---@field transform? fun(data: string): string\n---@field detected? boolean\n---@field remote? boolean this is a remote client, so full transfer of the image data is required\n```\n\n```lua\n---@class snacks.image.Opts\n---@field pos? snacks.image.Pos (row, col) (1,0)-indexed. defaults to the top-left corner\n---@field range? Range4\n---@field conceal? boolean\n---@field inline? boolean render the image inline in the buffer\n---@field width? number\n---@field min_width? number\n---@field max_width? number\n---@field height? number\n---@field min_height? number\n---@field max_height? number\n---@field on_update? fun(placement: snacks.image.Placement)\n---@field on_update_pre? fun(placement: snacks.image.Placement)\n---@field type? snacks.image.Type\n---@field auto_resize? boolean\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.image\n---@field terminal snacks.image.terminal\n---@field image snacks.Image\n---@field placement snacks.image.Placement\n---@field util snacks.image.util\n---@field buf snacks.image.buf\n---@field doc snacks.image.doc\n---@field convert snacks.image.convert\n---@field inline snacks.image.inline\nSnacks.image = {}\n```\n\n### `Snacks.image.hover()`\n\nShow the image at the cursor in a floating window\n\n```lua\nSnacks.image.hover()\n```\n\n### `Snacks.image.langs()`\n\n```lua\n---@return string[]\nSnacks.image.langs()\n```\n\n### `Snacks.image.supports()`\n\nCheck if the file format is supported and the terminal supports the kitty graphics protocol\n\n```lua\n---@param file string\nSnacks.image.supports(file)\n```\n\n### `Snacks.image.supports_file()`\n\nCheck if the file format is supported\n\n```lua\n---@param file string\nSnacks.image.supports_file(file)\n```\n\n### `Snacks.image.supports_terminal()`\n\nCheck if the terminal supports the kitty graphics protocol\n\n```lua\nSnacks.image.supports_terminal()\n```\n"
  },
  {
    "path": "docs/indent.md",
    "content": "# 🍿 indent\n\nVisualize indent guides and scopes based on treesitter or indent.\n\nSimilar plugins:\n\n- [indent-blankline.nvim](https://github.com/lukas-reineke/indent-blankline.nvim)\n- [mini.indentscope](https://github.com/nvim-mini/mini.indentscope)\n\n![image](https://github.com/user-attachments/assets/56a99495-05ab-488e-9619-574cb7ff2b7d)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    indent = {\n      -- your indent configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.indent.Config\n---@field enabled? boolean\n{\n  indent = {\n    priority = 1,\n    enabled = true, -- enable indent guides\n    char = \"│\",\n    only_scope = false, -- only show indent guides of the scope\n    only_current = false, -- only show indent guides in the current window\n    hl = \"SnacksIndent\", ---@type string|string[] hl groups for indent guides\n    -- can be a list of hl groups to cycle through\n    -- hl = {\n    --     \"SnacksIndent1\",\n    --     \"SnacksIndent2\",\n    --     \"SnacksIndent3\",\n    --     \"SnacksIndent4\",\n    --     \"SnacksIndent5\",\n    --     \"SnacksIndent6\",\n    --     \"SnacksIndent7\",\n    --     \"SnacksIndent8\",\n    -- },\n  },\n  -- animate scopes. Enabled by default for Neovim >= 0.10\n  -- Works on older versions but has to trigger redraws during animation.\n  ---@class snacks.indent.animate: snacks.animate.Config\n  ---@field enabled? boolean\n  --- * out: animate outwards from the cursor\n  --- * up: animate upwards from the cursor\n  --- * down: animate downwards from the cursor\n  --- * up_down: animate up or down based on the cursor position\n  ---@field style? \"out\"|\"up_down\"|\"down\"|\"up\"\n  animate = {\n    enabled = vim.fn.has(\"nvim-0.10\") == 1,\n    style = \"out\",\n    easing = \"linear\",\n    duration = {\n      step = 20, -- ms per step\n      total = 500, -- maximum duration\n    },\n  },\n  ---@class snacks.indent.Scope.Config: snacks.scope.Config\n  scope = {\n    enabled = true, -- enable highlighting the current scope\n    priority = 200,\n    char = \"│\",\n    underline = false, -- underline the start of the scope\n    only_current = false, -- only show scope in the current window\n    hl = \"SnacksIndentScope\", ---@type string|string[] hl group for scopes\n  },\n  chunk = {\n    -- when enabled, scopes will be rendered as chunks, except for the\n    -- top-level scope which will be rendered as a scope.\n    enabled = false,\n    -- only show chunk scopes in the current window\n    only_current = false,\n    priority = 200,\n    hl = \"SnacksIndentChunk\", ---@type string|string[] hl group for chunk scopes\n    char = {\n      corner_top = \"┌\",\n      corner_bottom = \"└\",\n      -- corner_top = \"╭\",\n      -- corner_bottom = \"╰\",\n      horizontal = \"─\",\n      vertical = \"│\",\n      arrow = \">\",\n    },\n  },\n  -- filter for buffers to enable indent guides\n  ---@param buf number\n  ---@param win number\n  filter = function(buf, win)\n    return vim.g.snacks_indent ~= false and vim.b[buf].snacks_indent ~= false and vim.bo[buf].buftype == \"\"\n  end,\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.indent.Scope: snacks.scope.Scope\n---@field win number\n---@field step? number\n---@field animate? {from: number, to: number}\n```\n\n## 📦 Module\n\n### `Snacks.indent.debug_win()`\n\n```lua\nSnacks.indent.debug_win()\n```\n\n### `Snacks.indent.disable()`\n\nDisable indent guides\n\n```lua\nSnacks.indent.disable()\n```\n\n### `Snacks.indent.enable()`\n\nEnable indent guides\n\n```lua\nSnacks.indent.enable()\n```\n"
  },
  {
    "path": "docs/init.md",
    "content": "# 🍿 init\n\n<!-- docgen -->\n\n## ⚙️ Config\n\n```lua\n---@class snacks.Config\n---@field animate? snacks.animate.Config\n---@field bigfile? snacks.bigfile.Config\n---@field dashboard? snacks.dashboard.Config\n---@field dim? snacks.dim.Config\n---@field explorer? snacks.explorer.Config\n---@field gh? snacks.gh.Config\n---@field gitbrowse? snacks.gitbrowse.Config\n---@field image? snacks.image.Config\n---@field indent? snacks.indent.Config\n---@field input? snacks.input.Config\n---@field layout? snacks.layout.Config\n---@field lazygit? snacks.lazygit.Config\n---@field notifier? snacks.notifier.Config\n---@field picker? snacks.picker.Config\n---@field profiler? snacks.profiler.Config\n---@field quickfile? snacks.quickfile.Config\n---@field scope? snacks.scope.Config\n---@field scratch? snacks.scratch.Config\n---@field scroll? snacks.scroll.Config\n---@field statuscolumn? snacks.statuscolumn.Config\n---@field terminal? snacks.terminal.Config\n---@field toggle? snacks.toggle.Config\n---@field win? snacks.win.Config\n---@field words? snacks.words.Config\n---@field zen? snacks.zen.Config\n---@field styles? table<string, snacks.win.Config>\n---@field image? snacks.image.Config|{}\n{\n  image = {\n    -- define these here, so that we don't need to load the image module\n    formats = {\n      \"png\",\n      \"jpg\",\n      \"jpeg\",\n      \"gif\",\n      \"bmp\",\n      \"webp\",\n      \"tiff\",\n      \"heic\",\n      \"avif\",\n      \"mp4\",\n      \"mov\",\n      \"avi\",\n      \"mkv\",\n      \"webm\",\n      \"pdf\",\n      \"icns\",\n    },\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.Config.base\n---@field example? string\n---@field config? fun(opts: table, defaults: table)\n```\n\n## 📦 Module\n\n```lua\n---@class Snacks\n---@field animate snacks.animate\n---@field bigfile snacks.bigfile\n---@field bufdelete snacks.bufdelete\n---@field dashboard snacks.dashboard\n---@field debug snacks.debug\n---@field dim snacks.dim\n---@field explorer snacks.explorer\n---@field gh snacks.gh\n---@field git snacks.git\n---@field gitbrowse snacks.gitbrowse\n---@field health snacks.health\n---@field image snacks.image\n---@field indent snacks.indent\n---@field input snacks.input\n---@field keymap snacks.keymap\n---@field layout snacks.layout\n---@field lazygit snacks.lazygit\n---@field meta snacks.meta\n---@field notifier snacks.notifier\n---@field notify snacks.notify\n---@field picker snacks.picker\n---@field profiler snacks.profiler\n---@field quickfile snacks.quickfile\n---@field rename snacks.rename\n---@field scope snacks.scope\n---@field scratch snacks.scratch\n---@field scroll snacks.scroll\n---@field statuscolumn snacks.statuscolumn\n---@field terminal snacks.terminal\n---@field toggle snacks.toggle\n---@field util snacks.util\n---@field win snacks.win\n---@field words snacks.words\n---@field zen snacks.zen\nSnacks = {}\n```\n\n### `Snacks.init.config.example()`\n\nGet an example config from the docs/examples directory.\n\n```lua\n---@param snack string\n---@param name string\n---@param opts? table\nSnacks.init.config.example(snack, name, opts)\n```\n\n### `Snacks.init.config.get()`\n\n```lua\n---@generic T: table\n---@param snack string\n---@param defaults T\n---@param ... T[]\n---@return T\nSnacks.init.config.get(snack, defaults, ...)\n```\n\n### `Snacks.init.config.merge()`\n\nMerges the values similar to vim.tbl_deep_extend with the **force** behavior,\nbut the values can be any type\n\n```lua\n---@generic T\n---@param ... T\n---@return T\nSnacks.init.config.merge(...)\n```\n\n### `Snacks.init.config.style()`\n\nRegister a new window style config.\n\n```lua\n---@param name string\n---@param defaults snacks.win.Config|{}\n---@return string\nSnacks.init.config.style(name, defaults)\n```\n\n### `Snacks.init.setup()`\n\n```lua\n---@param opts snacks.Config?\nSnacks.init.setup(opts)\n```\n"
  },
  {
    "path": "docs/input.md",
    "content": "# 🍿 input\n\nBetter `vim.ui.input`.\n\n![image](https://github.com/user-attachments/assets/f7579302-bea1-4f1c-8b3b-723c3f4ca04b)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    input = {\n      -- your input configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.input.Config\n---@field enabled? boolean\n---@field win? snacks.win.Config|{}\n---@field icon? string\n---@field icon_pos? snacks.input.Pos\n---@field prompt_pos? snacks.input.Pos\n{\n  icon = \" \",\n  icon_hl = \"SnacksInputIcon\",\n  icon_pos = \"left\",\n  prompt_pos = \"title\",\n  win = { style = \"input\" },\n  expand = true,\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `input`\n\n```lua\n{\n  backdrop = false,\n  position = \"float\",\n  border = true,\n  title_pos = \"center\",\n  height = 1,\n  width = 60,\n  relative = \"editor\",\n  noautocmd = true,\n  row = 2,\n  -- relative = \"cursor\",\n  -- row = -3,\n  -- col = 0,\n  wo = {\n    winhighlight = \"NormalFloat:SnacksInputNormal,FloatBorder:SnacksInputBorder,FloatTitle:SnacksInputTitle\",\n    cursorline = false,\n  },\n  bo = {\n    filetype = \"snacks_input\",\n    buftype = \"prompt\",\n  },\n  --- buffer local variables\n  b = {\n    completion = false, -- disable blink completions in input\n  },\n  keys = {\n    n_esc = { \"<esc>\", { \"cmp_close\", \"cancel\" }, mode = \"n\", expr = true },\n    i_esc = { \"<esc>\", { \"cmp_close\", \"stopinsert\" }, mode = \"i\", expr = true },\n    i_cr = { \"<cr>\", { \"cmp_accept\", \"confirm\" }, mode = { \"i\", \"n\" }, expr = true },\n    i_tab = { \"<tab>\", { \"cmp_select_next\", \"cmp\" }, mode = \"i\", expr = true },\n    i_ctrl_w = { \"<c-w>\", \"<c-s-w>\", mode = \"i\", expr = true },\n    i_up = { \"<up>\", { \"hist_up\" }, mode = { \"i\", \"n\" } },\n    i_down = { \"<down>\", { \"hist_down\" }, mode = { \"i\", \"n\" } },\n    q = \"cancel\",\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@alias snacks.input.Pos \"left\"|\"title\"|false\n```\n\n```lua\n---@alias snacks.input.Highlight {[1]:number, [2]:number, [3]:string}\n```\n\n```lua\n---@class snacks.input.Opts: snacks.input.Config,{}\n---@field prompt? string\n---@field default? string\n---@field completion? string\n---@field highlight? fun(text: string): snacks.input.Highlight[]\n```\n\n## 📦 Module\n\n### `Snacks.input()`\n\n```lua\n---@type fun(opts: snacks.input.Opts, on_confirm: fun(value?: string)): snacks.win\nSnacks.input()\n```\n\n### `Snacks.input.disable()`\n\n```lua\nSnacks.input.disable()\n```\n\n### `Snacks.input.enable()`\n\n```lua\nSnacks.input.enable()\n```\n\n### `Snacks.input.input()`\n\n```lua\n---@param opts? snacks.input.Opts\n---@param on_confirm fun(value?: string)\nSnacks.input.input(opts, on_confirm)\n```\n"
  },
  {
    "path": "docs/keymap.md",
    "content": "# 🍿 keymap\n\nBetter `vim.keymap.set` and `vim.keymap.del` with support for filetype-specific and LSP client-aware keymaps.\n\n## ✨ Features\n\n- **Filetype-specific keymaps**: Set keymaps that only apply to specific filetypes\n- **LSP-aware keymaps**: Set keymaps based on LSP client capabilities\n- **Automatic setup**: Keymaps are automatically applied to existing and new buffers\n- **Drop-in replacement**: Same API as `vim.keymap.set/del` with additional options\n- **Smart defaults**: Silent by default\n\n## 🚀 Usage\n\n### Filetype-specific Keymaps\n\nSet keymaps that only apply to buffers with specific filetypes:\n\n```lua\n-- Single filetype - execute the current lua buffer\nSnacks.keymap.set(\"n\", \"<localleader>r\", function()\n  vim.cmd.source()\nend, {\n  ft = \"lua\",\n  desc = \"Run Lua File\",\n})\n\n-- Multiple filetypes\nSnacks.keymap.set(\"n\", \"<leader>t\", \":TestNearest<cr>\", {\n  ft = { \"python\", \"ruby\", \"javascript\" },\n  desc = \"Run Test\",\n})\n```\n\n### LSP-aware Keymaps\n\nSet keymaps based on LSP client capabilities:\n\n```lua\n-- Set keymap for buffers with any LSP that supports code actions\nSnacks.keymap.set(\"n\", \"<leader>ca\", vim.lsp.buf.code_action, {\n  lsp = { method = \"textDocument/codeAction\" },\n  desc = \"Code Action\",\n})\n\n-- Set keymap for buffers with a specific LSP client\nSnacks.keymap.set(\"n\", \"<leader>co\", function()\n  vim.lsp.buf.code_action({\n    apply = true,\n    context = {\n      only = { \"source.organizeImports\" },\n      diagnostics = {},\n    },\n  })\nend, {\n  lsp = { name = \"vtsls\" },\n  desc = \"Organize Imports\",\n})\n\n-- Set keymap for buffers with LSP that supports definitions\nSnacks.keymap.set(\"n\", \"gd\", vim.lsp.buf.definition, {\n  lsp = { method = \"textDocument/definition\" },\n  desc = \"Go to Definition\",\n})\n```\n\n### Standard Keymaps\n\nWorks exactly like `vim.keymap.set` without special options:\n\n```lua\nSnacks.keymap.set(\"n\", \"<leader>w\", \":w<cr>\", { desc = \"Save\" })\nSnacks.keymap.set({ \"n\", \"v\" }, \"<leader>y\", '\"+y', { desc = \"Copy to clipboard\" })\n```\n\n### Deleting Keymaps\n\n```lua\n-- Delete a standard keymap\nSnacks.keymap.del(\"n\", \"<leader>w\")\n\n-- Delete a filetype-specific keymap\nSnacks.keymap.del(\"n\", \"<leader><leader>\", { ft = \"lua\" })\n\n-- Delete an LSP-aware keymap\nSnacks.keymap.del(\"n\", \"<leader>ca\", { lsp = { method = \"textDocument/codeAction\" } })\n```\n\n<!-- docgen -->\n\n## 📚 Types\n\n```lua\n---@class snacks.keymap.set.Opts: vim.keymap.set.Opts\n---@field ft? string|string[] Filetype(s) to set the keymap for.\n---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.\n---@field enabled? boolean|fun(buf?:number): boolean condition to enable the keymap.\n```\n\n```lua\n---@class snacks.keymap.del.Opts: vim.keymap.del.Opts\n---@field buffer? boolean|number If true or 0, use the current buffer.\n---@field ft? string|string[] Filetype(s) to set the keymap for.\n---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.\n```\n\n```lua\n---@class snacks.Keymap\n---@field id number           Unique ID for the keymap.\n---@field key string          Unique key for the keymap, in the format \"mode:lhs\".\n---@field mode string         Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n---@field lhs string          Left-hand side |{lhs}| of the mapping.\n---@field rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function.\n---@field lsp? vim.lsp.get_clients.Filter\n---@field opts? snacks.keymap.set.Opts\n---@field enabled fun(buf:number): boolean\n```\n\n## 📦 Module\n\n### `Snacks.keymap.del()`\n\n```lua\n---@param mode string|string[] Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n---@param lhs string           Left-hand side |{lhs}| of the mapping.\n---@param opts? snacks.keymap.del.Opts\nSnacks.keymap.del(mode, lhs, opts)\n```\n\n### `Snacks.keymap.set()`\n\n```lua\n---@param mode string|string[] Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n---@param lhs string           Left-hand side |{lhs}| of the mapping.\n---@param rhs string|function  Right-hand side |{rhs}| of the mapping, can be a Lua function.\n---@param opts? snacks.keymap.set.Opts\nSnacks.keymap.set(mode, lhs, rhs, opts)\n```\n"
  },
  {
    "path": "docs/layout.md",
    "content": "# 🍿 layout\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    layout = {\n      -- your layout configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.layout.Config\n---@field show? boolean show the layout on creation (default: true)\n---@field wins table<string, snacks.win> windows to include in the layout\n---@field layout snacks.layout.Box layout definition\n---@field fullscreen? boolean open in fullscreen\n---@field hidden? string[] list of windows that will be excluded from the layout (but can be toggled)\n---@field on_update? fun(layout: snacks.layout)\n---@field on_update_pre? fun(layout: snacks.layout)\n---@field on_close? fun(layout: snacks.layout)\n{\n  layout = {\n    width = 0.6,\n    height = 0.6,\n    zindex = 50,\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.layout.Win: snacks.win.Config,{}\n---@field depth? number\n---@field win string layout window name\n```\n\n```lua\n---@class snacks.layout.Box: snacks.layout.Win,{}\n---@field box \"horizontal\" | \"vertical\"\n---@field id? number\n---@field [number] snacks.layout.Win | snacks.layout.Box children\n```\n\n```lua\n---@alias snacks.layout.Widget snacks.layout.Win | snacks.layout.Box\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.layout\n---@field opts snacks.layout.Config\n---@field root snacks.win\n---@field wins table<string, snacks.win|{enabled?:boolean, layout?:boolean}>\n---@field box_wins snacks.win[]\n---@field win_opts table<string, snacks.win.Config>\n---@field closed? boolean\n---@field split? boolean\n---@field screenpos number[]?\nSnacks.layout = {}\n```\n\n### `Snacks.layout.new()`\n\n```lua\n---@param opts snacks.layout.Config\nSnacks.layout.new(opts)\n```\n\n### `layout:close()`\n\nClose the layout\n\n```lua\n---@param opts? {wins?: boolean}\nlayout:close(opts)\n```\n\n### `layout:each()`\n\n```lua\n---@param cb fun(widget: snacks.layout.Widget, parent?: snacks.layout.Box)\n---@param opts? {wins?:boolean, boxes?:boolean, box?:snacks.layout.Box}\nlayout:each(cb, opts)\n```\n\n### `layout:hide()`\n\n```lua\nlayout:hide()\n```\n\n### `layout:is_enabled()`\n\nCheck if the window has been used in the layout\n\n```lua\n---@param w string\nlayout:is_enabled(w)\n```\n\n### `layout:is_hidden()`\n\nCheck if a window is hidden\n\n```lua\n---@param win string\nlayout:is_hidden(win)\n```\n\n### `layout:maximize()`\n\nToggle fullscreen\n\n```lua\nlayout:maximize()\n```\n\n### `layout:needs_layout()`\n\n```lua\n---@param win string\nlayout:needs_layout(win)\n```\n\n### `layout:show()`\n\nShow the layout\n\n```lua\nlayout:show()\n```\n\n### `layout:toggle()`\n\nToggle a window\n\n```lua\n---@param win string\n---@param enable? boolean\n---@param on_update? fun(enabled: boolean) called when the layout will be updated\nlayout:toggle(win, enable, on_update)\n```\n\n### `layout:unhide()`\n\n```lua\nlayout:unhide()\n```\n\n### `layout:valid()`\n\nCheck if layout is valid (visible)\n\n```lua\nlayout:valid()\n```\n"
  },
  {
    "path": "docs/lazygit.md",
    "content": "# 🍿 lazygit\n\nAutomatically configures lazygit with a theme generated based on your Neovim colorscheme\nand integrate edit with the current neovim instance.\n\n![image](https://github.com/user-attachments/assets/5e5ca232-af65-4ebc-b0ca-02bc9c33d23d)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    lazygit = {\n      -- your lazygit configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.lazygit.Config: snacks.terminal.Opts\n---@field args? string[]\n---@field theme? snacks.lazygit.Theme\n{\n  -- automatically configure lazygit to use the current colorscheme\n  -- and integrate edit with the current neovim instance\n  configure = true,\n  -- extra configuration for lazygit that will be merged with the default\n  -- snacks does NOT have a full yaml parser, so if you need `\"test\"` to appear with the quotes\n  -- you need to double quote it: `\"\\\"test\\\"\"`\n  config = {\n    os = { editPreset = \"nvim-remote\" },\n    gui = {\n      -- set to an empty string \"\" to disable icons\n      nerdFontsVersion = \"3\",\n    },\n  },\n  theme_path = svim.fs.normalize(vim.fn.stdpath(\"cache\") .. \"/lazygit-theme.yml\"),\n  -- Theme for lazygit\n  theme = {\n    [241]                      = { fg = \"Special\" },\n    activeBorderColor          = { fg = \"MatchParen\", bold = true },\n    cherryPickedCommitBgColor  = { fg = \"Identifier\" },\n    cherryPickedCommitFgColor  = { fg = \"Function\" },\n    defaultFgColor             = { fg = \"Normal\" },\n    inactiveBorderColor        = { fg = \"FloatBorder\" },\n    optionsTextColor           = { fg = \"Function\" },\n    searchingActiveBorderColor = { fg = \"MatchParen\", bold = true },\n    selectedLineBgColor        = { bg = \"Visual\" }, -- set to `default` to have no background colour\n    unstagedChangesColor       = { fg = \"DiagnosticError\" },\n  },\n  win = {\n    style = \"lazygit\",\n  },\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `lazygit`\n\n```lua\n{}\n```\n\n## 📚 Types\n\n```lua\n---@alias snacks.lazygit.Color {fg?:string, bg?:string, bold?:boolean}\n```\n\n```lua\n---@class snacks.lazygit.Theme: table<number, snacks.lazygit.Color>\n---@field activeBorderColor snacks.lazygit.Color\n---@field cherryPickedCommitBgColor snacks.lazygit.Color\n---@field cherryPickedCommitFgColor snacks.lazygit.Color\n---@field defaultFgColor snacks.lazygit.Color\n---@field inactiveBorderColor snacks.lazygit.Color\n---@field optionsTextColor snacks.lazygit.Color\n---@field searchingActiveBorderColor snacks.lazygit.Color\n---@field selectedLineBgColor snacks.lazygit.Color\n---@field unstagedChangesColor snacks.lazygit.Color\n```\n\n## 📦 Module\n\n### `Snacks.lazygit()`\n\n```lua\n---@type fun(opts?: snacks.lazygit.Config): snacks.win\nSnacks.lazygit()\n```\n\n### `Snacks.lazygit.log()`\n\nOpens lazygit with the log view\n\n```lua\n---@param opts? snacks.lazygit.Config\nSnacks.lazygit.log(opts)\n```\n\n### `Snacks.lazygit.log_file()`\n\nOpens lazygit with the log of the current file\n\n```lua\n---@param opts? snacks.lazygit.Config|{}\nSnacks.lazygit.log_file(opts)\n```\n\n### `Snacks.lazygit.open()`\n\nOpens lazygit, properly configured to use the current colorscheme\nand integrate with the current neovim instance\n\n```lua\n---@param opts? snacks.lazygit.Config\nSnacks.lazygit.open(opts)\n```\n"
  },
  {
    "path": "docs/meta.md",
    "content": "# 🍿 meta\n\nMeta functions for Snacks\n\n<!-- docgen -->\n\n## 📚 Types\n\n```lua\n---@class snacks.meta.Meta\n---@field desc string\n---@field needs_setup? boolean\n---@field hide? boolean\n---@field readme? boolean\n---@field docs? boolean\n---@field health? boolean\n---@field types? boolean\n---@field config? boolean\n---@field merge? { [string|number]: string }\n```\n\n```lua\n---@class snacks.meta.Plugin\n---@field name string\n---@field file string\n---@field meta snacks.meta.Meta\n---@field health? fun()\n```\n\n## 📦 Module\n\n### `Snacks.meta.file()`\n\n```lua\nSnacks.meta.file(name)\n```\n\n### `Snacks.meta.get()`\n\nGet the metadata for all snacks plugins\n\n```lua\n---@return snacks.meta.Plugin[]\nSnacks.meta.get()\n```\n"
  },
  {
    "path": "docs/notifier.md",
    "content": "# 🍿 notifier\n\n![image](https://github.com/user-attachments/assets/b89eb279-08fb-40b2-9330-9a77014b9389)\n\n## Notification History\n\n![image](https://github.com/user-attachments/assets/0dc449f4-b275-49e4-a25f-f58efcba3079)\n\n## 💡 Examples\n\n<details><summary>Replace a notification</summary>\n\n```lua\n-- to replace an existing notification just use the same id.\n-- you can also use the return value of the notify function as id.\nfor i = 1, 10 do\n  vim.defer_fn(function()\n    vim.notify(\"Hello \" .. i, \"info\", { id = \"test\" })\n  end, i * 500)\nend\n```\n\n</details>\n\n<details><summary>Simple LSP Progress</summary>\n\n```lua\nvim.api.nvim_create_autocmd(\"LspProgress\", {\n  ---@param ev {data: {client_id: integer, params: lsp.ProgressParams}}\n  callback = function(ev)\n    local spinner = { \"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\" }\n    vim.notify(vim.lsp.status(), \"info\", {\n      id = \"lsp_progress\",\n      title = \"LSP Progress\",\n      opts = function(notif)\n        notif.icon = ev.data.params.value.kind == \"end\" and \" \"\n          or spinner[math.floor(vim.uv.hrtime() / (1e6 * 80)) % #spinner + 1]\n      end,\n    })\n  end,\n})\n```\n\n</details>\n\n<details><summary>Advanced LSP Progress</summary>\n\n![image](https://github.com/user-attachments/assets/a81b411c-150a-43ec-8def-87270c6f8dde)\n\n```lua\n---@type table<number, {token:lsp.ProgressToken, msg:string, done:boolean}[]>\nlocal progress = vim.defaulttable()\nvim.api.nvim_create_autocmd(\"LspProgress\", {\n  ---@param ev {data: {client_id: integer, params: lsp.ProgressParams}}\n  callback = function(ev)\n    local client = vim.lsp.get_client_by_id(ev.data.client_id)\n    local value = ev.data.params.value --[[@as {percentage?: number, title?: string, message?: string, kind: \"begin\" | \"report\" | \"end\"}]]\n    if not client or type(value) ~= \"table\" then\n      return\n    end\n    local p = progress[client.id]\n\n    for i = 1, #p + 1 do\n      if i == #p + 1 or p[i].token == ev.data.params.token then\n        p[i] = {\n          token = ev.data.params.token,\n          msg = (\"[%3d%%] %s%s\"):format(\n            value.kind == \"end\" and 100 or value.percentage or 100,\n            value.title or \"\",\n            value.message and (\" **%s**\"):format(value.message) or \"\"\n          ),\n          done = value.kind == \"end\",\n        }\n        break\n      end\n    end\n\n    local msg = {} ---@type string[]\n    progress[client.id] = vim.tbl_filter(function(v)\n      return table.insert(msg, v.msg) or not v.done\n    end, p)\n\n    local spinner = { \"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\" }\n    vim.notify(table.concat(msg, \"\\n\"), \"info\", {\n      id = \"lsp_progress\",\n      title = client.name,\n      opts = function(notif)\n        notif.icon = #progress[client.id] == 0 and \" \"\n          or spinner[math.floor(vim.uv.hrtime() / (1e6 * 80)) % #spinner + 1]\n      end,\n    })\n  end,\n})\n```\n\n</details>\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    notifier = {\n      -- your notifier configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.notifier.Config\n---@field enabled? boolean\n---@field keep? fun(notif: snacks.notifier.Notif): boolean # global keep function\n---@field filter? fun(notif: snacks.notifier.Notif): boolean # filter our unwanted notifications (return false to hide)\n{\n  timeout = 3000, -- default timeout in ms\n  width = { min = 40, max = 0.4 },\n  height = { min = 1, max = 0.6 },\n  -- editor margin to keep free. tabline and statusline are taken into account automatically\n  margin = { top = 0, right = 1, bottom = 0 },\n  padding = true, -- add 1 cell of left/right padding to the notification window\n  gap = 0, -- gap between notifications\n  sort = { \"level\", \"added\" }, -- sort by level and time\n  -- minimum log level to display. TRACE is the lowest\n  -- all notifications are stored in history\n  level = vim.log.levels.TRACE,\n  icons = {\n    error = \" \",\n    warn = \" \",\n    info = \" \",\n    debug = \" \",\n    trace = \" \",\n  },\n  keep = function(notif)\n    return vim.fn.getcmdpos() > 0\n  end,\n  ---@type snacks.notifier.style\n  style = \"compact\",\n  top_down = true, -- place notifications from top to bottom\n  date_format = \"%R\", -- time format for notifications\n  -- format for footer when more lines are available\n  -- `%d` is replaced with the number of lines.\n  -- only works for styles with a border\n  ---@type string|boolean\n  more_format = \" ↓ %d lines \",\n  refresh = 50, -- refresh at most every 50ms\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `notification`\n\n```lua\n{\n  border = true,\n  zindex = 100,\n  ft = \"markdown\",\n  wo = {\n    winblend = 5,\n    wrap = false,\n    conceallevel = 2,\n    colorcolumn = \"\",\n  },\n  bo = { filetype = \"snacks_notif\" },\n}\n```\n\n### `notification_history`\n\n```lua\n{\n  border = true,\n  zindex = 100,\n  width = 0.6,\n  height = 0.6,\n  minimal = false,\n  title = \" Notification History \",\n  title_pos = \"center\",\n  ft = \"markdown\",\n  bo = { filetype = \"snacks_notif_history\", modifiable = false },\n  wo = { winhighlight = \"Normal:SnacksNotifierHistory\" },\n  keys = { q = \"close\" },\n}\n```\n\n## 📚 Types\n\nRender styles:\n* compact: use border for icon and title\n* minimal: no border, only icon and message\n* fancy: similar to the default nvim-notify style\n\n```lua\n---@alias snacks.notifier.style snacks.notifier.render|\"compact\"|\"fancy\"|\"minimal\"\n```\n\n### Notifications\n\nNotification options\n\n```lua\n---@class snacks.notifier.Notif.opts\n---@field id? number|string\n---@field msg? string\n---@field level? number|snacks.notifier.level\n---@field title? string\n---@field icon? string\n---@field timeout? number|boolean timeout in ms. Set to 0|false to keep until manually closed\n---@field ft? string\n---@field keep? fun(notif: snacks.notifier.Notif): boolean\n---@field style? snacks.notifier.style\n---@field opts? fun(notif: snacks.notifier.Notif) -- dynamic opts\n---@field hl? snacks.notifier.hl -- highlight overrides\n---@field history? boolean\n```\n\nNotification object\n\n```lua\n---@class snacks.notifier.Notif: snacks.notifier.Notif.opts\n---@field id number|string\n---@field msg string\n---@field win? snacks.win\n---@field icon string\n---@field level snacks.notifier.level\n---@field timeout number\n---@field dirty? boolean\n---@field added number timestamp with nano precision\n---@field updated number timestamp with nano precision\n---@field shown? number timestamp with nano precision\n---@field hidden? number timestamp with nano precision\n---@field layout? { top?: number, width: number, height: number }\n```\n\n### Rendering\n\n```lua\n---@alias snacks.notifier.render fun(buf: number, notif: snacks.notifier.Notif, ctx: snacks.notifier.ctx)\n```\n\n```lua\n---@class snacks.notifier.hl\n---@field title string\n---@field icon string\n---@field border string\n---@field footer string\n---@field msg string\n```\n\n```lua\n---@class snacks.notifier.ctx\n---@field opts snacks.win.Config\n---@field notifier snacks.notifier.Class\n---@field hl snacks.notifier.hl\n---@field ns number\n```\n\n### History\n\n```lua\n---@class snacks.notifier.history\n---@field filter? vim.log.levels|snacks.notifier.level|fun(notif: snacks.notifier.Notif): boolean\n---@field sort? string[] # sort fields, default: {\"added\"}\n---@field reverse? boolean\n```\n\n```lua\n---@alias snacks.notifier.level \"trace\"|\"debug\"|\"info\"|\"warn\"|\"error\"\n```\n\n## 📦 Module\n\n### `Snacks.notifier()`\n\n```lua\n---@type fun(msg: string, level?: snacks.notifier.level|number, opts?: snacks.notifier.Notif.opts): number|string\nSnacks.notifier()\n```\n\n### `Snacks.notifier.get_history()`\n\n```lua\n---@param opts? snacks.notifier.history\nSnacks.notifier.get_history(opts)\n```\n\n### `Snacks.notifier.hide()`\n\n```lua\n---@param id? number|string\nSnacks.notifier.hide(id)\n```\n\n### `Snacks.notifier.notify()`\n\n```lua\n---@param msg string\n---@param level? snacks.notifier.level|number\n---@param opts? snacks.notifier.Notif.opts\nSnacks.notifier.notify(msg, level, opts)\n```\n\n### `Snacks.notifier.show_history()`\n\n```lua\n---@param opts? snacks.notifier.history\nSnacks.notifier.show_history(opts)\n```\n"
  },
  {
    "path": "docs/notify.md",
    "content": "# 🍿 notify\n\n<!-- docgen -->\n\n## 📚 Types\n\n```lua\n---@alias snacks.notify.Opts snacks.notifier.Notif.opts|{once?: boolean}\n```\n\n## 📦 Module\n\n### `Snacks.notify()`\n\n```lua\n---@type fun(msg: string|string[], opts?: snacks.notify.Opts)\nSnacks.notify()\n```\n\n### `Snacks.notify.error()`\n\n```lua\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nSnacks.notify.error(msg, opts)\n```\n\n### `Snacks.notify.info()`\n\n```lua\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nSnacks.notify.info(msg, opts)\n```\n\n### `Snacks.notify.notify()`\n\n```lua\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nSnacks.notify.notify(msg, opts)\n```\n\n### `Snacks.notify.warn()`\n\n```lua\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nSnacks.notify.warn(msg, opts)\n```\n"
  },
  {
    "path": "docs/picker.md",
    "content": "# 🍿 picker\n\nSnacks now comes with a modern fuzzy-finder to navigate the Neovim universe.\n\n![image](https://github.com/user-attachments/assets/b454fc3c-6613-4aa4-9296-f57a8b02bf6d)\n![image](https://github.com/user-attachments/assets/3203aec4-7d75-4bca-b3d5-18d931277e4e)\n![image](https://github.com/user-attachments/assets/e09d25f8-8559-441c-a0f7-576d2aa57097)\n![image](https://github.com/user-attachments/assets/291dcf63-0c1d-4e9a-97cb-dd5503660e6f)\n![image](https://github.com/user-attachments/assets/1aba5737-a650-4a00-94f8-033b7d8d21ba)\n![image](https://github.com/user-attachments/assets/976e0ed8-eb80-43e1-93ac-4683136c0a3c)\n\n## ✨ Features\n\n- 🔎 over 40 [built-in sources](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#-sources)\n- 🚀 Fast and powerful fuzzy matching engine that supports the [fzf](https://junegunn.github.io/fzf/search-syntax/) search syntax\n  - additionally supports field searches like `file:lua$ 'function`\n  - `files` and `grep` additionally support adding options like `foo -- -e=lua`\n- 🌲 uses **treesitter** highlighting where it makes sense\n- 🧹 Sane default settings so you can start using it right away\n- 💪 Finders and matchers run asynchronously for maximum performance\n- 🪟 Different [layouts](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#%EF%B8%8F-layouts) to suit your needs, or create your own.\n  Uses [Snacks.layout](https://github.com/folke/snacks.nvim/blob/main/docs/layout.md)\n  under the hood.\n- 💻 Simple API to create your own pickers\n- 📋 Better `vim.ui.select`\n\nSome acknowledgements:\n\n- [fzf-lua](https://github.com/ibhagwan/fzf-lua)\n- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)\n- [mini.pick](https://github.com/nvim-mini/mini.pick)\n\n## 📚 Usage\n\nThe best way to get started is to copy some of the [example configs](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#-examples) below.\n\n```lua\n-- Show all pickers\nSnacks.picker()\n\n-- run files picker (all three are equivalent)\nSnacks.picker.files(opts)\nSnacks.picker.pick(\"files\", opts)\nSnacks.picker.pick({source = \"files\", ...})\n```\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    picker = {\n      -- your picker configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.picker.Config\n---@field multi? (string|snacks.picker.Config)[]\n---@field source? string source name and config to use\n---@field pattern? string|fun(picker:snacks.Picker):string pattern used to filter items by the matcher\n---@field search? string|fun(picker:snacks.Picker):string search string used by finders\n---@field cwd? string current working directory\n---@field live? boolean when true, typing will trigger live searches\n---@field limit? number when set, the finder will stop after finding this number of items. useful for live searches\n---@field limit_live? number when set, the finder will stop after finding this number of items during live searches. useful for performance\n---@field ui_select? boolean set `vim.ui.select` to a snacks picker\n---@field filter? snacks.picker.filter.Config generic filter used by some finders\n--- Source definition\n---@field items? snacks.picker.finder.Item[] items to show instead of using a finder\n---@field format? string|snacks.picker.format|string format function or preset\n---@field finder? string|snacks.picker.finder|snacks.picker.finder.multi finder function or preset\n---@field preview? snacks.picker.preview|string preview function or preset\n---@field matcher? snacks.picker.matcher.Config|{} matcher config\n---@field sort? snacks.picker.sort|snacks.picker.sort.Config sort function or config\n---@field transform? string|snacks.picker.transform transform/filter function\n--- UI\n---@field win? snacks.picker.win.Config\n---@field layout? snacks.picker.layout.Config|string|{}|fun(source:string):(snacks.picker.layout.Config|string)\n---@field icons? snacks.picker.icons\n---@field prompt? string prompt text / icon\n---@field title? string defaults to a capitalized source name\n---@field auto_close? boolean automatically close the picker when focusing another window (defaults to true)\n---@field show_empty? boolean show the picker even when there are no items\n---@field show_delay? number delay (in ms) to wait before showing the picker while no results yet\n---@field focus? \"input\"|\"list\" where to focus when the picker is opened (defaults to \"input\")\n---@field enter? boolean enter the picker when opening it\n---@field toggles? table<string, string|false|snacks.picker.toggle>\n--- Preset options\n---@field previewers? snacks.picker.previewers.Config|{}\n---@field formatters? snacks.picker.formatters.Config|{}\n---@field sources? snacks.picker.sources.Config|{}|table<string, snacks.picker.Config|{}>\n---@field layouts? table<string, snacks.picker.layout.Config>\n--- Actions\n---@field actions? table<string, snacks.picker.Action.spec> actions used by keymaps\n---@field confirm? snacks.picker.Action.spec shortcut for confirm action\n---@field auto_confirm? boolean automatically confirm if there is only one item\n---@field main? snacks.picker.main.Config main editor window config\n---@field on_change? fun(picker:snacks.Picker, item?:snacks.picker.Item) called when the cursor changes\n---@field on_show? fun(picker:snacks.Picker) called when the picker is shown\n---@field on_close? fun(picker:snacks.Picker) called when the picker is closed\n---@field jump? snacks.picker.jump.Config|{}\n--- Other\n---@field config? fun(opts:snacks.picker.Config):snacks.picker.Config? custom config function\n---@field db? snacks.picker.db.Config|{}\n---@field debug? snacks.picker.debug|{}\n{\n  prompt = \" \",\n  sources = {},\n  focus = \"input\",\n  show_delay = 5000,\n  limit_live = 10000,\n  layout = {\n    cycle = true,\n    --- Use the default layout or vertical if the window is too narrow\n    preset = function()\n      return vim.o.columns >= 120 and \"default\" or \"vertical\"\n    end,\n  },\n  ---@class snacks.picker.matcher.Config\n  matcher = {\n    fuzzy = true, -- use fuzzy matching\n    smartcase = true, -- use smartcase\n    ignorecase = true, -- use ignorecase\n    sort_empty = false, -- sort results when the search string is empty\n    filename_bonus = true, -- give bonus for matching file names (last part of the path)\n    file_pos = true, -- support patterns like `file:line:col` and `file:line`\n    -- the bonusses below, possibly require string concatenation and path normalization,\n    -- so this can have a performance impact for large lists and increase memory usage\n    cwd_bonus = false, -- give bonus for matching files in the cwd\n    frecency = false, -- frecency bonus\n    history_bonus = false, -- give more weight to chronological order\n  },\n  sort = {\n    -- default sort is by score, text length and index\n    fields = { \"score:desc\", \"#text\", \"idx\" },\n  },\n  ui_select = true, -- replace `vim.ui.select` with the snacks picker\n  ---@class snacks.picker.formatters.Config\n  formatters = {\n    text = {\n      ft = nil, ---@type string? filetype for highlighting\n    },\n    file = {\n      filename_first = false, -- display filename before the file path\n      --- * left: truncate the beginning of the path\n      --- * center: truncate the middle of the path\n      --- * right: truncate the end of the path\n      ---@type \"left\"|\"center\"|\"right\"\n      truncate = \"center\",\n      min_width = 40, -- minimum length of the truncated path\n      filename_only = false, -- only show the filename\n      icon_width = 2, -- width of the icon (in characters)\n      git_status_hl = true, -- use the git status highlight group for the filename\n    },\n    selected = {\n      show_always = false, -- only show the selected column when there are multiple selections\n      unselected = true, -- use the unselected icon for unselected items\n    },\n    severity = {\n      icons = true, -- show severity icons\n      level = false, -- show severity level\n      ---@type \"left\"|\"right\"\n      pos = \"left\", -- position of the diagnostics\n    },\n  },\n  ---@class snacks.picker.previewers.Config\n  previewers = {\n    diff = {\n      -- fancy: Snacks fancy diff (borders, multi-column line numbers, syntax highlighting)\n      -- syntax: Neovim's built-in diff syntax highlighting\n      -- terminal: external command (git's pager for git commands, `cmd` for other diffs)\n      style = \"fancy\", ---@type \"fancy\"|\"syntax\"|\"terminal\"\n      cmd = { \"delta\" }, -- example for using `delta` as the external diff command\n      ---@type vim.wo?|{} window options for the fancy diff preview window\n      wo = {\n        breakindent = true,\n        wrap = true,\n        linebreak = true,\n        showbreak = \"\",\n      },\n    },\n    git = {\n      args = {}, -- additional arguments passed to the git command. Useful to set pager options usin `-c ...`\n    },\n    file = {\n      max_size = 1024 * 1024, -- 1MB\n      max_line_length = 500, -- max line length\n      ft = nil, ---@type string? filetype for highlighting. Use `nil` for auto detect\n    },\n    man_pager = nil, ---@type string? MANPAGER env to use for `man` preview\n  },\n  ---@class snacks.picker.jump.Config\n  jump = {\n    jumplist = true, -- save the current position in the jumplist\n    tagstack = false, -- save the current position in the tagstack\n    reuse_win = false, -- reuse an existing window if the buffer is already open\n    close = true, -- close the picker when jumping/editing to a location (defaults to true)\n    match = false, -- jump to the first match position. (useful for `lines`)\n  },\n  toggles = {\n    follow = \"f\",\n    hidden = \"h\",\n    ignored = \"i\",\n    modified = \"m\",\n    regex = { icon = \"R\", value = false },\n  },\n  win = {\n    -- input window\n    input = {\n      keys = {\n        -- to close the picker on ESC instead of going to normal mode,\n        -- add the following keymap to your config\n        -- [\"<Esc>\"] = { \"close\", mode = { \"n\", \"i\" } },\n        [\"/\"] = \"toggle_focus\",\n        [\"<C-Down>\"] = { \"history_forward\", mode = { \"i\", \"n\" } },\n        [\"<C-Up>\"] = { \"history_back\", mode = { \"i\", \"n\" } },\n        [\"<C-c>\"] = { \"cancel\", mode = \"i\" },\n        [\"<C-w>\"] = { \"<c-s-w>\", mode = { \"i\" }, expr = true, desc = \"delete word\" },\n        [\"<CR>\"] = { \"confirm\", mode = { \"n\", \"i\" } },\n        [\"<Down>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n        [\"<Esc>\"] = \"cancel\",\n        [\"<S-CR>\"] = { { \"pick_win\", \"jump\" }, mode = { \"n\", \"i\" } },\n        [\"<S-Tab>\"] = { \"select_and_prev\", mode = { \"i\", \"n\" } },\n        [\"<Tab>\"] = { \"select_and_next\", mode = { \"i\", \"n\" } },\n        [\"<Up>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n        [\"<a-d>\"] = { \"inspect\", mode = { \"n\", \"i\" } },\n        [\"<a-f>\"] = { \"toggle_follow\", mode = { \"i\", \"n\" } },\n        [\"<a-h>\"] = { \"toggle_hidden\", mode = { \"i\", \"n\" } },\n        [\"<a-i>\"] = { \"toggle_ignored\", mode = { \"i\", \"n\" } },\n        [\"<a-r>\"] = { \"toggle_regex\", mode = { \"i\", \"n\" } },\n        [\"<a-m>\"] = { \"toggle_maximize\", mode = { \"i\", \"n\" } },\n        [\"<a-p>\"] = { \"toggle_preview\", mode = { \"i\", \"n\" } },\n        [\"<a-w>\"] = { \"cycle_win\", mode = { \"i\", \"n\" } },\n        [\"<c-a>\"] = { \"select_all\", mode = { \"n\", \"i\" } },\n        [\"<c-b>\"] = { \"preview_scroll_up\", mode = { \"i\", \"n\" } },\n        [\"<c-d>\"] = { \"list_scroll_down\", mode = { \"i\", \"n\" } },\n        [\"<c-f>\"] = { \"preview_scroll_down\", mode = { \"i\", \"n\" } },\n        [\"<c-g>\"] = { \"toggle_live\", mode = { \"i\", \"n\" } },\n        [\"<c-j>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n        [\"<c-k>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n        [\"<c-n>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n        [\"<c-p>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n        [\"<c-q>\"] = { \"qflist\", mode = { \"i\", \"n\" } },\n        [\"<c-s>\"] = { \"edit_split\", mode = { \"i\", \"n\" } },\n        [\"<c-t>\"] = { \"tab\", mode = { \"n\", \"i\" } },\n        [\"<c-u>\"] = { \"list_scroll_up\", mode = { \"i\", \"n\" } },\n        [\"<c-v>\"] = { \"edit_vsplit\", mode = { \"i\", \"n\" } },\n        [\"<c-r>#\"] = { \"insert_alt\", mode = \"i\" },\n        [\"<c-r>%\"] = { \"insert_filename\", mode = \"i\" },\n        [\"<c-r><c-a>\"] = { \"insert_cWORD\", mode = \"i\" },\n        [\"<c-r><c-f>\"] = { \"insert_file\", mode = \"i\" },\n        [\"<c-r><c-l>\"] = { \"insert_line\", mode = \"i\" },\n        [\"<c-r><c-p>\"] = { \"insert_file_full\", mode = \"i\" },\n        [\"<c-r><c-w>\"] = { \"insert_cword\", mode = \"i\" },\n        [\"<c-w>H\"] = \"layout_left\",\n        [\"<c-w>J\"] = \"layout_bottom\",\n        [\"<c-w>K\"] = \"layout_top\",\n        [\"<c-w>L\"] = \"layout_right\",\n        [\"?\"] = \"toggle_help_input\",\n        [\"G\"] = \"list_bottom\",\n        [\"gg\"] = \"list_top\",\n        [\"j\"] = \"list_down\",\n        [\"k\"] = \"list_up\",\n        [\"q\"] = \"cancel\",\n      },\n      b = {\n        minipairs_disable = true,\n      },\n    },\n    -- result list window\n    list = {\n      keys = {\n        [\"/\"] = \"toggle_focus\",\n        [\"<2-LeftMouse>\"] = \"confirm\",\n        [\"<CR>\"] = \"confirm\",\n        [\"<Down>\"] = \"list_down\",\n        [\"<Esc>\"] = \"cancel\",\n        [\"<S-CR>\"] = { { \"pick_win\", \"jump\" } },\n        [\"<S-Tab>\"] = { \"select_and_prev\", mode = { \"n\", \"x\" } },\n        [\"<Tab>\"] = { \"select_and_next\", mode = { \"n\", \"x\" } },\n        [\"<Up>\"] = \"list_up\",\n        [\"<a-d>\"] = \"inspect\",\n        [\"<a-f>\"] = \"toggle_follow\",\n        [\"<a-h>\"] = \"toggle_hidden\",\n        [\"<a-i>\"] = \"toggle_ignored\",\n        [\"<a-m>\"] = \"toggle_maximize\",\n        [\"<a-p>\"] = \"toggle_preview\",\n        [\"<a-w>\"] = \"cycle_win\",\n        [\"<c-a>\"] = \"select_all\",\n        [\"<c-b>\"] = \"preview_scroll_up\",\n        [\"<c-d>\"] = \"list_scroll_down\",\n        [\"<c-f>\"] = \"preview_scroll_down\",\n        [\"<c-j>\"] = \"list_down\",\n        [\"<c-k>\"] = \"list_up\",\n        [\"<c-n>\"] = \"list_down\",\n        [\"<c-p>\"] = \"list_up\",\n        [\"<c-q>\"] = \"qflist\",\n        [\"<c-g>\"] = \"print_path\",\n        [\"<c-s>\"] = \"edit_split\",\n        [\"<c-t>\"] = \"tab\",\n        [\"<c-u>\"] = \"list_scroll_up\",\n        [\"<c-v>\"] = \"edit_vsplit\",\n        [\"<c-w>H\"] = \"layout_left\",\n        [\"<c-w>J\"] = \"layout_bottom\",\n        [\"<c-w>K\"] = \"layout_top\",\n        [\"<c-w>L\"] = \"layout_right\",\n        [\"?\"] = \"toggle_help_list\",\n        [\"G\"] = \"list_bottom\",\n        [\"gg\"] = \"list_top\",\n        [\"i\"] = \"focus_input\",\n        [\"j\"] = \"list_down\",\n        [\"k\"] = \"list_up\",\n        [\"q\"] = \"cancel\",\n        [\"zb\"] = \"list_scroll_bottom\",\n        [\"zt\"] = \"list_scroll_top\",\n        [\"zz\"] = \"list_scroll_center\",\n      },\n      wo = {\n        conceallevel = 2,\n        concealcursor = \"nvc\",\n      },\n    },\n    -- preview window\n    preview = {\n      keys = {\n        [\"<Esc>\"] = \"cancel\",\n        [\"q\"] = \"cancel\",\n        [\"i\"] = \"focus_input\",\n        [\"<a-w>\"] = \"cycle_win\",\n      },\n    },\n  },\n  ---@class snacks.picker.icons\n  icons = {\n    files = {\n      enabled = true, -- show file icons\n      dir = \"󰉋 \",\n      dir_open = \"󰝰 \",\n      file = \"󰈔 \"\n    },\n    keymaps = {\n      nowait = \"󰓅 \"\n    },\n    tree = {\n      vertical = \"│ \",\n      middle   = \"├╴\",\n      last     = \"└╴\",\n    },\n    undo = {\n      saved   = \" \",\n    },\n    ui = {\n      live        = \"󰐰 \",\n      hidden      = \"h\",\n      ignored     = \"i\",\n      follow      = \"f\",\n      selected    = \"● \",\n      unselected  = \"○ \",\n      -- selected = \" \",\n    },\n    git = {\n      enabled   = true, -- show git icons\n      commit    = \"󰜘 \", -- used by git log\n      staged    = \"●\", -- staged changes. always overrides the type icons\n      added     = \"\",\n      deleted   = \"\",\n      ignored   = \" \",\n      modified  = \"○\",\n      renamed   = \"\",\n      unmerged  = \" \",\n      untracked = \"?\",\n    },\n    diagnostics = {\n      Error = \" \",\n      Warn  = \" \",\n      Hint  = \" \",\n      Info  = \" \",\n    },\n    lsp = {\n      unavailable = \"\",\n      enabled = \" \",\n      disabled = \" \",\n      attached = \"󰖩 \"\n    },\n    kinds = {\n      Array         = \" \",\n      Boolean       = \"󰨙 \",\n      Class         = \" \",\n      Color         = \" \",\n      Control       = \" \",\n      Collapsed     = \" \",\n      Constant      = \"󰏿 \",\n      Constructor   = \" \",\n      Copilot       = \" \",\n      Enum          = \" \",\n      EnumMember    = \" \",\n      Event         = \" \",\n      Field         = \" \",\n      File          = \" \",\n      Folder        = \" \",\n      Function      = \"󰊕 \",\n      Interface     = \" \",\n      Key           = \" \",\n      Keyword       = \" \",\n      Method        = \"󰊕 \",\n      Module        = \" \",\n      Namespace     = \"󰦮 \",\n      Null          = \" \",\n      Number        = \"󰎠 \",\n      Object        = \" \",\n      Operator      = \" \",\n      Package       = \" \",\n      Property      = \" \",\n      Reference     = \" \",\n      Snippet       = \"󱄽 \",\n      String        = \" \",\n      Struct        = \"󰆼 \",\n      Text          = \" \",\n      TypeParameter = \" \",\n      Unit          = \" \",\n      Unknown        = \" \",\n      Value         = \" \",\n      Variable      = \"󰀫 \",\n    },\n  },\n  ---@class snacks.picker.db.Config\n  db = {\n    -- path to the sqlite3 library\n    -- If not set, it will try to load the library by name.\n    -- On Windows it will download the library from the internet.\n    sqlite3_path = nil, ---@type string?\n  },\n  ---@class snacks.picker.debug\n  debug = {\n    scores = false, -- show scores in the list\n    leaks = false, -- show when pickers don't get garbage collected\n    explorer = false, -- show explorer debug info\n    files = false, -- show file debug info\n    grep = false, -- show file debug info\n    proc = false, -- show proc debug info\n    extmarks = false, -- show extmarks errors\n  },\n}\n```\n\n## 🚀 Examples\n\n### `flash`\n\n```lua\n{\n  \"folke/flash.nvim\",\n  optional = true,\n  specs = {\n    {\n      \"folke/snacks.nvim\",\n      opts = {\n        picker = {\n          win = {\n            input = {\n              keys = {\n                [\"<a-s>\"] = { \"flash\", mode = { \"n\", \"i\" } },\n                [\"s\"] = { \"flash\" },\n              },\n            },\n          },\n          actions = {\n            flash = function(picker)\n              require(\"flash\").jump({\n                pattern = \"^\",\n                label = { after = { 0, 0 } },\n                search = {\n                  mode = \"search\",\n                  exclude = {\n                    function(win)\n                      return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= \"snacks_picker_list\"\n                    end,\n                  },\n                },\n                action = function(match)\n                  local idx = picker.list:row2idx(match.pos[1])\n                  picker.list:_move(idx, true, true)\n                end,\n              })\n            end,\n          },\n        },\n      },\n    },\n  },\n}\n```\n\n### `general`\n\n```lua\n{\n  \"folke/snacks.nvim\",\n  opts = {\n    picker = {},\n    explorer = {},\n  },\n  keys = {\n    -- Top Pickers & Explorer\n    { \"<leader><space>\", function() Snacks.picker.smart() end, desc = \"Smart Find Files\" },\n    { \"<leader>,\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n    { \"<leader>/\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n    { \"<leader>:\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n    { \"<leader>n\", function() Snacks.picker.notifications() end, desc = \"Notification History\" },\n    { \"<leader>e\", function() Snacks.explorer() end, desc = \"File Explorer\" },\n    -- find\n    { \"<leader>fb\", function() Snacks.picker.buffers() end, desc = \"Buffers\" },\n    { \"<leader>fc\", function() Snacks.picker.files({ cwd = vim.fn.stdpath(\"config\") }) end, desc = \"Find Config File\" },\n    { \"<leader>ff\", function() Snacks.picker.files() end, desc = \"Find Files\" },\n    { \"<leader>fg\", function() Snacks.picker.git_files() end, desc = \"Find Git Files\" },\n    { \"<leader>fp\", function() Snacks.picker.projects() end, desc = \"Projects\" },\n    { \"<leader>fr\", function() Snacks.picker.recent() end, desc = \"Recent\" },\n    -- git\n    { \"<leader>gb\", function() Snacks.picker.git_branches() end, desc = \"Git Branches\" },\n    { \"<leader>gl\", function() Snacks.picker.git_log() end, desc = \"Git Log\" },\n    { \"<leader>gL\", function() Snacks.picker.git_log_line() end, desc = \"Git Log Line\" },\n    { \"<leader>gs\", function() Snacks.picker.git_status() end, desc = \"Git Status\" },\n    { \"<leader>gS\", function() Snacks.picker.git_stash() end, desc = \"Git Stash\" },\n    { \"<leader>gd\", function() Snacks.picker.git_diff() end, desc = \"Git Diff (Hunks)\" },\n    { \"<leader>gf\", function() Snacks.picker.git_log_file() end, desc = \"Git Log File\" },\n    -- gh\n    { \"<leader>gi\", function() Snacks.picker.gh_issue() end, desc = \"GitHub Issues (open)\" },\n    { \"<leader>gI\", function() Snacks.picker.gh_issue({ state = \"all\" }) end, desc = \"GitHub Issues (all)\" },\n    { \"<leader>gp\", function() Snacks.picker.gh_pr() end, desc = \"GitHub Pull Requests (open)\" },\n    { \"<leader>gP\", function() Snacks.picker.gh_pr({ state = \"all\" }) end, desc = \"GitHub Pull Requests (all)\" },\n    -- Grep\n    { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n    { \"<leader>sB\", function() Snacks.picker.grep_buffers() end, desc = \"Grep Open Buffers\" },\n    { \"<leader>sg\", function() Snacks.picker.grep() end, desc = \"Grep\" },\n    { \"<leader>sw\", function() Snacks.picker.grep_word() end, desc = \"Visual selection or word\", mode = { \"n\", \"x\" } },\n    -- search\n    { '<leader>s\"', function() Snacks.picker.registers() end, desc = \"Registers\" },\n    { '<leader>s/', function() Snacks.picker.search_history() end, desc = \"Search History\" },\n    { \"<leader>sa\", function() Snacks.picker.autocmds() end, desc = \"Autocmds\" },\n    { \"<leader>sb\", function() Snacks.picker.lines() end, desc = \"Buffer Lines\" },\n    { \"<leader>sc\", function() Snacks.picker.command_history() end, desc = \"Command History\" },\n    { \"<leader>sC\", function() Snacks.picker.commands() end, desc = \"Commands\" },\n    { \"<leader>sd\", function() Snacks.picker.diagnostics() end, desc = \"Diagnostics\" },\n    { \"<leader>sD\", function() Snacks.picker.diagnostics_buffer() end, desc = \"Buffer Diagnostics\" },\n    { \"<leader>sh\", function() Snacks.picker.help() end, desc = \"Help Pages\" },\n    { \"<leader>sH\", function() Snacks.picker.highlights() end, desc = \"Highlights\" },\n    { \"<leader>si\", function() Snacks.picker.icons() end, desc = \"Icons\" },\n    { \"<leader>sj\", function() Snacks.picker.jumps() end, desc = \"Jumps\" },\n    { \"<leader>sk\", function() Snacks.picker.keymaps() end, desc = \"Keymaps\" },\n    { \"<leader>sl\", function() Snacks.picker.loclist() end, desc = \"Location List\" },\n    { \"<leader>sm\", function() Snacks.picker.marks() end, desc = \"Marks\" },\n    { \"<leader>sM\", function() Snacks.picker.man() end, desc = \"Man Pages\" },\n    { \"<leader>sp\", function() Snacks.picker.lazy() end, desc = \"Search for Plugin Spec\" },\n    { \"<leader>sq\", function() Snacks.picker.qflist() end, desc = \"Quickfix List\" },\n    { \"<leader>sR\", function() Snacks.picker.resume() end, desc = \"Resume\" },\n    { \"<leader>su\", function() Snacks.picker.undo() end, desc = \"Undo History\" },\n    { \"<leader>uC\", function() Snacks.picker.colorschemes() end, desc = \"Colorschemes\" },\n    -- LSP\n    { \"gd\", function() Snacks.picker.lsp_definitions() end, desc = \"Goto Definition\" },\n    { \"gD\", function() Snacks.picker.lsp_declarations() end, desc = \"Goto Declaration\" },\n    { \"gr\", function() Snacks.picker.lsp_references() end, nowait = true, desc = \"References\" },\n    { \"gI\", function() Snacks.picker.lsp_implementations() end, desc = \"Goto Implementation\" },\n    { \"gy\", function() Snacks.picker.lsp_type_definitions() end, desc = \"Goto T[y]pe Definition\" },\n    { \"gai\", function() Snacks.picker.lsp_incoming_calls() end, desc = \"C[a]lls Incoming\" },\n    { \"gao\", function() Snacks.picker.lsp_outgoing_calls() end, desc = \"C[a]lls Outgoing\" },\n    { \"<leader>ss\", function() Snacks.picker.lsp_symbols() end, desc = \"LSP Symbols\" },\n    { \"<leader>sS\", function() Snacks.picker.lsp_workspace_symbols() end, desc = \"LSP Workspace Symbols\" },\n  },\n}\n```\n\n### `todo_comments`\n\n```lua\n{\n  \"folke/todo-comments.nvim\",\n  optional = true,\n  keys = {\n    { \"<leader>st\", function() Snacks.picker.todo_comments() end, desc = \"Todo\" },\n    { \"<leader>sT\", function () Snacks.picker.todo_comments({ keywords = { \"TODO\", \"FIX\", \"FIXME\" } }) end, desc = \"Todo/Fix/Fixme\" },\n  },\n}\n```\n\n### `trouble`\n\n```lua\n{\n  \"folke/trouble.nvim\",\n  optional = true,\n  specs = {\n    \"folke/snacks.nvim\",\n    opts = function(_, opts)\n      return vim.tbl_deep_extend(\"force\", opts or {}, {\n        picker = {\n          actions = require(\"trouble.sources.snacks\").actions,\n          win = {\n            input = {\n              keys = {\n                [\"<c-t>\"] = {\n                  \"trouble_open\",\n                  mode = { \"n\", \"i\" },\n                },\n              },\n            },\n          },\n        },\n      })\n    end,\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.picker.resume.Opts\n---@field source? string\n---@field include? string[]\n---@field exclude? string[]\n```\n\n```lua\n---@class snacks.picker.jump.Action: snacks.picker.Action\n---@field cmd? snacks.picker.EditCmd\n```\n\n```lua\n---@class snacks.picker.layout.Action: snacks.picker.Action\n---@field layout? snacks.picker.layout.Config|string\n```\n\n```lua\n---@class snacks.picker.yank.Action: snacks.picker.Action\n---@field reg? string\n---@field field? string\n---@field notify? boolean\n```\n\n```lua\n---@class snacks.picker.insert.Action: snacks.picker.Action\n---@field expr string\n```\n\n```lua\n---@alias snacks.picker.format.resolve fun(max_width:number):snacks.picker.Highlight[]\n---@alias snacks.picker.Extmark vim.api.keyset.set_extmark|{col:number, row?:number, field?:string}\n---@alias snacks.picker.Meta {[string]:any}\n---@alias snacks.picker.Text {[1]:string, [2]:(string|string[])?, virtual?:boolean, field?:string, resolve?:snacks.picker.format.resolve, inline?:boolean}\n---@alias snacks.picker.Highlight snacks.picker.Text|snacks.picker.Extmark|{meta?:snacks.picker.Meta}\n---@alias snacks.picker.format fun(item:snacks.picker.Item, picker:snacks.Picker):snacks.picker.Highlight[]\n---@alias snacks.picker.preview fun(ctx: snacks.picker.preview.ctx):boolean?\n---@alias snacks.picker.sort fun(a:snacks.picker.Item, b:snacks.picker.Item):boolean\n---@alias snacks.picker.transform fun(item:snacks.picker.finder.Item, ctx:snacks.picker.finder.ctx):(boolean|snacks.picker.finder.Item|nil)\n---@alias snacks.picker.Pos {[1]:number, [2]:number}\n---@alias snacks.picker.toggle {icon?:string, enabled?:boolean, value?:boolean}\n```\n\nGeneric filter used by some finders to pre-filter items\n\n```lua\n---@class snacks.picker.filter.Config\n---@field cwd? boolean|string only show files for the given cwd\n---@field buf? boolean|number only show items for the current or given buffer\n---@field paths? table<string, boolean> only show items that include or exclude the given paths\n---@field filter? fun(item:snacks.picker.finder.Item, filter:snacks.picker.Filter):boolean? custom filter function\n---@field transform? fun(picker:snacks.Picker, filter:snacks.picker.Filter):boolean? filter transform. Return `true` to force refresh\n```\n\nThis is only used when using `opts.preview = \"preview\"`.\nIt's a previewer that shows a preview based on the item data.\n\n```lua\n---@class snacks.picker.Item.preview\n---@field text string text to show in the preview buffer\n---@field ft? string optional filetype used tohighlight the preview buffer\n---@field extmarks? snacks.picker.Extmark[] additional extmarks\n---@field loc? boolean set to false to disable showing the item location in the preview\n```\n\n```lua\n---@class snacks.picker.Item\n---@field [string] any\n---@field idx number\n---@field score number\n---@field frecency? number\n---@field score_add? number\n---@field score_mul? number\n---@field source_id? number\n---@field file? string\n---@field text string\n---@field pos? snacks.picker.Pos\n---@field loc? snacks.picker.lsp.Loc\n---@field end_pos? snacks.picker.Pos\n---@field highlights? snacks.picker.Highlight[][]\n---@field preview? snacks.picker.Item.preview\n---@field resolve? fun(item:snacks.picker.Item)\n---@field positions? number[] indices of matched characters in `text`\n```\n\n```lua\n---@class snacks.picker.finder.Item: snacks.picker.Item\n---@field idx? number\n---@field score? number\n```\n\n```lua\n---@class snacks.picker.layout.Config\n---@field layout snacks.layout.Box\n---@field reverse? boolean when true, the list will be reversed (bottom-up)\n---@field fullscreen? boolean open in fullscreen\n---@field cycle? boolean cycle through the list\n---@field preview? \"main\" show preview window in the picker or the main window\n---@field preset? string|fun(source:string):string\n---@field hidden? (\"input\"|\"preview\"|\"list\")[] don't show the given windows when opening the picker. (only \"input\" and \"preview\" make sense)\n---@field auto_hide? (\"input\"|\"preview\"|\"list\")[] hide the given windows when not focused (only \"input\" makes real sense)\n---@field config? fun(layout:snacks.picker.layout.Config) customize the resolved layout config\n```\n\n```lua\n---@class snacks.picker.win.Config\n---@field input? snacks.win.Config|{} input window config\n---@field list? snacks.win.Config|{} result list window config\n---@field preview? snacks.win.Config|{} preview window config\n```\n\n```lua\n---@alias snacks.Picker.ref (fun():snacks.Picker?)|{value?: snacks.Picker}\n```\n\n```lua\n---@alias snacks.picker.history.Record {pattern: string, search: string, live?: boolean}\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.picker\n---@field actions snacks.picker.actions\n---@field config snacks.picker.config\n---@field format snacks.picker.formatters\n---@field preview snacks.picker.previewers\n---@field sort snacks.picker.sorters\n---@field util snacks.picker.util\n---@field current? snacks.Picker\n---@field highlight snacks.picker.highlight\n---@field resume fun(opts?: snacks.picker.Config):snacks.Picker\n---@field sources snacks.picker.sources.Config\nSnacks.picker = {}\n```\n\n### `Snacks.picker()`\n\n```lua\n---@type fun(source: string, opts: snacks.picker.Config): snacks.Picker\nSnacks.picker()\n```\n\n```lua\n---@type fun(opts: snacks.picker.Config): snacks.Picker\nSnacks.picker()\n```\n\n### `Snacks.picker.get()`\n\nGet active pickers, optionally filtered by source,\nor the current tab\n\n```lua\n---@param opts? {source?: string, tab?: boolean} tab defaults to true\nSnacks.picker.get(opts)\n```\n\n### `Snacks.picker.pick()`\n\nCreate a new picker\n\n```lua\n---@param source? string\n---@param opts? snacks.picker.Config\n---@overload fun(opts: snacks.picker.Config): snacks.Picker\nSnacks.picker.pick(source, opts)\n```\n\n### `Snacks.picker.resume()`\n\n```lua\n---@param opts? snacks.picker.resume.Opts\n---@overload fun(source:string):snacks.Picker?\n---@return snacks.Picker?\nSnacks.picker.resume(opts)\n```\n\n### `Snacks.picker.select()`\n\nImplementation for `vim.ui.select`\n\n```lua\n---@type snacks.picker.ui_select\nSnacks.picker.select(...)\n```\n## 🔍 Sources\n\n### `autocmds`\n\n```vim\n:lua Snacks.picker.autocmds(opts?)\n```\n\n```lua\n{\n  finder = \"vim_autocmds\",\n  format = \"autocmd\",\n  preview = \"preview\",\n}\n```\n\n### `buffers`\n\n```vim\n:lua Snacks.picker.buffers(opts?)\n```\n\n```lua\n---@class snacks.picker.buffers.Config: snacks.picker.Config\n---@field hidden? boolean show hidden buffers (unlisted)\n---@field unloaded? boolean show loaded buffers\n---@field current? boolean show current buffer\n---@field nofile? boolean show `buftype=nofile` buffers\n---@field modified? boolean show only modified buffers\n---@field sort_lastused? boolean sort by last used\n---@field filter? snacks.picker.filter.Config\n{\n  finder = \"buffers\",\n  format = \"buffer\",\n  hidden = false,\n  unloaded = true,\n  current = true,\n  sort_lastused = true,\n  win = {\n    input = {\n      keys = {\n        [\"<c-x>\"] = { \"bufdelete\", mode = { \"n\", \"i\" } },\n      },\n    },\n    list = { keys = { [\"dd\"] = \"bufdelete\" } },\n  },\n}\n```\n\n### `cliphist`\n\n```vim\n:lua Snacks.picker.cliphist(opts?)\n```\n\n```lua\n{\n  finder = \"system_cliphist\",\n  format = \"text\",\n  preview = \"preview\",\n  confirm = { \"copy\", \"close\" },\n}\n```\n\n### `colorschemes`\n\n```vim\n:lua Snacks.picker.colorschemes(opts?)\n```\n\nNeovim colorschemes with live preview\n\n```lua\n{\n  finder = \"vim_colorschemes\",\n  format = \"text\",\n  preview = \"colorscheme\",\n  preset = \"vertical\",\n  confirm = function(picker, item)\n    picker:close()\n    if item then\n      picker.preview.state.colorscheme = nil\n      vim.schedule(function()\n        vim.cmd(\"colorscheme \" .. item.text)\n      end)\n    end\n  end,\n}\n```\n\n### `command_history`\n\n```vim\n:lua Snacks.picker.command_history(opts?)\n```\n\nNeovim command history\n\n```lua\n---@type snacks.picker.history.Config\n{\n  finder = \"vim_history\",\n  name = \"cmd\",\n  format = \"text\",\n  preview = \"none\",\n  main = { current = true },\n  layout = {\n    preset = \"vscode\",\n  },\n  confirm = \"cmd\",\n  formatters = { text = { ft = \"vim\" } },\n}\n```\n\n### `commands`\n\n```vim\n:lua Snacks.picker.commands(opts?)\n```\n\nNeovim commands\n\n```lua\n{\n  finder = \"vim_commands\",\n  format = \"command\",\n  preview = \"preview\",\n  confirm = \"cmd\",\n}\n```\n\n### `diagnostics`\n\n```vim\n:lua Snacks.picker.diagnostics(opts?)\n```\n\n```lua\n---@class snacks.picker.diagnostics.Config: snacks.picker.Config\n---@field filter? snacks.picker.filter.Config\n---@field severity? vim.diagnostic.SeverityFilter\n{\n  finder = \"diagnostics\",\n  format = \"diagnostic\",\n  sort = {\n    fields = {\n      \"is_current\",\n      \"is_cwd\",\n      \"severity\",\n      \"file\",\n      \"lnum\",\n    },\n  },\n  matcher = { sort_empty = true },\n  -- only show diagnostics from the cwd by default\n  filter = { cwd = true },\n}\n```\n\n### `diagnostics_buffer`\n\n```vim\n:lua Snacks.picker.diagnostics_buffer(opts?)\n```\n\n```lua\n---@type snacks.picker.diagnostics.Config\n{\n  finder = \"diagnostics\",\n  format = \"diagnostic\",\n  sort = {\n    fields = { \"severity\", \"file\", \"lnum\" },\n  },\n  matcher = { sort_empty = true },\n  filter = { buf = true },\n}\n```\n\n### `explorer`\n\n```vim\n:lua Snacks.picker.explorer(opts?)\n```\n\n```lua\n---@class snacks.picker.explorer.Config: snacks.picker.files.Config|{}\n---@field follow_file? boolean follow the file from the current buffer\n---@field tree? boolean show the file tree (default: true)\n---@field git_status? boolean show git status (default: true)\n---@field git_status_open? boolean show recursive git status for open directories\n---@field git_untracked? boolean needed to show untracked git status\n---@field diagnostics? boolean show diagnostics\n---@field diagnostics_open? boolean show recursive diagnostics for open directories\n---@field watch? boolean watch for file changes\n---@field exclude? string[] exclude glob patterns\n---@field include? string[] include glob patterns. These take precedence over `exclude`, `ignored` and `hidden`\n{\n  finder = \"explorer\",\n  sort = { fields = { \"sort\" } },\n  supports_live = true,\n  tree = true,\n  watch = true,\n  diagnostics = true,\n  diagnostics_open = false,\n  git_status = true,\n  git_status_open = false,\n  git_untracked = true,\n  follow_file = true,\n  focus = \"list\",\n  auto_close = false,\n  jump = { close = false },\n  layout = { preset = \"sidebar\", preview = false },\n  -- to show the explorer to the right, add the below to\n  -- your config under `opts.picker.sources.explorer`\n  -- layout = { layout = { position = \"right\" } },\n  formatters = {\n    file = { filename_only = true },\n    severity = { pos = \"right\" },\n  },\n  matcher = { sort_empty = false, fuzzy = false },\n  config = function(opts)\n    return require(\"snacks.picker.source.explorer\").setup(opts)\n  end,\n  win = {\n    list = {\n      keys = {\n        [\"<BS>\"] = \"explorer_up\",\n        [\"l\"] = \"confirm\",\n        [\"h\"] = \"explorer_close\", -- close directory\n        [\"a\"] = \"explorer_add\",\n        [\"d\"] = \"explorer_del\",\n        [\"r\"] = \"explorer_rename\",\n        [\"c\"] = \"explorer_copy\",\n        [\"m\"] = \"explorer_move\",\n        [\"o\"] = \"explorer_open\", -- open with system application\n        [\"P\"] = \"toggle_preview\",\n        [\"y\"] = { \"explorer_yank\", mode = { \"n\", \"x\" } },\n        [\"p\"] = \"explorer_paste\",\n        [\"u\"] = \"explorer_update\",\n        [\"<c-c>\"] = \"tcd\",\n        [\"<leader>/\"] = \"picker_grep\",\n        [\"<c-t>\"] = \"terminal\",\n        [\".\"] = \"explorer_focus\",\n        [\"I\"] = \"toggle_ignored\",\n        [\"H\"] = \"toggle_hidden\",\n        [\"Z\"] = \"explorer_close_all\",\n        [\"]g\"] = \"explorer_git_next\",\n        [\"[g\"] = \"explorer_git_prev\",\n        [\"]d\"] = \"explorer_diagnostic_next\",\n        [\"[d\"] = \"explorer_diagnostic_prev\",\n        [\"]w\"] = \"explorer_warn_next\",\n        [\"[w\"] = \"explorer_warn_prev\",\n        [\"]e\"] = \"explorer_error_next\",\n        [\"[e\"] = \"explorer_error_prev\",\n      },\n    },\n  },\n}\n```\n\n### `files`\n\n```vim\n:lua Snacks.picker.files(opts?)\n```\n\n```lua\n---@class snacks.picker.files.Config: snacks.picker.proc.Config\n---@field cmd? \"fd\"| \"rg\"| \"find\" command to use. Leave empty to auto-detect\n---@field hidden? boolean show hidden files\n---@field ignored? boolean show ignored files\n---@field dirs? string[] directories to search\n---@field follow? boolean follow symlinks\n---@field exclude? string[] exclude patterns\n---@field args? string[] additional arguments\n---@field ft? string|string[] file extension(s)\n---@field rtp? boolean search in runtimepath\n{\n  finder = \"files\",\n  format = \"file\",\n  show_empty = true,\n  hidden = false,\n  ignored = false,\n  follow = false,\n  supports_live = true,\n}\n```\n\n### `gh_actions`\n\n```vim\n:lua Snacks.picker.gh_actions(opts?)\n```\n\n```lua\n---@class snacks.picker.gh.actions.Config: snacks.picker.Config\n---@field number number issue or PR number\n---@field repo string GitHub repository (owner/repo). Defaults to current git repo\n---@field type \"issue\" | \"pr\"\n---@field item? snacks.picker.gh.Item\n{\n  layout = { preset = \"select\", layout = { max_width = 50 } },\n  title = \"  Actions\",\n  main = { current = true },\n  finder = \"gh_get_actions\",\n  format = \"gh_format_action\",\n  confirm = \"gh_perform_action\",\n}\n```\n\n### `gh_diff`\n\n```vim\n:lua Snacks.picker.gh_diff(opts?)\n```\n\n```lua\n---@class snacks.picker.gh.diff.Config: snacks.picker.Config\n---@field group? boolean group changes by file (when false, show individual hunks)\n---@field pr number number PR number to diff against\n---@field repo? string GitHub repository (owner/repo). Defaults to current git repo\n{\n  title = \"  Pull Request Diff\",\n  group = true,\n  finder = \"gh_diff\",\n  format = \"git_status\",\n  preview = \"gh_preview_diff\",\n  win = {\n    preview = {\n      keys = {\n        [\"a\"] = { \"gh_comment\", mode = { \"n\", \"x\" } },\n        [\"<cr>\"] = { \"gh_actions\", mode = { \"n\", \"x\" } },\n      },\n    },\n  },\n}\n```\n\n### `gh_issue`\n\n```vim\n:lua Snacks.picker.gh_issue(opts?)\n```\n\n```lua\n---@class snacks.picker.gh.issue.Config: snacks.picker.gh.Config\n---@field state \"open\" | \"closed\" | \"all\"\n---@field mention? string filter by mention\n---@field milestone? string filter by milestone\n{\n  title = \"  Issues\",\n  finder = \"gh_issue\",\n  format = \"gh_format\",\n  preview = \"gh_preview\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n  supports_live = true,\n  live = true,\n  confirm = \"gh_actions\",\n  win = {\n    input = {\n      keys = {\n        [\"<a-b>\"] = { \"gh_browse\", mode = { \"n\", \"i\" } },\n        [\"<c-y>\"] = { \"gh_yank\", mode = { \"n\", \"i\" } },\n      },\n    },\n    list = {\n      keys = {\n        [\"y\"] = { \"gh_yank\", mode = { \"n\", \"x\" } },\n      },\n    },\n  },\n}\n```\n\n### `gh_labels`\n\n```vim\n:lua Snacks.picker.gh_labels(opts?)\n```\n\n```lua\n---@class snacks.picker.gh.labels.Config: snacks.picker.Config\n---@field number number issue or PR number\n---@field repo string GitHub repository (owner/repo). Defaults to current git repo\n{\n  layout = { preset = \"select\", layout = { max_width = 50 } },\n  title = \"  Labels\",\n  main = { current = true },\n  group = true,\n  finder = \"gh_labels\",\n  format = \"gh_format_label\",\n}\n```\n\n### `gh_pr`\n\n```vim\n:lua Snacks.picker.gh_pr(opts?)\n```\n\n```lua\n---@class snacks.picker.gh.pr.Config: snacks.picker.gh.Config\n---@field state \"open\" | \"closed\" | \"merged\" | \"all\"\n---@field draft? boolean filter draft PRs\n---@field base? string filter by base branch\n{\n  title = \"  Pull Requests\",\n  finder = \"gh_pr\",\n  format = \"gh_format\",\n  preview = \"gh_preview\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n  supports_live = true,\n  live = true,\n  confirm = \"gh_actions\",\n  win = {\n    input = {\n      keys = {\n        [\"<a-b>\"] = { \"gh_browse\", mode = { \"n\", \"i\" } },\n        [\"<c-y>\"] = { \"gh_yank\", mode = { \"n\", \"i\" } },\n      },\n    },\n    list = {\n      keys = {\n        [\"y\"] = { \"gh_yank\", mode = { \"n\", \"x\" } },\n      },\n    },\n  },\n}\n```\n\n### `gh_reactions`\n\n```vim\n:lua Snacks.picker.gh_reactions(opts?)\n```\n\n```lua\n---@class snacks.picker.gh.reactions.Config: snacks.picker.Config\n---@field number number issue or PR number\n---@field repo string GitHub repository (owner/repo). Defaults to current git repo\n{\n  layout = { preset = \"select\", layout = { max_width = 50 } },\n  title = \"  Reactions\",\n  main = { current = true },\n  group = true,\n  finder = \"gh_reactions\",\n  format = \"gh_format_reaction\",\n}\n```\n\n### `git_branches`\n\n```vim\n:lua Snacks.picker.git_branches(opts?)\n```\n\n```lua\n---@class snacks.picker.git.branches.Config: snacks.picker.git.Config\n---@field all? boolean show all branches, including remote\n{\n  all = false,\n  finder = \"git_branches\",\n  format = \"git_branch\",\n  preview = \"git_log\",\n  confirm = \"git_checkout\",\n  win = {\n    input = {\n      keys = {\n        [\"<c-a>\"] = { \"git_branch_add\", mode = { \"n\", \"i\" } },\n        [\"<c-x>\"] = { \"git_branch_del\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n  ---@param picker snacks.Picker\n  on_show = function(picker)\n    for i, item in ipairs(picker:items()) do\n      if item.current then\n        picker.list:view(i)\n        Snacks.picker.actions.list_scroll_center(picker)\n        break\n      end\n    end\n  end,\n}\n```\n\n### `git_diff`\n\n```vim\n:lua Snacks.picker.git_diff(opts?)\n```\n\n```lua\n---@class snacks.picker.git.diff.Config: snacks.picker.git.Config\n---@field group? boolean group changes by file (when false, show individual hunks)\n---@field staged? boolean show staged changes\n---@field base? string base commit/branch/tag to diff against (default: HEAD)\n{\n  group = false,\n  finder = \"git_diff\",\n  format = \"git_status\",\n  preview = \"diff\",\n  matcher = { sort_empty = true },\n  sort = { fields = { \"score:desc\", \"file\", \"idx\" } },\n  win = {\n    input = {\n      keys = {\n        [\"<Tab>\"] = { \"git_stage\", mode = { \"n\", \"i\" } },\n        [\"<c-r>\"] = { \"git_restore\", mode = { \"n\", \"i\" }, nowait = true },\n      },\n    },\n  },\n}\n```\n\n### `git_files`\n\n```vim\n:lua Snacks.picker.git_files(opts?)\n```\n\nFind git files\n\n```lua\n---@class snacks.picker.git.files.Config: snacks.picker.git.Config\n---@field untracked? boolean show untracked files\n---@field submodules? boolean show submodule files\n{\n  finder = \"git_files\",\n  show_empty = true,\n  format = \"file\",\n  untracked = false,\n  submodules = false,\n}\n```\n\n### `git_grep`\n\n```vim\n:lua Snacks.picker.git_grep(opts?)\n```\n\nGrep in git files\n\n```lua\n---@class snacks.picker.git.grep.Config: snacks.picker.git.Config\n---@field untracked? boolean search in untracked files\n---@field submodules? boolean search in submodule files\n---@field need_search? boolean require a search pattern\n---@field pathspec? string|string[] pathspec pattern(s)\n---@field ignorecase? boolean ignore case\n{\n  finder = \"git_grep\",\n  format = \"file\",\n  untracked = false,\n  need_search = true,\n  submodules = false,\n  show_empty = true,\n  supports_live = true,\n  live = true,\n}\n```\n\n### `git_log`\n\n```vim\n:lua Snacks.picker.git_log(opts?)\n```\n\nGit log\n\n```lua\n---@class snacks.picker.git.log.Config: snacks.picker.git.Config\n---@field follow? boolean track file history across renames\n---@field current_file? boolean show current file log\n---@field current_line? boolean show current line log\n---@field author? string filter commits by author\n{\n  finder = \"git_log\",\n  format = \"git_log\",\n  preview = \"git_show\",\n  confirm = \"git_checkout\",\n  supports_live = true,\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n```\n\n### `git_log_file`\n\n```vim\n:lua Snacks.picker.git_log_file(opts?)\n```\n\n```lua\n---@type snacks.picker.git.log.Config\n{\n  finder = \"git_log\",\n  format = \"git_log\",\n  preview = \"git_show\",\n  current_file = true,\n  follow = true,\n  confirm = \"git_checkout\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n```\n\n### `git_log_line`\n\n```vim\n:lua Snacks.picker.git_log_line(opts?)\n```\n\n```lua\n---@type snacks.picker.git.log.Config\n{\n  finder = \"git_log\",\n  format = \"git_log\",\n  preview = \"git_show\",\n  current_line = true,\n  follow = true,\n  confirm = \"git_checkout\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n```\n\n### `git_stash`\n\n```vim\n:lua Snacks.picker.git_stash(opts?)\n```\n\n```lua\n{\n  finder = \"git_stash\",\n  format = \"git_stash\",\n  preview = \"git_stash\",\n  confirm = \"git_stash_apply\",\n}\n```\n\n### `git_status`\n\n```vim\n:lua Snacks.picker.git_status(opts?)\n```\n\n```lua\n---@class snacks.picker.git.status.Config: snacks.picker.git.Config\n---@field ignored? boolean show ignored files\n{\n  finder = \"git_status\",\n  format = \"git_status\",\n  preview = \"git_status\",\n  win = {\n    input = {\n      keys = {\n        [\"<Tab>\"] = { \"git_stage\", mode = { \"n\", \"i\" } },\n        [\"<c-r>\"] = { \"git_restore\", mode = { \"n\", \"i\" }, nowait = true },\n      },\n    },\n  },\n}\n```\n\n### `grep`\n\n```vim\n:lua Snacks.picker.grep(opts?)\n```\n\n```lua\n---@class snacks.picker.grep.Config: snacks.picker.proc.Config\n---@field cmd? string\n---@field hidden? boolean show hidden files\n---@field ignored? boolean show ignored files\n---@field dirs? string[] directories to search\n---@field follow? boolean follow symlinks\n---@field glob? string|string[] glob file pattern(s)\n---@field ft? string|string[] ripgrep file type(s). See `rg --type-list`\n---@field regex? boolean use regex search pattern (defaults to `true`)\n---@field buffers? boolean search in open buffers\n---@field need_search? boolean require a search pattern\n---@field exclude? string[] exclude patterns\n---@field args? string[] additional arguments\n---@field rtp? boolean search in runtimepath\n{\n  finder = \"grep\",\n  regex = true,\n  format = \"file\",\n  show_empty = true,\n  live = true, -- live grep by default\n  supports_live = true,\n}\n```\n\n### `grep_buffers`\n\n```vim\n:lua Snacks.picker.grep_buffers(opts?)\n```\n\n```lua\n---@type snacks.picker.grep.Config|{}\n{\n  finder = \"grep\",\n  format = \"file\",\n  live = true,\n  buffers = true,\n  need_search = false,\n  supports_live = true,\n}\n```\n\n### `grep_word`\n\n```vim\n:lua Snacks.picker.grep_word(opts?)\n```\n\n```lua\n---@type snacks.picker.grep.Config|{}\n{\n  finder = \"grep\",\n  regex = false,\n  args = { \"--word-regexp\" },\n  format = \"file\",\n  search = function(picker)\n    return picker:word()\n  end,\n  live = false,\n  supports_live = true,\n}\n```\n\n### `help`\n\n```vim\n:lua Snacks.picker.help(opts?)\n```\n\nNeovim help tags\n\n```lua\n---@class snacks.picker.help.Config: snacks.picker.Config\n---@field lang? string[] defaults to `vim.opt.helplang`\n{\n  finder = \"help\",\n  format = \"text\",\n  previewers = {\n    file = { ft = \"help\" },\n  },\n  win = { preview = { minimal = true } },\n  confirm = \"help\",\n}\n```\n\n### `highlights`\n\n```vim\n:lua Snacks.picker.highlights(opts?)\n```\n\n```lua\n{\n  finder = \"vim_highlights\",\n  format = \"hl\",\n  preview = \"preview\",\n  confirm = \"close\",\n}\n```\n\n### `icons`\n\n```vim\n:lua Snacks.picker.icons(opts?)\n```\n\n```lua\n---@class snacks.picker.icons.Config: snacks.picker.Config\n---@field icon_sources? string[] list of sources to use\n--- Custom icon sources can be added here. The key is the source name,\n--- and the value is the file path or URL to load icons from.\n--- The file should be a JSON array of:\n--- `{[1]:string, [2]:string}|{icon:string, name:string, category:string}`\n--- The format is compatible with https://github.com/nvim-telescope/telescope-symbols.nvim\n---@field custom_sources? table<string,string> additional icon sources `table<source,file|url>`\n{\n  main = { current = true },\n  finder = \"icons\",\n  format = \"icon\",\n  layout = { preset = \"vscode\" },\n  confirm = \"put\",\n}\n```\n\n### `jumps`\n\n```vim\n:lua Snacks.picker.jumps(opts?)\n```\n\n```lua\n{\n  finder = \"vim_jumps\",\n  format = \"file\",\n  main = { current = true },\n}\n```\n\n### `keymaps`\n\n```vim\n:lua Snacks.picker.keymaps(opts?)\n```\n\n```lua\n---@class snacks.picker.keymaps.Config: snacks.picker.Config\n---@field global? boolean show global keymaps\n---@field local? boolean show buffer keymaps\n---@field plugs? boolean show plugin keymaps\n---@field modes? string[]\n{\n  finder = \"vim_keymaps\",\n  format = \"keymap\",\n  preview = \"preview\",\n  global = true,\n  plugs = false,\n  [\"local\"] = true,\n  modes = { \"n\", \"v\", \"x\", \"s\", \"o\", \"i\", \"c\", \"t\" },\n  ---@param picker snacks.Picker\n  confirm = function(picker, item)\n    picker:norm(function()\n      if item then\n        picker:close()\n        vim.api.nvim_input(item.item.lhs)\n      end\n    end)\n  end,\n  actions = {\n    toggle_global = function(picker)\n      picker.opts.global = not picker.opts.global\n      picker:find()\n    end,\n    toggle_buffer = function(picker)\n      picker.opts[\"local\"] = not picker.opts[\"local\"]\n      picker:find()\n    end,\n  },\n  win = {\n    input = {\n      keys = {\n        [\"<a-g>\"] = { \"toggle_global\", mode = { \"n\", \"i\" }, desc = \"Toggle Global Keymaps\" },\n        [\"<a-b>\"] = { \"toggle_buffer\", mode = { \"n\", \"i\" }, desc = \"Toggle Buffer Keymaps\" },\n      },\n    },\n  },\n}\n```\n\n### `lazy`\n\n```vim\n:lua Snacks.picker.lazy(opts?)\n```\n\nSearch for a lazy.nvim plugin spec\n\n```lua\n{\n  finder = \"lazy_spec\",\n  pattern = \"'\",\n}\n```\n\n### `lines`\n\n```vim\n:lua Snacks.picker.lines(opts?)\n```\n\nSearch lines in the current buffer\n\n```lua\n---@class snacks.picker.lines.Config: snacks.picker.Config\n---@field buf? number\n{\n  finder = \"lines\",\n  format = \"lines\",\n  layout = {\n    preview = \"main\",\n    preset = \"ivy\",\n  },\n  jump = { match = true },\n  -- allow any window to be used as the main window\n  main = { current = true },\n  ---@param picker snacks.Picker\n  on_show = function(picker)\n    local cursor = vim.api.nvim_win_get_cursor(picker.main)\n    local info = vim.api.nvim_win_call(picker.main, vim.fn.winsaveview)\n    picker.list:view(cursor[1], info.topline)\n    picker:show_preview()\n  end,\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n```\n\n### `loclist`\n\n```vim\n:lua Snacks.picker.loclist(opts?)\n```\n\nLoclist\n\n```lua\n---@type snacks.picker.qf.Config\n{\n  finder = \"qf\",\n  format = \"file\",\n  qf_win = 0,\n  main = { current = true },\n}\n```\n\n### `lsp_config`\n\n```vim\n:lua Snacks.picker.lsp_config(opts?)\n```\n\n```lua\n---@class snacks.picker.lsp.config.Config: snacks.picker.Config\n---@field installed? boolean only show installed servers\n---@field configured? boolean only show configured servers (setup with lspconfig)\n---@field attached? boolean|number only show attached servers. When `number`, show only servers attached to that buffer (can be 0)\n{\n  finder = \"lsp.config#find\",\n  format = \"lsp.config#format\",\n  preview = \"lsp.config#preview\",\n  confirm = \"close\",\n  sort = { fields = { \"score:desc\", \"attached_buf\", \"attached\", \"enabled\", \"installed\", \"name\" } },\n  matcher = { sort_empty = true },\n}\n```\n\n### `lsp_declarations`\n\n```vim\n:lua Snacks.picker.lsp_declarations(opts?)\n```\n\nLSP declarations\n\n```lua\n---@type snacks.picker.lsp.Config\n{\n  finder = \"lsp_declarations\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n```\n\n### `lsp_definitions`\n\n```vim\n:lua Snacks.picker.lsp_definitions(opts?)\n```\n\nLSP definitions\n\n```lua\n---@type snacks.picker.lsp.Config\n{\n  finder = \"lsp_definitions\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n```\n\n### `lsp_implementations`\n\n```vim\n:lua Snacks.picker.lsp_implementations(opts?)\n```\n\nLSP implementations\n\n```lua\n---@type snacks.picker.lsp.Config\n{\n  finder = \"lsp_implementations\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n```\n\n### `lsp_incoming_calls`\n\n```vim\n:lua Snacks.picker.lsp_incoming_calls(opts?)\n```\n\nLSP incoming calls\n\n```lua\n---@type snacks.picker.lsp.Config\n{\n  finder = \"lsp_incoming_calls\",\n  format = \"lsp_symbol\",\n  include_current = false,\n  workspace = true, -- this ensures the file is included in the formatter\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n```\n\n### `lsp_outgoing_calls`\n\n```vim\n:lua Snacks.picker.lsp_outgoing_calls(opts?)\n```\n\nLSP outgoing calls\n\n```lua\n---@type snacks.picker.lsp.Config\n{\n  finder = \"lsp_outgoing_calls\",\n  format = \"lsp_symbol\",\n  include_current = false,\n  workspace = true, -- this ensures the file is included in the formatter\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n```\n\n### `lsp_references`\n\n```vim\n:lua Snacks.picker.lsp_references(opts?)\n```\n\nLSP references\n\n```lua\n---@class snacks.picker.lsp.references.Config: snacks.picker.lsp.Config\n---@field include_declaration? boolean default true\n{\n  finder = \"lsp_references\",\n  format = \"file\",\n  include_declaration = true,\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n```\n\n### `lsp_symbols`\n\n```vim\n:lua Snacks.picker.lsp_symbols(opts?)\n```\n\nLSP document symbols\n\n```lua\n---@class snacks.picker.lsp.symbols.Config: snacks.picker.Config\n---@field tree? boolean show symbol tree\n---@field keep_parents? boolean keep parent symbols when filtering\n---@field filter table<string, string[]|boolean>? symbol kind filter\n---@field workspace? boolean show workspace symbols\n{\n  finder = \"lsp_symbols\",\n  format = \"lsp_symbol\",\n  tree = true,\n  filter = {\n    default = {\n      \"Class\",\n      \"Constructor\",\n      \"Enum\",\n      \"Field\",\n      \"Function\",\n      \"Interface\",\n      \"Method\",\n      \"Module\",\n      \"Namespace\",\n      \"Package\",\n      \"Property\",\n      \"Struct\",\n      \"Trait\",\n    },\n    -- set to `true` to include all symbols\n    markdown = true,\n    help = true,\n    -- you can specify a different filter for each filetype\n    lua = {\n      \"Class\",\n      \"Constructor\",\n      \"Enum\",\n      \"Field\",\n      \"Function\",\n      \"Interface\",\n      \"Method\",\n      \"Module\",\n      \"Namespace\",\n      -- \"Package\", -- remove package since luals uses it for control flow structures\n      \"Property\",\n      \"Struct\",\n      \"Trait\",\n    },\n  },\n}\n```\n\n### `lsp_type_definitions`\n\n```vim\n:lua Snacks.picker.lsp_type_definitions(opts?)\n```\n\nLSP type definitions\n\n```lua\n---@type snacks.picker.lsp.Config\n{\n  finder = \"lsp_type_definitions\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n```\n\n### `lsp_workspace_symbols`\n\n```vim\n:lua Snacks.picker.lsp_workspace_symbols(opts?)\n```\n\n```lua\n---@type snacks.picker.lsp.symbols.Config\nvim.tbl_extend(\"force\", {}, M.lsp_symbols, {\n  workspace = true,\n  tree = false,\n  supports_live = true,\n  live = true, -- live by default\n})\n```\n\n### `man`\n\n```vim\n:lua Snacks.picker.man(opts?)\n```\n\n```lua\n{\n  finder = \"system_man\",\n  format = \"man\",\n  preview = \"man\",\n  confirm = function(picker, item, action)\n    ---@cast action snacks.picker.jump.Action\n    picker:close()\n    if item then\n      vim.schedule(function()\n        local cmd = \"Man \" .. item.ref ---@type string\n        if action.cmd == \"vsplit\" then\n          cmd = \"vert \" .. cmd\n        elseif action.cmd == \"tab\" then\n          cmd = \"tab \" .. cmd\n        end\n        vim.cmd(cmd)\n      end)\n    end\n  end,\n}\n```\n\n### `marks`\n\n```vim\n:lua Snacks.picker.marks(opts?)\n```\n\n```lua\n---@class snacks.picker.marks.Config: snacks.picker.Config\n---@field global? boolean show global marks\n---@field local? boolean show buffer marks\n{\n  finder = \"vim_marks\",\n  format = \"file\",\n  global = true,\n  [\"local\"] = true,\n  win = {\n    input = {\n      keys = {\n        [\"<c-x>\"] = { \"mark_delete\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n}\n```\n\n### `notifications`\n\n```vim\n:lua Snacks.picker.notifications(opts?)\n```\n\n```lua\n---@class snacks.picker.notifications.Config: snacks.picker.Config\n---@field filter? snacks.notifier.level|fun(notif: snacks.notifier.Notif): boolean\n{\n  finder = \"snacks_notifier\",\n  format = \"notification\",\n  preview = \"preview\",\n  formatters = { severity = { level = true } },\n  confirm = \"close\",\n}\n```\n\n### `picker_actions`\n\n```vim\n:lua Snacks.picker.picker_actions(opts?)\n```\n\n```lua\n{\n  finder = \"meta_actions\",\n  format = \"text\",\n}\n```\n\n### `picker_format`\n\n```vim\n:lua Snacks.picker.picker_format(opts?)\n```\n\n```lua\n{\n  finder = \"meta_format\",\n  format = \"text\",\n}\n```\n\n### `picker_layouts`\n\n```vim\n:lua Snacks.picker.picker_layouts(opts?)\n```\n\n```lua\n{\n  finder = \"meta_layouts\",\n  format = \"text\",\n  on_change = function(picker, item)\n    vim.schedule(function()\n      picker:set_layout(item.text)\n    end)\n  end,\n}\n```\n\n### `picker_preview`\n\n```vim\n:lua Snacks.picker.picker_preview(opts?)\n```\n\n```lua\n{\n  finder = \"meta_preview\",\n  format = \"text\",\n}\n```\n\n### `pickers`\n\n```vim\n:lua Snacks.picker.pickers(opts?)\n```\n\nList all available sources\n\n```lua\n{\n  finder = \"meta_pickers\",\n  format = \"text\",\n  confirm = function(picker, item)\n    picker:close()\n    if item then\n      vim.schedule(function()\n        Snacks.picker(item.text)\n      end)\n    end\n  end,\n}\n```\n\n### `projects`\n\n```vim\n:lua Snacks.picker.projects(opts?)\n```\n\nOpen recent projects\n\n```lua\n---@class snacks.picker.projects.Config: snacks.picker.Config\n---@field filter? snacks.picker.filter.Config\n---@field dev? string|string[] top-level directories containing multiple projects (sub-folders that contains a root pattern)\n---@field projects? string[] list of project directories\n---@field patterns? string[] patterns to detect project root directories\n---@field recent? boolean include project directories of recent files\n---@field max_depth? number maximum depth to search in dev directories (default: 2)\n{\n  finder = \"recent_projects\",\n  format = \"file\",\n  dev = { \"~/dev\", \"~/projects\" },\n  confirm = \"load_session\",\n  patterns = { \".git\", \"_darcs\", \".hg\", \".bzr\", \".svn\", \"package.json\", \"Makefile\" },\n  recent = true,\n  matcher = {\n    frecency = true, -- use frecency boosting\n    sort_empty = true, -- sort even when the filter is empty\n    cwd_bonus = false,\n  },\n  sort = { fields = { \"score:desc\", \"idx\" } },\n  win = {\n    preview = { minimal = true },\n    input = {\n      keys = {\n        -- every action will always first change the cwd of the current tabpage to the project\n        [\"<c-e>\"] = { { \"tcd\", \"picker_explorer\" }, mode = { \"n\", \"i\" } },\n        [\"<c-f>\"] = { { \"tcd\", \"picker_files\" }, mode = { \"n\", \"i\" } },\n        [\"<c-g>\"] = { { \"tcd\", \"picker_grep\" }, mode = { \"n\", \"i\" } },\n        [\"<c-r>\"] = { { \"tcd\", \"picker_recent\" }, mode = { \"n\", \"i\" }, nowait = true },\n        [\"<c-w>\"] = { { \"tcd\" }, mode = { \"n\", \"i\" } },\n        [\"<c-t>\"] = {\n          function(picker)\n            vim.cmd(\"tabnew\")\n            Snacks.notify(\"New tab opened\")\n            picker:close()\n            Snacks.picker.projects()\n          end,\n          mode = { \"n\", \"i\" },\n        },\n      },\n    },\n  },\n}\n```\n\n### `qflist`\n\n```vim\n:lua Snacks.picker.qflist(opts?)\n```\n\nQuickfix list\n\n```lua\n---@type snacks.picker.qf.Config\n{\n  finder = \"qf\",\n  format = \"file\",\n}\n```\n\n### `recent`\n\n```vim\n:lua Snacks.picker.recent(opts?)\n```\n\nFind recent files\n\n```lua\n---@class snacks.picker.recent.Config: snacks.picker.Config\n---@field filter? snacks.picker.filter.Config\n{\n  finder = \"recent_files\",\n  format = \"file\",\n  filter = {\n    paths = {\n      [vim.fn.stdpath(\"data\")] = false,\n      [vim.fn.stdpath(\"cache\")] = false,\n      [vim.fn.stdpath(\"state\")] = false,\n    },\n  },\n}\n```\n\n### `registers`\n\n```vim\n:lua Snacks.picker.registers(opts?)\n```\n\nNeovim registers\n\n```lua\n{\n  finder = \"vim_registers\",\n  main = { current = true },\n  format = \"register\",\n  preview = \"preview\",\n  confirm = { \"copy\", \"close\" },\n}\n```\n\n### `resume`\n\n```vim\n:lua Snacks.picker.resume(opts?)\n```\n\nSpecial picker that resumes the last picker\n\n```lua\n{}\n```\n\n### `scratch`\n\n```vim\n:lua Snacks.picker.scratch(opts?)\n```\n\nOpen or create scratch buffers\n\n```lua\n{\n  finder = \"scratch\",\n  format = \"scratch_format\",\n  confirm = \"scratch_open\",\n  win = {\n    input = {\n      keys = {\n        [\"<c-x>\"] = { \"scratch_delete\", mode = { \"n\", \"i\" } },\n        [\"<c-n>\"] = { \"scratch_new\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n}\n```\n\n### `search_history`\n\n```vim\n:lua Snacks.picker.search_history(opts?)\n```\n\nNeovim search history\n\n```lua\n---@type snacks.picker.history.Config\n{\n  finder = \"vim_history\",\n  name = \"search\",\n  format = \"text\",\n  preview = \"none\",\n  main = { current = true },\n  layout = { preset = \"vscode\" },\n  confirm = \"search\",\n  formatters = { text = { ft = \"regex\" } },\n}\n```\n\n### `select`\n\n```vim\n:lua Snacks.picker.select(opts?)\n```\n\nConfig used by `vim.ui.select`.\nNot meant to be used directly.\n\n```lua\n---@class snacks.picker.select.Config: snacks.picker.Config\n---@field kinds? table<string, snacks.picker.Config|{}> custom snacks picker configs for specific `vim.ui.select` kinds\n{\n  items = {}, -- these are set dynamically\n  main = { current = true },\n  layout = { preset = \"select\" },\n}\n```\n\n### `smart`\n\n```vim\n:lua Snacks.picker.smart(opts?)\n```\n\n```lua\n---@class snacks.picker.smart.Config: snacks.picker.Config\n---@field finders? string[] list of finders to use\n---@field filter? snacks.picker.filter.Config\n{\n  multi = { \"buffers\", \"recent\", \"files\" },\n  format = \"file\", -- use `file` format for all sources\n  matcher = {\n    cwd_bonus = true, -- boost cwd matches\n    frecency = true, -- use frecency boosting\n    sort_empty = true, -- sort even when the filter is empty\n  },\n  transform = \"unique_file\",\n}\n```\n\n### `spelling`\n\n```vim\n:lua Snacks.picker.spelling(opts?)\n```\n\n```lua\n{\n  finder = \"vim_spelling\",\n  format = \"text\",\n  main = { current = true },\n  layout = { preset = \"vscode\" },\n  confirm = \"item_action\",\n}\n```\n\n### `tags`\n\n```vim\n:lua Snacks.picker.tags(opts?)\n```\n\nSearch tags file\n\n```lua\n---@class snacks.picker.tags.Config: snacks.picker.Config\n{\n  workspace = true, -- search tags in the workspace\n  finder = \"vim_tags\",\n  format = \"lsp_symbol\",\n}\n```\n\n### `treesitter`\n\n```vim\n:lua Snacks.picker.treesitter(opts?)\n```\n\n```lua\n---@class snacks.picker.treesitter.Config: snacks.picker.Config\n---@field filter table<string, string[]|boolean>? symbol kind filter\n---@field tree? boolean show symbol tree\n{\n  finder = \"treesitter_symbols\",\n  format = \"lsp_symbol\",\n  tree = true,\n  filter = {\n    default = {\n      \"Class\",\n      \"Enum\",\n      \"Field\",\n      \"Function\",\n      \"Method\",\n      \"Module\",\n      \"Namespace\",\n      \"Struct\",\n      \"Trait\",\n    },\n    -- set to `true` to include all symbols\n    markdown = true,\n    help = true,\n  },\n}\n```\n\n### `undo`\n\n```vim\n:lua Snacks.picker.undo(opts?)\n```\n\n```lua\n---@class snacks.picker.undo.Config: snacks.picker.Config\n---@field diff? vim.text.diff.Opts\n{\n  finder = \"vim_undo\",\n  format = \"undo\",\n  preview = \"diff\",\n  confirm = \"item_action\",\n  win = {\n    preview = { wo = { number = false, relativenumber = false, signcolumn = \"no\" } },\n    input = {\n      keys = {\n        [\"<c-y>\"] = { \"yank_add\", mode = { \"n\", \"i\" } },\n        [\"<c-s-y>\"] = { \"yank_del\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n  actions = {\n    yank_add = { action = \"yank\", field = \"added_lines\" },\n    yank_del = { action = \"yank\", field = \"removed_lines\" },\n  },\n  icons = { tree = { last = \"┌╴\" } }, -- the tree is upside down\n  diff = {\n    ctxlen = 4,\n    ignore_cr_at_eol = true,\n    ignore_whitespace_change_at_eol = true,\n    indent_heuristic = true,\n  },\n}\n```\n\n### `zoxide`\n\n```vim\n:lua Snacks.picker.zoxide(opts?)\n```\n\nOpen a project from zoxide\n\n```lua\n{\n  finder = \"files_zoxide\",\n  format = \"file\",\n  confirm = \"load_session\",\n  win = {\n    preview = {\n      minimal = true,\n    },\n  },\n}\n```\n\n## 🖼️ Layouts\n\n### `bottom`\n\n```lua\n{ preset = \"ivy\", layout = { position = \"bottom\" } }\n```\n\n### `default`\n\n```lua\n{\n  layout = {\n    box = \"horizontal\",\n    width = 0.8,\n    min_width = 120,\n    height = 0.8,\n    {\n      box = \"vertical\",\n      border = true,\n      title = \"{title} {live} {flags}\",\n      { win = \"input\", height = 1, border = \"bottom\" },\n      { win = \"list\", border = \"none\" },\n    },\n    { win = \"preview\", title = \"{preview}\", border = true, width = 0.5 },\n  },\n}\n```\n\n### `dropdown`\n\n```lua\n{\n  layout = {\n    backdrop = false,\n    row = 1,\n    width = 0.4,\n    min_width = 80,\n    height = 0.8,\n    border = \"none\",\n    box = \"vertical\",\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = true },\n    {\n      box = \"vertical\",\n      border = true,\n      title = \"{title} {live} {flags}\",\n      title_pos = \"center\",\n      { win = \"input\", height = 1, border = \"bottom\" },\n      { win = \"list\", border = \"none\" },\n    },\n  },\n}\n```\n\n### `ivy`\n\n```lua\n{\n  layout = {\n    box = \"vertical\",\n    backdrop = false,\n    row = -1,\n    width = 0,\n    height = 0.4,\n    border = \"top\",\n    title = \" {title} {live} {flags}\",\n    title_pos = \"left\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    {\n      box = \"horizontal\",\n      { win = \"list\", border = \"none\" },\n      { win = \"preview\", title = \"{preview}\", width = 0.6, border = \"left\" },\n    },\n  },\n}\n```\n\n### `ivy_split`\n\n```lua\n{\n  preview = \"main\",\n  layout = {\n    box = \"vertical\",\n    backdrop = false,\n    width = 0,\n    height = 0.4,\n    position = \"bottom\",\n    border = \"top\",\n    title = \" {title} {live} {flags}\",\n    title_pos = \"left\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    {\n      box = \"horizontal\",\n      { win = \"list\", border = \"none\" },\n      { win = \"preview\", title = \"{preview}\", width = 0.6, border = \"left\" },\n    },\n  },\n}\n```\n\n### `left`\n\n```lua\nM.sidebar\n```\n\n### `right`\n\n```lua\n{ preset = \"sidebar\", layout = { position = \"right\" } }\n```\n\n### `select`\n\n```lua\n{\n  hidden = { \"preview\" },\n  layout = {\n    backdrop = false,\n    width = 0.5,\n    min_width = 80,\n    max_width = 100,\n    height = 0.4,\n    min_height = 2,\n    box = \"vertical\",\n    border = true,\n    title = \"{title}\",\n    title_pos = \"center\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    { win = \"list\", border = \"none\" },\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n  },\n}\n```\n\n### `sidebar`\n\n```lua\n{\n  preview = \"main\",\n  layout = {\n    backdrop = false,\n    width = 40,\n    min_width = 40,\n    height = 0,\n    position = \"left\",\n    border = \"none\",\n    box = \"vertical\",\n    {\n      win = \"input\",\n      height = 1,\n      border = true,\n      title = \"{title} {live} {flags}\",\n      title_pos = \"center\",\n    },\n    { win = \"list\", border = \"none\" },\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n  },\n}\n```\n\n### `telescope`\n\n```lua\n{\n  reverse = true,\n  layout = {\n    box = \"horizontal\",\n    backdrop = false,\n    width = 0.8,\n    height = 0.9,\n    border = \"none\",\n    {\n      box = \"vertical\",\n      { win = \"list\", title = \" Results \", title_pos = \"center\", border = true },\n      { win = \"input\", height = 1, border = true, title = \"{title} {live} {flags}\", title_pos = \"center\" },\n    },\n    {\n      win = \"preview\",\n      title = \"{preview:Preview}\",\n      width = 0.45,\n      border = true,\n      title_pos = \"center\",\n    },\n  },\n}\n```\n\n### `top`\n\n```lua\n{ preset = \"ivy\", layout = { position = \"top\" } }\n```\n\n### `vertical`\n\n```lua\n{\n  layout = {\n    backdrop = false,\n    width = 0.5,\n    min_width = 80,\n    height = 0.8,\n    min_height = 30,\n    box = \"vertical\",\n    border = true,\n    title = \"{title} {live} {flags}\",\n    title_pos = \"center\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    { win = \"list\", border = \"none\" },\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n  },\n}\n```\n\n### `vscode`\n\n```lua\n{\n  hidden = { \"preview\" },\n  layout = {\n    backdrop = false,\n    row = 1,\n    width = 0.4,\n    min_width = 80,\n    height = 0.4,\n    border = \"none\",\n    box = \"vertical\",\n    { win = \"input\", height = 1, border = true, title = \"{title} {live} {flags}\", title_pos = \"center\" },\n    { win = \"list\", border = \"hpad\" },\n    { win = \"preview\", title = \"{preview}\", border = true },\n  },\n}\n```\n\n\n## 📦 `snacks.picker.actions`\n\n```lua\n---@class snacks.picker.actions\n---@field [string] snacks.picker.Action.spec\nlocal M = {}\n```\n\n### `Snacks.picker.actions.bufdelete()`\n\n```lua\nSnacks.picker.actions.bufdelete(picker)\n```\n\n### `Snacks.picker.actions.cancel()`\n\n```lua\nSnacks.picker.actions.cancel(picker)\n```\n\n### `Snacks.picker.actions.cd()`\n\n```lua\nSnacks.picker.actions.cd(_, item)\n```\n\n### `Snacks.picker.actions.close()`\n\n```lua\nSnacks.picker.actions.close(picker)\n```\n\n### `Snacks.picker.actions.cmd()`\n\n```lua\nSnacks.picker.actions.cmd(picker, item)\n```\n\n### `Snacks.picker.actions.cycle_win()`\n\n```lua\nSnacks.picker.actions.cycle_win(picker)\n```\n\n### `Snacks.picker.actions.focus_input()`\n\n```lua\nSnacks.picker.actions.focus_input(picker)\n```\n\n### `Snacks.picker.actions.focus_list()`\n\n```lua\nSnacks.picker.actions.focus_list(picker)\n```\n\n### `Snacks.picker.actions.focus_preview()`\n\n```lua\nSnacks.picker.actions.focus_preview(picker)\n```\n\n### `Snacks.picker.actions.git_branch_add()`\n\n```lua\nSnacks.picker.actions.git_branch_add(picker)\n```\n\n### `Snacks.picker.actions.git_branch_del()`\n\n```lua\nSnacks.picker.actions.git_branch_del(picker, item)\n```\n\n### `Snacks.picker.actions.git_checkout()`\n\n```lua\nSnacks.picker.actions.git_checkout(picker, item)\n```\n\n### `Snacks.picker.actions.git_restore()`\n\n```lua\nSnacks.picker.actions.git_restore(picker)\n```\n\n### `Snacks.picker.actions.git_stage()`\n\n```lua\nSnacks.picker.actions.git_stage(picker)\n```\n\n### `Snacks.picker.actions.git_stash_apply()`\n\n```lua\nSnacks.picker.actions.git_stash_apply(_, item)\n```\n\n### `Snacks.picker.actions.help()`\n\n```lua\nSnacks.picker.actions.help(picker, item, action)\n```\n\n### `Snacks.picker.actions.history_back()`\n\n```lua\nSnacks.picker.actions.history_back(picker)\n```\n\n### `Snacks.picker.actions.history_forward()`\n\n```lua\nSnacks.picker.actions.history_forward(picker)\n```\n\n### `Snacks.picker.actions.insert()`\n\n```lua\nSnacks.picker.actions.insert(picker, _, action)\n```\n\n### `Snacks.picker.actions.inspect()`\n\n```lua\nSnacks.picker.actions.inspect(picker, item)\n```\n\n### `Snacks.picker.actions.item_action()`\n\n```lua\nSnacks.picker.actions.item_action(picker, item, action)\n```\n\n### `Snacks.picker.actions.jump()`\n\n```lua\nSnacks.picker.actions.jump(picker, _, action)\n```\n\n### `Snacks.picker.actions.layout()`\n\n```lua\nSnacks.picker.actions.layout(picker, _, action)\n```\n\n### `Snacks.picker.actions.lcd()`\n\n```lua\nSnacks.picker.actions.lcd(_, item)\n```\n\n### `Snacks.picker.actions.list_bottom()`\n\n```lua\nSnacks.picker.actions.list_bottom(picker)\n```\n\n### `Snacks.picker.actions.list_down()`\n\n```lua\nSnacks.picker.actions.list_down(picker)\n```\n\n### `Snacks.picker.actions.list_scroll_bottom()`\n\n```lua\nSnacks.picker.actions.list_scroll_bottom(picker)\n```\n\n### `Snacks.picker.actions.list_scroll_center()`\n\n```lua\nSnacks.picker.actions.list_scroll_center(picker)\n```\n\n### `Snacks.picker.actions.list_scroll_down()`\n\n```lua\nSnacks.picker.actions.list_scroll_down(picker)\n```\n\n### `Snacks.picker.actions.list_scroll_top()`\n\n```lua\nSnacks.picker.actions.list_scroll_top(picker)\n```\n\n### `Snacks.picker.actions.list_scroll_up()`\n\n```lua\nSnacks.picker.actions.list_scroll_up(picker)\n```\n\n### `Snacks.picker.actions.list_top()`\n\n```lua\nSnacks.picker.actions.list_top(picker)\n```\n\n### `Snacks.picker.actions.list_up()`\n\n```lua\nSnacks.picker.actions.list_up(picker)\n```\n\n### `Snacks.picker.actions.load_session()`\n\nTries to load the session, if it fails, it will open the picker.\n\n```lua\nSnacks.picker.actions.load_session(picker, item)\n```\n\n### `Snacks.picker.actions.loclist()`\n\nSend selected or all items to the location list.\n\n```lua\nSnacks.picker.actions.loclist(picker)\n```\n\n### `Snacks.picker.actions.mark_delete()`\n\n```lua\nSnacks.picker.actions.mark_delete(picker)\n```\n\n### `Snacks.picker.actions.paste()`\n\n```lua\nSnacks.picker.actions.paste(picker, item, action)\n```\n\n### `Snacks.picker.actions.pick_win()`\n\n```lua\nSnacks.picker.actions.pick_win(picker, item, action)\n```\n\n### `Snacks.picker.actions.picker()`\n\n```lua\nSnacks.picker.actions.picker(picker, item, action)\n```\n\n### `Snacks.picker.actions.picker_grep()`\n\n```lua\nSnacks.picker.actions.picker_grep(_, item)\n```\n\n### `Snacks.picker.actions.preview_scroll_down()`\n\n```lua\nSnacks.picker.actions.preview_scroll_down(picker)\n```\n\n### `Snacks.picker.actions.preview_scroll_left()`\n\n```lua\nSnacks.picker.actions.preview_scroll_left(picker)\n```\n\n### `Snacks.picker.actions.preview_scroll_right()`\n\n```lua\nSnacks.picker.actions.preview_scroll_right(picker)\n```\n\n### `Snacks.picker.actions.preview_scroll_up()`\n\n```lua\nSnacks.picker.actions.preview_scroll_up(picker)\n```\n\n### `Snacks.picker.actions.print_cwd()`\n\n```lua\nSnacks.picker.actions.print_cwd(picker)\n```\n\n### `Snacks.picker.actions.print_dir()`\n\n```lua\nSnacks.picker.actions.print_dir(picker)\n```\n\n### `Snacks.picker.actions.print_path()`\n\n```lua\nSnacks.picker.actions.print_path(picker, item)\n```\n\n### `Snacks.picker.actions.qflist()`\n\nSend selected or all items to the quickfix list.\n\n```lua\nSnacks.picker.actions.qflist(picker)\n```\n\n### `Snacks.picker.actions.qflist_all()`\n\nSend all items to the quickfix list.\n\n```lua\nSnacks.picker.actions.qflist_all(picker)\n```\n\n### `Snacks.picker.actions.search()`\n\n```lua\nSnacks.picker.actions.search(picker, item)\n```\n\n### `Snacks.picker.actions.select_all()`\n\nSelects all items in the list.\nOr clears the selection if all items are selected.\n\n```lua\nSnacks.picker.actions.select_all(picker)\n```\n\n### `Snacks.picker.actions.select_and_next()`\n\nToggles the selection of the current item,\nand moves the cursor to the next item.\n\n```lua\nSnacks.picker.actions.select_and_next(picker)\n```\n\n### `Snacks.picker.actions.select_and_prev()`\n\nToggles the selection of the current item,\nand moves the cursor to the prev item.\n\n```lua\nSnacks.picker.actions.select_and_prev(picker)\n```\n\n### `Snacks.picker.actions.tcd()`\n\n```lua\nSnacks.picker.actions.tcd(_, item)\n```\n\n### `Snacks.picker.actions.terminal()`\n\n```lua\nSnacks.picker.actions.terminal(_, item)\n```\n\n### `Snacks.picker.actions.toggle_focus()`\n\n```lua\nSnacks.picker.actions.toggle_focus(picker)\n```\n\n### `Snacks.picker.actions.toggle_help_input()`\n\n```lua\nSnacks.picker.actions.toggle_help_input(picker)\n```\n\n### `Snacks.picker.actions.toggle_help_list()`\n\n```lua\nSnacks.picker.actions.toggle_help_list(picker)\n```\n\n### `Snacks.picker.actions.toggle_input()`\n\n```lua\nSnacks.picker.actions.toggle_input(picker)\n```\n\n### `Snacks.picker.actions.toggle_live()`\n\n```lua\nSnacks.picker.actions.toggle_live(picker)\n```\n\n### `Snacks.picker.actions.toggle_maximize()`\n\n```lua\nSnacks.picker.actions.toggle_maximize(picker)\n```\n\n### `Snacks.picker.actions.toggle_preview()`\n\n```lua\nSnacks.picker.actions.toggle_preview(picker)\n```\n\n### `Snacks.picker.actions.yank()`\n\n```lua\nSnacks.picker.actions.yank(picker, item, action)\n```\n\n\n\n## 📦 `snacks.picker.core.picker`\n\n```lua\n---@class snacks.Picker\n---@field id number\n---@field opts snacks.picker.Config\n---@field init_opts? snacks.picker.Config\n---@field finder snacks.picker.Finder\n---@field format snacks.picker.format\n---@field input snacks.picker.input\n---@field layout snacks.layout\n---@field resolved_layout snacks.picker.layout.Config\n---@field list snacks.picker.list\n---@field matcher snacks.picker.Matcher\n---@field main number\n---@field _main snacks.picker.Main\n---@field preview snacks.picker.Preview\n---@field shown? boolean\n---@field sort snacks.picker.sort\n---@field updater uv.uv_timer_t\n---@field start_time number\n---@field title string\n---@field closed? boolean\n---@field history snacks.picker.History\n---@field visual? snacks.picker.Visual\nlocal M = {}\n```\n\n### `Snacks.picker.picker.get()`\n\n```lua\n---@param opts? {source?: string, tab?: boolean}\nSnacks.picker.picker.get(opts)\n```\n\n### `picker:action()`\n\nExecute the given action(s)\n\n```lua\n---@param actions string|string[]\npicker:action(actions)\n```\n\n### `picker:close()`\n\nClose the picker\n\n```lua\npicker:close()\n```\n\n### `picker:count()`\n\nTotal number of items in the picker\n\n```lua\npicker:count()\n```\n\n### `picker:current()`\n\nGet the current item at the cursor\n\n```lua\n---@param opts? {resolve?: boolean} default is `true`\npicker:current(opts)\n```\n\n### `picker:current_win()`\n\n```lua\n---@return string? name, snacks.win? win\npicker:current_win()\n```\n\n### `picker:cwd()`\n\n```lua\npicker:cwd()\n```\n\n### `picker:dir()`\n\nReturns the directory of the current item or the cwd.\nWhen the item is a directory, return item path,\notherwise return the directory of the item.\n\n```lua\npicker:dir()\n```\n\n### `picker:empty()`\n\nCheck if the picker is empty\n\n```lua\npicker:empty()\n```\n\n### `picker:filter()`\n\nGet the active filter\n\n```lua\npicker:filter()\n```\n\n### `picker:find()`\n\nCheck if the finder and/or matcher need to run,\nbased on the current pattern and search string.\n\n```lua\n---@param opts? { on_done?: fun(), refresh?: boolean }\npicker:find(opts)\n```\n\n### `picker:focus()`\n\nFocuses the given or configured window.\nFalls back to the first available window if the window is hidden.\n\n```lua\n---@param win? \"input\"|\"list\"|\"preview\"\n---@param opts? {show?: boolean} when enable is true, the window will be shown if hidden\npicker:focus(win, opts)\n```\n\n### `picker:hist()`\n\nMove the history cursor\n\n```lua\n---@param forward? boolean\npicker:hist(forward)\n```\n\n### `picker:is_active()`\n\nCheck if the finder or matcher is running\n\n```lua\npicker:is_active()\n```\n\n### `picker:is_focused()`\n\n```lua\npicker:is_focused()\n```\n\n### `picker:items()`\n\nGet all filtered items in the picker.\n\n```lua\npicker:items()\n```\n\n### `picker:iter()`\n\nReturns an iterator over the filtered items in the picker.\nItems will be in sorted order.\n\n```lua\n---@return fun():(snacks.picker.Item?, number?)\npicker:iter()\n```\n\n### `picker:norm()`\n\nExecute the callback in normal mode.\nWhen still in insert mode, stop insert mode first,\nand then`vim.schedule` the callback.\n\n```lua\n---@param cb fun()\npicker:norm(cb)\n```\n\n### `picker:on_current_tab()`\n\n```lua\npicker:on_current_tab()\n```\n\n### `picker:ref()`\n\n```lua\n---@return snacks.Picker.ref\npicker:ref()\n```\n\n### `picker:refresh()`\n\nClears the selection, set the target to the current item,\nand refresh the finder and matcher.\n\n```lua\npicker:refresh()\n```\n\n### `picker:resolve()`\n\n```lua\n---@param item snacks.picker.Item?\npicker:resolve(item)\n```\n\n### `picker:selected()`\n\nGet the selected items.\nIf `fallback=true` and there is no selection, return the current item.\n\n```lua\n---@param opts? {fallback?: boolean} default is `false`\n---@return snacks.picker.Item[]\npicker:selected(opts)\n```\n\n### `picker:set_cwd()`\n\n```lua\npicker:set_cwd(cwd)\n```\n\n### `picker:set_layout()`\n\nSet the picker layout. Can be either the name of a preset layout\nor a custom layout configuration.\n\n```lua\n---@param layout? string|snacks.picker.layout.Config\npicker:set_layout(layout)\n```\n\n### `picker:show_preview()`\n\nShow the preview. Show instantly when no item is yet in the preview,\notherwise throttle the preview.\n\n```lua\npicker:show_preview()\n```\n\n### `picker:toggle()`\n\nToggle the given window and optionally focus\n\n```lua\n---@param win \"input\"|\"list\"|\"preview\"\n---@param opts? {enable?: boolean, focus?: boolean|string}\npicker:toggle(win, opts)\n```\n\n### `picker:word()`\n\nGet the word under the cursor or the current visual selection\n\n```lua\npicker:word()\n```\n"
  },
  {
    "path": "docs/profiler.md",
    "content": "# 🍿 profiler\n\nA low overhead Lua profiler for Neovim.\n\n![image](https://github.com/user-attachments/assets/cebb1308-077b-4f20-bee3-28644fb121b8)\n\n![image](https://github.com/user-attachments/assets/4ee557c4-a290-4a52-b5c9-64e325bf1094)\n\n![image](https://github.com/user-attachments/assets/ec03e440-6719-4463-a649-a8626dcfe2ec)\n\n## ✨ Features\n\n- low overhead **instrumentation**\n- captures a function's **def**inition and **ref**erence (_caller_) locations\n- profiling of **autocmds**\n- profiling of **require**d modules\n- buffer **highlighting** of functions and calls\n- lots of different ways to **filter** and **group** traces\n- show traces with:\n  - [fzf-lua](https://github.com/ibhagwan/fzf-lua)\n  - [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)\n  - [trouble.nvim](https://github.com/folke/trouble.nvim)\n\n## ⁉️ Why?\n\nBefore the snacks profiler, I used to use a combination of my own profiler(s),\n**lazy.nvim**'s internal profiler, [profile.nvim](https://github.com/stevearc/profile.nvim)\nand [perfanno.nvim](https://github.com/t-troebst/perfanno.nvim).\n\nThey all have their strengths and weaknesses:\n\n- **lazy.nvim**'s profiler is great for structured traces, but needed a lot of\n  manual work to get the traces I wanted.\n- **profile.nvim** does proper instrumentation, but was lacking in the UI department.\n- **perfanno.nvim** has a great UI, but uses `jit.profile` which is not as\n  detailed as instrumentation.\n\nThe snacks profiler tries to combine the best of all worlds.\n\n## 🚀 Usage\n\nThe easiest way to use the profiler is to toggle it with the suggested keybindings.\n\nWhen the profiler stops, it will show a picker using the `on_stop` preset.\n\nTo quickly change picker options, you can use the `Snacks.profiler.scratch()`\nscratch buffer.\n\n### Caveats\n\n- your Neovim session might slow down when profiling\n- due to the overhead of instrumentation, fast functions that are called\n  often, might skew the results. Best to add those to the `opts.filter_fn` config.\n- by default, only captures functions defined on lua modules.\n  If you want to profile others, add them to `opts.globals`\n- the profiler is not perfect and might not capture all calls\n- the profiler might not work well with some plugins\n- it can only profile `autocmds` created when the profiler is running.\n- only `autocmds` with a lua function callback can be profiled\n- functions that `resume` or `yield` won't be captured correctly\n- functions that do blocking calls like `vim.fn.getchar` will work,\n  but the time will include the time spent waiting for the blocking call\n\n### Recommended Setup\n\n```lua\n{\n  {\n    \"folke/snacks.nvim\",\n    opts = function()\n      -- Toggle the profiler\n      Snacks.toggle.profiler():map(\"<leader>pp\")\n      -- Toggle the profiler highlights\n      Snacks.toggle.profiler_highlights():map(\"<leader>ph\")\n    end,\n    keys = {\n      { \"<leader>ps\", function() Snacks.profiler.scratch() end, desc = \"Profiler Scratch Bufer\" },\n    }\n  },\n  -- optional lualine component to show captured events\n  -- when the profiler is running\n  {\n    \"nvim-lualine/lualine.nvim\",\n    opts = function(_, opts)\n      table.insert(opts.sections.lualine_x, Snacks.profiler.status())\n    end,\n  },\n}\n```\n\n### Profiling Neovim Startup\n\nIn order to profile Neovim's startup, you need to make sure `snacks.nvim` is\ninstalled and loaded **before** doing anything else. So also before loading\nyour plugin manager.\n\nYou can add something like the below to the top of your `init.lua`.\n\nThen you can profile your Neovim session, with `PROF=1 nvim`.\n\n```lua\nif vim.env.PROF then\n  -- example for lazy.nvim\n  -- change this to the correct path for your plugin manager\n  local snacks = vim.fn.stdpath(\"data\") .. \"/lazy/snacks.nvim\"\n  vim.opt.rtp:append(snacks)\n  require(\"snacks.profiler\").startup({\n    startup = {\n      event = \"VimEnter\", -- stop profiler on this event. Defaults to `VimEnter`\n      -- event = \"UIEnter\",\n      -- event = \"VeryLazy\",\n    },\n  })\nend\n```\n\n### Filtering\n\nFor the full definition, see the `snacks.profiler.Filter` type.\n\nEach field can be a string or a boolean.\n\nWhen a field is a string, it will match the exact value,\nunless it starts with `^` in which case it will match the pattern.\n\nWhen any of the `def`/`ref` fields are `true`,\nthe filter matches the current location of the cursor.\n\nFor example, `{ref_file = true}` will match all traces calling something,\nin the current file.\n\nAll other fields equal to `true` will match if the trace has a value for that field.\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    profiler = {\n      -- your profiler configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.profiler.Config\n{\n  autocmds = true,\n  runtime = vim.env.VIMRUNTIME, ---@type string\n  -- thresholds for buttons to be shown as info, warn or error\n  -- value is a tuple of [warn, error]\n  thresholds = {\n    time = { 2, 10 },\n    pct = { 10, 20 },\n    count = { 10, 100 },\n  },\n  on_stop = {\n    highlights = true, -- highlight entries after stopping the profiler\n    pick = true, -- show a picker after stopping the profiler (uses the `on_stop` preset)\n  },\n  ---@type snacks.profiler.Highlights\n  highlights = {\n    min_time = 0, -- only highlight entries with time > min_time (in ms)\n    max_shade = 20, -- time in ms for the darkest shade\n    badges = { \"time\", \"pct\", \"count\", \"trace\" },\n    align = 80,\n  },\n  pick = {\n    picker = \"snacks\", ---@type snacks.profiler.Picker\n    ---@type snacks.profiler.Badge.type[]\n    badges = { \"time\", \"count\", \"name\" },\n    ---@type snacks.profiler.Highlights\n    preview = {\n      badges = { \"time\", \"pct\", \"count\" },\n      align = \"right\",\n    },\n  },\n  startup = {\n    event = \"VimEnter\", -- stop profiler on this event. Defaults to `VimEnter`\n    after = true, -- stop the profiler **after** the event. When false it stops **at** the event\n    pattern = nil, -- pattern to match for the autocmd\n    pick = true, -- show a picker after starting the profiler (uses the `startup` preset)\n  },\n  ---@type table<string, snacks.profiler.Pick|fun():snacks.profiler.Pick?>\n  presets = {\n    startup = { min_time = 1, sort = false },\n    on_stop = {},\n    filter_by_plugin = function()\n      return { filter = { def_plugin = vim.fn.input(\"Filter by plugin: \") } }\n    end,\n  },\n  ---@type string[]\n  globals = {\n    -- \"vim\",\n    -- \"vim.api\",\n    -- \"vim.keymap\",\n    -- \"Snacks.dashboard.Dashboard\",\n  },\n  -- filter modules by pattern.\n  -- longest patterns are matched first\n  filter_mod = {\n    default = true, -- default value for unmatched patterns\n    [\"^vim%.\"] = false,\n    [\"mason-core.functional\"] = false,\n    [\"mason-core.functional.data\"] = false,\n    [\"mason-core.optional\"] = false,\n    [\"which-key.state\"] = false,\n  },\n  filter_fn = {\n    default = true,\n    [\"^.*%._[^%.]*$\"] = false,\n    [\"trouble.filter.is\"] = false,\n    [\"trouble.item.__index\"] = false,\n    [\"which-key.node.__index\"] = false,\n    [\"smear_cursor.draw.wo\"] = false,\n    [\"^ibl%.utils%.\"] = false,\n  },\n  icons = {\n    time    = \" \",\n    pct     = \" \",\n    count   = \" \",\n    require = \"󰋺 \",\n    modname = \"󰆼 \",\n    plugin  = \" \",\n    autocmd = \"⚡\",\n    file    = \" \",\n    fn      = \"󰊕 \",\n    status  = \"󰈸 \",\n  },\n}\n```\n\n## 📚 Types\n\n### Traces\n\n```lua\n---@class snacks.profiler.Trace\n---@field name string fully qualified name of the function\n---@field time number time in nanoseconds\n---@field depth number stack depth\n---@field [number] snacks.profiler.Trace child traces\n---@field fname string function name\n---@field fn function function reference\n---@field modname? string module name\n---@field require? string special case for require\n---@field autocmd? string special case for autocmd\n---@field count? number number of calls\n---@field def? snacks.profiler.Loc location of the definition\n---@field ref? snacks.profiler.Loc location of the reference (caller)\n---@field loc? snacks.profiler.Loc normalized location\n```\n\n```lua\n---@class snacks.profiler.Loc\n---@field file string path to the file\n---@field line number line number\n---@field loc? string normalized location\n---@field modname? string module name\n---@field plugin? string plugin name\n```\n\n### Pick: grouping, filtering and sorting\n\n```lua\n---@class snacks.profiler.Find\n---@field structure? boolean show traces as a tree or flat list\n---@field sort? \"time\"|\"count\"|false sort by time or count, or keep original order\n---@field loc? \"def\"|\"ref\" what location to show in the preview\n---@field group? boolean|snacks.profiler.Field group traces by field\n---@field filter? snacks.profiler.Filter filter traces by field(s)\n---@field min_time? number only show grouped traces with `time >= min_time`\n```\n\n```lua\n---@class snacks.profiler.Pick: snacks.profiler.Find\n---@field picker? snacks.profiler.Picker\n```\n\n```lua\n---@alias snacks.profiler.Picker \"snacks\"|\"trouble\"\n---@alias snacks.profiler.Pick.spec snacks.profiler.Pick|{preset?:string}|fun():snacks.profiler.Pick\n```\n\n```lua\n---@alias snacks.profiler.Field\n---| \"name\" fully qualified name of the function\n---| \"def\" definition\n---| \"ref\" reference (caller)\n---| \"require\" require\n---| \"autocmd\" autocmd\n---| \"modname\" module name of the called function\n---| \"def_file\" file of the definition\n---| \"def_modname\" module name of the definition\n---| \"def_plugin\" plugin that defines the function\n---| \"ref_file\" file of the reference\n---| \"ref_modname\" module name of the reference\n---| \"ref_plugin\" plugin that references the function\n```\n\n```lua\n---@class snacks.profiler.Filter\n---@field name? string|boolean fully qualified name of the function\n---@field def? string|boolean location of the definition\n---@field ref? string|boolean location of the reference (caller)\n---@field require? string|boolean special case for require\n---@field autocmd? string|boolean special case for autocmd\n---@field modname? string|boolean module name\n---@field def_file? string|boolean file of the definition\n---@field def_modname? string|boolean module name of the definition\n---@field def_plugin? string|boolean plugin that defines the function\n---@field ref_file? string|boolean file of the reference\n---@field ref_modname? string|boolean module name of the reference\n---@field ref_plugin? string|boolean plugin that references the function\n```\n\n### UI\n\n```lua\n---@alias snacks.profiler.Badge {icon:string, text:string, padding?:boolean, level?:string}\n---@alias snacks.profiler.Badge.type \"time\"|\"pct\"|\"count\"|\"name\"|\"trace\"\n```\n\n```lua\n---@class snacks.profiler.Highlights\n---@field min_time? number only highlight entries with time >= min_time\n---@field max_shade? number -- time in ms for the darkest shade\n---@field badges? snacks.profiler.Badge.type[] badges to show\n---@field align? \"right\"|\"left\"|number align the badges right, left or at a specific column\n```\n\n### Other\n\n```lua\n---@class snacks.profiler.Startup\n---@field event? string\n---@field pattern? string|string[] pattern to match for the autocmd\n```\n\n```lua\n---@alias snacks.profiler.GroupFn fun(entry:snacks.profiler.Trace):{key:string, name?:string}?\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.profiler\n---@field core snacks.profiler.core\n---@field loc snacks.profiler.loc\n---@field tracer snacks.profiler.tracer\n---@field ui snacks.profiler.ui\n---@field picker snacks.profiler.picker\nSnacks.profiler = {}\n```\n\n### `Snacks.profiler.find()`\n\nGroup and filter traces\n\n```lua\n---@param opts snacks.profiler.Find\nSnacks.profiler.find(opts)\n```\n\n### `Snacks.profiler.highlight()`\n\nToggle the profiler highlights\n\n```lua\n---@param enable? boolean\nSnacks.profiler.highlight(enable)\n```\n\n### `Snacks.profiler.pick()`\n\nGroup and filter traces and open a picker\n\n```lua\n---@param opts? snacks.profiler.Pick.spec\nSnacks.profiler.pick(opts)\n```\n\n### `Snacks.profiler.running()`\n\nCheck if the profiler is running\n\n```lua\nSnacks.profiler.running()\n```\n\n### `Snacks.profiler.scratch()`\n\nOpen a scratch buffer with the profiler picker options\n\n```lua\nSnacks.profiler.scratch()\n```\n\n### `Snacks.profiler.start()`\n\nStart the profiler\n\n```lua\n---@param opts? snacks.profiler.Config\nSnacks.profiler.start(opts)\n```\n\n### `Snacks.profiler.startup()`\n\nStart the profiler on startup, and stop it after the event has been triggered.\n\n```lua\n---@param opts snacks.profiler.Config\nSnacks.profiler.startup(opts)\n```\n\n### `Snacks.profiler.status()`\n\nStatusline component\n\n```lua\nSnacks.profiler.status()\n```\n\n### `Snacks.profiler.stop()`\n\nStop the profiler\n\n```lua\n---@param opts? {highlights?:boolean, pick?:snacks.profiler.Pick.spec}\nSnacks.profiler.stop(opts)\n```\n\n### `Snacks.profiler.toggle()`\n\nToggle the profiler\n\n```lua\nSnacks.profiler.toggle()\n```\n"
  },
  {
    "path": "docs/quickfile.md",
    "content": "# 🍿 quickfile\n\nWhen doing `nvim somefile.txt`, it will render the file as quickly as possible,\nbefore loading your plugins.\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    quickfile = {\n      -- your quickfile configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.quickfile.Config\n{\n  -- any treesitter langs to exclude\n  exclude = { \"latex\" },\n}\n```\n"
  },
  {
    "path": "docs/rename.md",
    "content": "# 🍿 rename\n\nLSP-integrated file renaming with support for plugins like\n[neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim) and [mini.files](https://github.com/nvim-mini/mini.files).\n\n## 🚀 Usage\n\n## [mini.files](https://github.com/nvim-mini/mini.files)\n\n```lua\nvim.api.nvim_create_autocmd(\"User\", {\n  pattern = \"MiniFilesActionRename\",\n  callback = function(event)\n    Snacks.rename.on_rename_file(event.data.from, event.data.to)\n  end,\n})\n```\n\n## [oil.nvim](https://github.com/stevearc/oil.nvim)\n\n```lua\nvim.api.nvim_create_autocmd(\"User\", {\n  pattern = \"OilActionsPost\",\n  callback = function(event)\n      if event.data.actions[1].type == \"move\" then\n          Snacks.rename.on_rename_file(event.data.actions[1].src_url, event.data.actions[1].dest_url)\n      end\n  end,\n})\n```\n\n## [fyler.nvim](https://github.com/A7Lavinraj/fyler.nvim)\n\n```lua\nreturn {\n  \"A7Lavinraj/fyler.nvim\",\n  dependencies = { \"echasnovski/mini.icons\" },\n  opts = {\n    hooks = {\n      on_rename = function(src_path, destination_path)\n        Snacks.rename.on_rename_file(src_path, destination_path)\n      end,\n    },\n  },\n}\n```\n\n## [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim)\n\n```lua\n{\n  \"nvim-neo-tree/neo-tree.nvim\",\n  opts = function(_, opts)\n    local function on_move(data)\n      Snacks.rename.on_rename_file(data.source, data.destination)\n    end\n    local events = require(\"neo-tree.events\")\n    opts.event_handlers = opts.event_handlers or {}\n    vim.list_extend(opts.event_handlers, {\n      { event = events.FILE_MOVED, handler = on_move },\n      { event = events.FILE_RENAMED, handler = on_move },\n    })\n  end,\n}\n```\n\n## [nvim-tree](https://github.com/nvim-tree/nvim-tree.lua)\n\n```lua\nlocal prev = { new_name = \"\", old_name = \"\" } -- Prevents duplicate events\nvim.api.nvim_create_autocmd(\"User\", {\n  pattern = \"NvimTreeSetup\",\n  callback = function()\n    local events = require(\"nvim-tree.api\").events\n    events.subscribe(events.Event.NodeRenamed, function(data)\n      if prev.new_name ~= data.new_name or prev.old_name ~= data.old_name then\n        data = data\n        Snacks.rename.on_rename_file(data.old_name, data.new_name)\n      end\n    end)\n  end,\n})\n```\n\n## netrw (builtin file explorer)\n\n```lua\nvim.api.nvim_create_autocmd({ 'FileType' }, {\n  pattern = { 'netrw' },\n  group = vim.api.nvim_create_augroup('NetrwOnRename', { clear = true }),\n  callback = function()\n    vim.keymap.set(\"n\", \"R\", function()\n      local original_file_path = vim.b.netrw_curdir .. '/' .. vim.fn[\"netrw#Call\"](\"NetrwGetWord\")\n\n      vim.ui.input({ prompt = 'Move/rename to:', default = original_file_path }, function(target_file_path)\n        if target_file_path and target_file_path ~= \"\" then\n          local file_exists = vim.uv.fs_access(target_file_path, \"W\")\n\n          if not file_exists then\n            vim.uv.fs_rename(original_file_path, target_file_path)\n\n            Snacks.rename.on_rename_file(original_file_path, target_file_path)\n          else\n            vim.notify(\"File '\" .. target_file_path .. \"' already exists! Skipping...\", vim.log.levels.ERROR)\n          end\n\n          -- Refresh netrw\n          vim.cmd(':Ex ' .. vim.b.netrw_curdir)\n        end\n      end)\n    end, { remap = true, buffer = true })\n  end\n})\n```\n\n<!-- docgen -->\n\n## 📦 Module\n\n### `Snacks.rename.on_rename_file()`\n\nLets LSP clients know that a file has been renamed\n\n```lua\n---@param from string\n---@param to string\n---@param rename? fun()\nSnacks.rename.on_rename_file(from, to, rename)\n```\n\n### `Snacks.rename.rename_file()`\n\nRenames the provided file, or the current buffer's file.\nPrompt for the new filename if `to` is not provided.\ndo the rename, and trigger LSP handlers\n\n```lua\n---@param opts? {from?: string, to?:string, on_rename?: fun(to:string, from:string, ok:boolean)}\nSnacks.rename.rename_file(opts)\n```\n"
  },
  {
    "path": "docs/scope.md",
    "content": "# 🍿 scope\n\nScope detection based on treesitter or indent.\n\nThe indent-based algorithm is similar to what is used\nin [mini.indentscope](https://github.com/nvim-mini/mini.indentscope).\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    scope = {\n      -- your scope configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.scope.Config\n---@field max_size? number\n---@field enabled? boolean\n{\n  -- absolute minimum size of the scope.\n  -- can be less if the scope is a top-level single line scope\n  min_size = 2,\n  -- try to expand the scope to this size\n  max_size = nil,\n  cursor = true, -- when true, the column of the cursor is used to determine the scope\n  edge = true, -- include the edge of the scope (typically the line above and below with smaller indent)\n  siblings = false, -- expand single line scopes with single line siblings\n  -- what buffers to attach to\n  filter = function(buf)\n    return vim.bo[buf].buftype == \"\" and vim.b[buf].snacks_scope ~= false and vim.g.snacks_scope ~= false\n  end,\n  -- debounce scope detection in ms\n  debounce = 30,\n  treesitter = {\n    -- detect scope based on treesitter.\n    -- falls back to indent based detection if not available\n    enabled = true,\n    injections = true, -- include language injections when detecting scope (useful for languages like `vue`)\n    ---@type string[]|{enabled?:boolean}\n    blocks = {\n      enabled = false, -- enable to use the following blocks\n      \"function_declaration\",\n      \"function_definition\",\n      \"method_declaration\",\n      \"method_definition\",\n      \"class_declaration\",\n      \"class_definition\",\n      \"do_statement\",\n      \"while_statement\",\n      \"repeat_statement\",\n      \"if_statement\",\n      \"for_statement\",\n    },\n    -- these treesitter fields will be considered as blocks\n    field_blocks = {\n      \"local_declaration\",\n    },\n  },\n  -- These keymaps will only be set if the `scope` plugin is enabled.\n  -- Alternatively, you can set them manually in your config,\n  -- using the `Snacks.scope.textobject` and `Snacks.scope.jump` functions.\n  keys = {\n    ---@type table<string, snacks.scope.TextObject|{desc?:string}|false>\n    textobject = {\n      ii = {\n        min_size = 2, -- minimum size of the scope\n        edge = false, -- inner scope\n        cursor = false,\n        treesitter = { blocks = { enabled = false } },\n        desc = \"inner scope\",\n      },\n      ai = {\n        cursor = false,\n        min_size = 2, -- minimum size of the scope\n        treesitter = { blocks = { enabled = false } },\n        desc = \"full scope\",\n      },\n    },\n    ---@type table<string, snacks.scope.Jump|{desc?:string}|false>\n    jump = {\n      [\"[i\"] = {\n        min_size = 1, -- allow single line scopes\n        bottom = false,\n        cursor = false,\n        edge = true,\n        treesitter = { blocks = { enabled = false } },\n        desc = \"jump to top edge of scope\",\n      },\n      [\"]i\"] = {\n        min_size = 1, -- allow single line scopes\n        bottom = true,\n        cursor = false,\n        edge = true,\n        treesitter = { blocks = { enabled = false } },\n        desc = \"jump to bottom edge of scope\",\n      },\n    },\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.scope.Opts: snacks.scope.Config,{}\n---@field buf? number\n---@field pos? {[1]:number, [2]:number} -- (1,0) indexed\n---@field end_pos? {[1]:number, [2]:number} -- (1,0) indexed\n---@field async? boolean run scope detection asynchronously (defaults to true)\n```\n\n```lua\n---@class snacks.scope.TextObject: snacks.scope.Opts\n---@field linewise? boolean if nil, use visual mode. Defaults to `false` when not in visual mode\n---@field notify? boolean show a notification when no scope is found (defaults to true)\n```\n\n```lua\n---@class snacks.scope.Jump: snacks.scope.Opts\n---@field bottom? boolean if true, jump to the bottom of the scope, otherwise to the top\n---@field notify? boolean show a notification when no scope is found (defaults to true)\n```\n\n```lua\n---@alias snacks.scope.Attach.cb fun(win: number, buf: number, scope:snacks.scope.Scope?, prev:snacks.scope.Scope?)\n```\n\n```lua\n---@alias snacks.scope.scope {buf: number, from: number, to: number, indent?: number}\n```\n\n## 📦 Module\n\n### `Snacks.scope.attach()`\n\nAttach a scope listener\n\n```lua\n---@param cb snacks.scope.Attach.cb\n---@param opts? snacks.scope.Config\n---@return snacks.scope.Listener\nSnacks.scope.attach(cb, opts)\n```\n\n### `Snacks.scope.get()`\n\n```lua\n---@param cb fun(scope?: snacks.scope.Scope)\n---@param opts? snacks.scope.Opts|{parse?:boolean}\nSnacks.scope.get(cb, opts)\n```\n\n### `Snacks.scope.jump()`\n\nJump to the top or bottom of the scope\nIf the scope is the same as the current scope, it will jump to the parent scope instead.\n\n```lua\n---@param opts? snacks.scope.Jump\nSnacks.scope.jump(opts)\n```\n\n### `Snacks.scope.textobject()`\n\nText objects for indent scopes.\nBest to use with Treesitter disabled.\nWhen in visual mode, it will select the scope containing the visual selection.\nWhen the scope is the same as the visual selection, it will select the parent scope instead.\n\n```lua\n---@param opts? snacks.scope.TextObject\nSnacks.scope.textobject(opts)\n```\n"
  },
  {
    "path": "docs/scratch.md",
    "content": "# 🍿 scratch\n\nQuickly open scratch buffers for testing code, creating notes or\njust messing around. Scratch buffers are organized by using context\nlike your working directory, Git branch and `vim.v.count1`.\n\nIt supports templates, custom keymaps, and auto-saves when you hide the buffer.\n\nIn lua buffers, pressing `<cr>` will execute the buffer / selection with\n`Snacks.debug.run()` that will show print output inline and show errors as diagnostics.\n\n![image](https://github.com/user-attachments/assets/52ac7c1a-908f-4d1d-97a2-ad4642f8dc36)\n\n![image](https://github.com/user-attachments/assets/d3e766e9-e64a-4c22-85b4-3d965f645b59)\n\n## 🚀 Usage\n\nSuggested config:\n\n```lua\n{\n  \"folke/snacks.nvim\",\n  keys = {\n    { \"<leader>.\",  function() Snacks.scratch() end, desc = \"Toggle Scratch Buffer\" },\n    { \"<leader>S\",  function() Snacks.scratch.select() end, desc = \"Select Scratch Buffer\" },\n  }\n}\n```\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    scratch = {\n      -- your scratch configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.scratch.Config\n---@field win? snacks.win.Config scratch window\n---@field template? string template for new buffers\n---@field file? string scratch file path. You probably don't need to set this.\n---@field ft? string|fun():string the filetype of the scratch buffer\n{\n  name = \"Scratch\",\n  ft = function()\n    if vim.bo.buftype == \"\" and vim.bo.filetype ~= \"\" then\n      return vim.bo.filetype\n    end\n    return \"markdown\"\n  end,\n  ---@type string|string[]?\n  icon = nil, -- `icon|{icon, icon_hl}`. defaults to the filetype icon\n  root = vim.fn.stdpath(\"data\") .. \"/scratch\",\n  autowrite = true, -- automatically write when the buffer is hidden\n  -- unique key for the scratch file is based on:\n  -- * name\n  -- * ft\n  -- * vim.v.count1 (useful for keymaps)\n  -- * cwd (optional)\n  -- * branch (optional)\n  filekey = {\n    id = nil, ---@type string? unique id used instead of name for the filename hash\n    cwd = true, -- use current working directory\n    branch = true, -- use current branch name\n    count = true, -- use vim.v.count1\n  },\n  win = { style = \"scratch\" },\n  ---@type table<string, snacks.win.Config>\n  win_by_ft = {\n    lua = {\n      keys = {\n        [\"source\"] = {\n          \"<cr>\",\n          function(self)\n            local name = \"scratch.\" .. vim.fn.fnamemodify(vim.api.nvim_buf_get_name(self.buf), \":e\")\n            Snacks.debug.run({ buf = self.buf, name = name })\n          end,\n          desc = \"Source buffer\",\n          mode = { \"n\", \"x\" },\n        },\n      },\n    },\n  },\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `scratch`\n\n```lua\n{\n  width = 100,\n  height = 30,\n  bo = { buftype = \"\", buflisted = false, bufhidden = \"hide\", swapfile = false },\n  minimal = false,\n  noautocmd = false,\n  -- position = \"right\",\n  zindex = 20,\n  wo = { winhighlight = \"NormalFloat:Normal\" },\n  footer_keys = true,\n  border = true,\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.scratch.File\n---@field file string full path to the scratch buffer\n---@field name string name of the scratch buffer\n---@field ft string file type\n---@field icon? string icon for the file type\n---@field icon_hl? string highlight group for the icon\n---@field cwd? string current working directory\n---@field branch? string Git branch\n---@field count? number vim.v.count1 used to open the buffer\n---@field id? string unique id used instead of name for the filename hash\n```\n\n## 📦 Module\n\n### `Snacks.scratch()`\n\n```lua\n---@type fun(opts?: snacks.scratch.Config): snacks.win\nSnacks.scratch()\n```\n\n### `Snacks.scratch.list()`\n\nReturn a list of scratch buffers sorted by mtime.\n\n```lua\n---@return snacks.scratch.File[]\nSnacks.scratch.list()\n```\n\n### `Snacks.scratch.open()`\n\nOpen a scratch buffer with the given options.\nIf a window is already open with the same buffer,\nit will be closed instead.\n\n```lua\n---@param opts? snacks.scratch.Config\nSnacks.scratch.open(opts)\n```\n\n### `Snacks.scratch.select()`\n\nSelect a scratch buffer from a list of scratch buffers.\n\n```lua\nSnacks.scratch.select()\n```\n"
  },
  {
    "path": "docs/scroll.md",
    "content": "# 🍿 scroll\n\nSmooth scrolling for Neovim.\nProperly handles `scrolloff` and mouse scrolling.\n\nSimilar plugins:\n\n- [mini.animate](https://github.com/nvim-mini/mini.animate)\n- [neoscroll.nvim](https://github.com/karb94/neoscroll.nvim)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    scroll = {\n      -- your scroll configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.scroll.Config\n---@field animate snacks.animate.Config|{}\n---@field animate_repeat snacks.animate.Config|{}|{delay:number}\n{\n  animate = {\n    duration = { step = 10, total = 200 },\n    easing = \"linear\",\n  },\n  -- faster animation when repeating scroll after delay\n  animate_repeat = {\n    delay = 100, -- delay in ms before using the repeat animation\n    duration = { step = 5, total = 50 },\n    easing = \"linear\",\n  },\n  -- what buffers to animate\n  filter = function(buf)\n    return vim.g.snacks_scroll ~= false and vim.b[buf].snacks_scroll ~= false and vim.bo[buf].buftype ~= \"terminal\"\n  end,\n}\n```\n\n## 📚 Types\n\n```lua\n---@alias snacks.scroll.View {topline:number, lnum:number}\n```\n\n## 📦 Module\n\n### `Snacks.scroll.disable()`\n\n```lua\nSnacks.scroll.disable()\n```\n\n### `Snacks.scroll.enable()`\n\n```lua\nSnacks.scroll.enable()\n```\n"
  },
  {
    "path": "docs/statuscolumn.md",
    "content": "# 🍿 statuscolumn\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    statuscolumn = {\n      -- your statuscolumn configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.statuscolumn.Config\n---@field left snacks.statuscolumn.Components\n---@field right snacks.statuscolumn.Components\n---@field enabled? boolean\n{\n  left = { \"mark\", \"sign\" }, -- priority of signs on the left (high to low)\n  right = { \"fold\", \"git\" }, -- priority of signs on the right (high to low)\n  folds = {\n    open = false, -- show open fold icons\n    git_hl = false, -- use Git Signs hl for fold icons\n  },\n  git = {\n    -- patterns to match Git signs\n    patterns = { \"GitSign\", \"MiniDiffSign\" },\n  },\n  refresh = 50, -- refresh at most every 50ms\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.statuscolumn.FoldInfo\n---@field start number Line number where deepest fold starts\n---@field level number Fold level, when zero other fields are N/A\n---@field llevel number Lowest level that starts in v:lnum\n---@field lines number Number of lines from v:lnum to end of closed fold\n```\n\n```lua\n---@alias snacks.statuscolumn.Component \"mark\"|\"sign\"|\"fold\"|\"git\"\n---@alias snacks.statuscolumn.Components snacks.statuscolumn.Component[]|fun(win:number,buf:number,lnum:number):snacks.statuscolumn.Component[]\n---@alias snacks.statuscolumn.Wanted table<snacks.statuscolumn.Component, boolean>\n```\n\n## 📦 Module\n\n### `Snacks.statuscolumn()`\n\n```lua\n---@type fun(): string\nSnacks.statuscolumn()\n```\n\n### `Snacks.statuscolumn.click_fold()`\n\n```lua\nSnacks.statuscolumn.click_fold()\n```\n\n### `Snacks.statuscolumn.get()`\n\n```lua\nSnacks.statuscolumn.get()\n```\n"
  },
  {
    "path": "docs/styles.md",
    "content": "# 🍿 styles\n\nPlugins provide window styles that can be customized\nwith the `opts.styles` option of `snacks.nvim`.\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    ---@type table<string, snacks.win.Config>\n    styles = {\n      -- your styles configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## 🎨 Styles\n\nThese are the default styles that Snacks provides.\nYou can customize them by adding your own styles to `opts.styles`.\n\n\n### `blame_line`\n\n```lua\n{\n  width = 0.6,\n  height = 0.6,\n  border = true,\n  title = \" Git Blame \",\n  title_pos = \"center\",\n  ft = \"git\",\n}\n```\n\n### `dashboard`\n\nThe default style for the dashboard.\nWhen opening the dashboard during startup, only the `bo` and `wo` options are used.\nThe other options are used with `:lua Snacks.dashboard()`\n\n```lua\n{\n  zindex = 10,\n  height = 0,\n  width = 0,\n  bo = {\n    bufhidden = \"wipe\",\n    buftype = \"nofile\",\n    buflisted = false,\n    filetype = \"snacks_dashboard\",\n    swapfile = false,\n    undofile = false,\n  },\n  wo = {\n    colorcolumn = \"\",\n    cursorcolumn = false,\n    cursorline = false,\n    foldmethod = \"manual\",\n    list = false,\n    number = false,\n    relativenumber = false,\n    sidescrolloff = 0,\n    signcolumn = \"no\",\n    spell = false,\n    statuscolumn = \"\",\n    statusline = \"\",\n    winbar = \"\",\n    winhighlight = \"Normal:SnacksDashboardNormal,NormalFloat:SnacksDashboardNormal\",\n    wrap = false,\n  },\n}\n```\n\n### `float`\n\n```lua\n{\n  position = \"float\",\n  backdrop = 60,\n  height = 0.9,\n  width = 0.9,\n  zindex = 50,\n}\n```\n\n### `help`\n\n```lua\n{\n  position = \"float\",\n  backdrop = false,\n  border = \"top\",\n  row = -1,\n  width = 0,\n  height = 0.3,\n}\n```\n\n### `input`\n\n```lua\n{\n  backdrop = false,\n  position = \"float\",\n  border = true,\n  title_pos = \"center\",\n  height = 1,\n  width = 60,\n  relative = \"editor\",\n  noautocmd = true,\n  row = 2,\n  -- relative = \"cursor\",\n  -- row = -3,\n  -- col = 0,\n  wo = {\n    winhighlight = \"NormalFloat:SnacksInputNormal,FloatBorder:SnacksInputBorder,FloatTitle:SnacksInputTitle\",\n    cursorline = false,\n  },\n  bo = {\n    filetype = \"snacks_input\",\n    buftype = \"prompt\",\n  },\n  --- buffer local variables\n  b = {\n    completion = false, -- disable blink completions in input\n  },\n  keys = {\n    n_esc = { \"<esc>\", { \"cmp_close\", \"cancel\" }, mode = \"n\", expr = true },\n    i_esc = { \"<esc>\", { \"cmp_close\", \"stopinsert\" }, mode = \"i\", expr = true },\n    i_cr = { \"<cr>\", { \"cmp_accept\", \"confirm\" }, mode = { \"i\", \"n\" }, expr = true },\n    i_tab = { \"<tab>\", { \"cmp_select_next\", \"cmp\" }, mode = \"i\", expr = true },\n    i_ctrl_w = { \"<c-w>\", \"<c-s-w>\", mode = \"i\", expr = true },\n    i_up = { \"<up>\", { \"hist_up\" }, mode = { \"i\", \"n\" } },\n    i_down = { \"<down>\", { \"hist_down\" }, mode = { \"i\", \"n\" } },\n    q = \"cancel\",\n  },\n}\n```\n\n### `lazygit`\n\n```lua\n{}\n```\n\n### `minimal`\n\n```lua\n{\n  wo = {\n    cursorcolumn = false,\n    cursorline = false,\n    cursorlineopt = \"both\",\n    colorcolumn = \"\",\n    fillchars = \"eob: ,lastline:…\",\n    foldcolumn = \"0\",\n    list = false,\n    listchars = \"extends:…,tab:  \",\n    number = false,\n    relativenumber = false,\n    signcolumn = \"no\",\n    spell = false,\n    winbar = \"\",\n    statuscolumn = \"\",\n    wrap = false,\n    sidescrolloff = 0,\n  },\n}\n```\n\n### `notification`\n\n```lua\n{\n  border = true,\n  zindex = 100,\n  ft = \"markdown\",\n  wo = {\n    winblend = 5,\n    wrap = false,\n    conceallevel = 2,\n    colorcolumn = \"\",\n  },\n  bo = { filetype = \"snacks_notif\" },\n}\n```\n\n### `notification_history`\n\n```lua\n{\n  border = true,\n  zindex = 100,\n  width = 0.6,\n  height = 0.6,\n  minimal = false,\n  title = \" Notification History \",\n  title_pos = \"center\",\n  ft = \"markdown\",\n  bo = { filetype = \"snacks_notif_history\", modifiable = false },\n  wo = { winhighlight = \"Normal:SnacksNotifierHistory\" },\n  keys = { q = \"close\" },\n}\n```\n\n### `scratch`\n\n```lua\n{\n  width = 100,\n  height = 30,\n  bo = { buftype = \"\", buflisted = false, bufhidden = \"hide\", swapfile = false },\n  minimal = false,\n  noautocmd = false,\n  -- position = \"right\",\n  zindex = 20,\n  wo = { winhighlight = \"NormalFloat:Normal\" },\n  footer_keys = true,\n  border = true,\n}\n```\n\n### `snacks_image`\n\n```lua\n{\n  relative = \"cursor\",\n  border = true,\n  focusable = false,\n  backdrop = false,\n  row = 1,\n  col = 1,\n  -- width/height are automatically set by the image size unless specified below\n}\n```\n\n### `split`\n\n```lua\n{\n  position = \"bottom\",\n  height = 0.4,\n  width = 0.4,\n}\n```\n\n### `terminal`\n\n```lua\n{\n  bo = {\n    filetype = \"snacks_terminal\",\n  },\n  wo = {},\n  stack = true, -- when enabled, multiple split windows with the same position will be stacked together (useful for terminals)\n  keys = {\n    q = \"hide\",\n    gf = function(self)\n      local f = vim.fn.findfile(vim.fn.expand(\"<cfile>\"), \"**\")\n      if f == \"\" then\n        Snacks.notify.warn(\"No file under cursor\")\n      else\n        self:hide()\n        vim.schedule(function()\n          vim.cmd(\"e \" .. f)\n        end)\n      end\n    end,\n    term_normal = {\n      \"<esc>\",\n      function(self)\n        self.esc_timer = self.esc_timer or (vim.uv or vim.loop).new_timer()\n        if self.esc_timer:is_active() then\n          self.esc_timer:stop()\n          vim.cmd(\"stopinsert\")\n        else\n          self.esc_timer:start(200, 0, function() end)\n          return \"<esc>\"\n        end\n      end,\n      mode = \"t\",\n      expr = true,\n      desc = \"Double escape to normal mode\",\n    },\n  },\n}\n```\n\n### `zen`\n\n```lua\n{\n  enter = true,\n  fixbuf = false,\n  minimal = false,\n  width = 120,\n  height = 0,\n  backdrop = { transparent = true, blend = 40 },\n  keys = { q = false },\n  zindex = 40,\n  wo = {\n    winhighlight = \"NormalFloat:Normal\",\n  },\n  w = {\n    snacks_main = true,\n  },\n}\n```\n\n### `zoom_indicator`\n\nfullscreen indicator\nonly shown when the window is maximized\n\n```lua\n{\n  text = \"▍ zoom  󰊓  \",\n  minimal = true,\n  enter = false,\n  focusable = false,\n  height = 1,\n  row = 0,\n  col = -1,\n  backdrop = false,\n}\n```\n"
  },
  {
    "path": "docs/terminal.md",
    "content": "# 🍿 terminal\n\nCreate and toggle terminal windows.\n\nBased on the provided options, some defaults will be set:\n\n- if no `cmd` is provided, the window will be opened in a bottom split\n- if `cmd` is provided, the window will be opened in a floating window\n- for splits, a `winbar` will be added with the terminal title\n\n![image](https://github.com/user-attachments/assets/afcc9989-57d7-4518-a390-cc7d6f0cec13)\n\n## 🚀 Usage\n\n### Edgy Integration\n\n```lua\n{\n  \"folke/edgy.nvim\",\n  ---@module 'edgy'\n  ---@param opts Edgy.Config\n  opts = function(_, opts)\n    for _, pos in ipairs({ \"top\", \"bottom\", \"left\", \"right\" }) do\n      opts[pos] = opts[pos] or {}\n      table.insert(opts[pos], {\n        ft = \"snacks_terminal\",\n        size = { height = 0.4 },\n        title = \"%{b:snacks_terminal.id}: %{b:term_title}\",\n        filter = function(_buf, win)\n          return vim.w[win].snacks_win\n            and vim.w[win].snacks_win.position == pos\n            and vim.w[win].snacks_win.relative == \"editor\"\n            and not vim.w[win].trouble_preview\n        end,\n      })\n    end\n  end,\n}\n```\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    terminal = {\n      -- your terminal configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.terminal.Config\n---@field win? snacks.win.Config|{}\n---@field shell? string|string[] The shell to use. Defaults to `vim.o.shell`\n---@field override? fun(cmd?: string|string[], opts?: snacks.terminal.Opts) Use this to use a different terminal implementation\n{\n  win = { style = \"terminal\" },\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `terminal`\n\n```lua\n{\n  bo = {\n    filetype = \"snacks_terminal\",\n  },\n  wo = {},\n  stack = true, -- when enabled, multiple split windows with the same position will be stacked together (useful for terminals)\n  keys = {\n    q = \"hide\",\n    gf = function(self)\n      local f = vim.fn.findfile(vim.fn.expand(\"<cfile>\"), \"**\")\n      if f == \"\" then\n        Snacks.notify.warn(\"No file under cursor\")\n      else\n        self:hide()\n        vim.schedule(function()\n          vim.cmd(\"e \" .. f)\n        end)\n      end\n    end,\n    term_normal = {\n      \"<esc>\",\n      function(self)\n        self.esc_timer = self.esc_timer or (vim.uv or vim.loop).new_timer()\n        if self.esc_timer:is_active() then\n          self.esc_timer:stop()\n          vim.cmd(\"stopinsert\")\n        else\n          self.esc_timer:start(200, 0, function() end)\n          return \"<esc>\"\n        end\n      end,\n      mode = \"t\",\n      expr = true,\n      desc = \"Double escape to normal mode\",\n    },\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.terminal.Opts: snacks.terminal.Config\n---@field cwd? string\n---@field count? integer\n---@field env? table<string, string>\n---@field start_insert? boolean start insert mode when starting the terminal\n---@field auto_insert? boolean start insert mode when entering the terminal buffer\n---@field auto_close? boolean close the terminal buffer when the process exits\n---@field interactive? boolean shortcut for `start_insert`, `auto_close` and `auto_insert` (default: true)\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.terminal: snacks.win\n---@field cmd? string | string[]\n---@field opts snacks.terminal.Opts\nSnacks.terminal = {}\n```\n\n### `Snacks.terminal()`\n\n```lua\n---@type fun(cmd?: string|string[], opts?: snacks.terminal.Opts): snacks.terminal\nSnacks.terminal()\n```\n\n### `Snacks.terminal.colorize()`\n\nColorize the current buffer.\nReplaces ansii color codes with the actual colors.\n\nExample:\n\n```sh\nls -la --color=always | nvim - -c \"lua Snacks.terminal.colorize()\"\n```\n\n```lua\nSnacks.terminal.colorize()\n```\n\n### `Snacks.terminal.focus()`\n\nFocus a terminal window. If already focused, hide it.\nThe terminal id is based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n\n```lua\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nSnacks.terminal.focus(cmd, opts)\n```\n\n### `Snacks.terminal.get()`\n\nGet or create a terminal window.\nThe terminal id is based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n`opts.create` defaults to `true`.\n\n```lua\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts| {create?: boolean}\n---@return snacks.win? terminal, boolean? created\nSnacks.terminal.get(cmd, opts)\n```\n\n### `Snacks.terminal.list()`\n\n```lua\n---@return snacks.win[]\nSnacks.terminal.list()\n```\n\n### `Snacks.terminal.open()`\n\nOpen a new terminal window.\n\n```lua\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nSnacks.terminal.open(cmd, opts)\n```\n\n### `Snacks.terminal.tid()`\n\nGet a terminal id based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n\n```lua\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nSnacks.terminal.tid(cmd, opts)\n```\n\n### `Snacks.terminal.toggle()`\n\nToggle a terminal window.\nThe terminal id is based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n\n```lua\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nSnacks.terminal.toggle(cmd, opts)\n```\n"
  },
  {
    "path": "docs/toggle.md",
    "content": "# 🍿 toggle\n\nToggle keymaps integrated with which-key icons / colors\n\n![image](https://github.com/user-attachments/assets/6d843acd-1ac1-44fd-b318-58b4c17de2d5)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    toggle = {\n      -- your toggle configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.toggle.Config\n---@field icon? string|{ enabled: string, disabled: string }\n---@field color? string|{ enabled: string, disabled: string }\n---@field wk_desc? string|{ enabled: string, disabled: string }\n---@field map? fun(mode: string|string[], lhs: string, rhs: string|fun(), opts?: vim.keymap.set.Opts)\n---@field which_key? boolean\n---@field notify? boolean|fun(state:boolean, opts: snacks.toggle.Opts)\n{\n  map = vim.keymap.set, -- keymap.set function to use\n  which_key = true, -- integrate with which-key to show enabled/disabled icons and colors\n  notify = true, -- show a notification when toggling\n  -- icons for enabled/disabled states\n  icon = {\n    enabled = \" \",\n    disabled = \" \",\n  },\n  -- colors for enabled/disabled states\n  color = {\n    enabled = \"green\",\n    disabled = \"yellow\",\n  },\n  wk_desc = {\n    enabled = \"Disable \",\n    disabled = \"Enable \",\n  },\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.toggle.Opts: snacks.toggle.Config\n---@field id? string\n---@field name string\n---@field get fun():boolean\n---@field set fun(state:boolean)\n```\n\n## 📦 Module\n\n### `Snacks.toggle()`\n\n```lua\n---@type fun(... :snacks.toggle.Opts): snacks.toggle.Class\nSnacks.toggle()\n```\n\n### `Snacks.toggle.animate()`\n\n```lua\nSnacks.toggle.animate()\n```\n\n### `Snacks.toggle.diagnostics()`\n\n```lua\n---@param opts? snacks.toggle.Config\nSnacks.toggle.diagnostics(opts)\n```\n\n### `Snacks.toggle.dim()`\n\n```lua\nSnacks.toggle.dim()\n```\n\n### `Snacks.toggle.get()`\n\n```lua\n---@param id string\n---@return snacks.toggle.Class?\nSnacks.toggle.get(id)\n```\n\n### `Snacks.toggle.indent()`\n\n```lua\nSnacks.toggle.indent()\n```\n\n### `Snacks.toggle.inlay_hints()`\n\n```lua\n---@param opts? snacks.toggle.Config\nSnacks.toggle.inlay_hints(opts)\n```\n\n### `Snacks.toggle.line_number()`\n\n```lua\n---@param opts? snacks.toggle.Config\nSnacks.toggle.line_number(opts)\n```\n\n### `Snacks.toggle.new()`\n\n```lua\n---@param ... snacks.toggle.Opts\nSnacks.toggle.new(...)\n```\n\n### `Snacks.toggle.option()`\n\n```lua\n---@param option string\n---@param opts? snacks.toggle.Config | {on?: unknown, off?: unknown, global?: boolean}\nSnacks.toggle.option(option, opts)\n```\n\n### `Snacks.toggle.profiler()`\n\n```lua\nSnacks.toggle.profiler()\n```\n\n### `Snacks.toggle.profiler_highlights()`\n\n```lua\nSnacks.toggle.profiler_highlights()\n```\n\n### `Snacks.toggle.scroll()`\n\n```lua\nSnacks.toggle.scroll()\n```\n\n### `Snacks.toggle.treesitter()`\n\n```lua\n---@param opts? snacks.toggle.Config\nSnacks.toggle.treesitter(opts)\n```\n\n### `Snacks.toggle.words()`\n\n```lua\nSnacks.toggle.words()\n```\n\n### `Snacks.toggle.zen()`\n\n```lua\nSnacks.toggle.zen()\n```\n\n### `Snacks.toggle.zoom()`\n\n```lua\nSnacks.toggle.zoom()\n```\n"
  },
  {
    "path": "docs/util.md",
    "content": "# 🍿 util\n\n<!-- docgen -->\n\n## 📚 Types\n\n```lua\n---@alias snacks.util.hl table<string, string|vim.api.keyset.highlight>\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.util\n---@field spawn snacks.spawn\n---@field lsp snacks.lsp\nSnacks.util = {}\n```\n\n### `Snacks.util.blend()`\n\n```lua\n---@param fg string foreground color\n---@param bg string background color\n---@param alpha number number between 0 and 1. 0 results in bg, 1 results in fg\nSnacks.util.blend(fg, bg, alpha)\n```\n\n### `Snacks.util.bo()`\n\nSet buffer-local options.\n\n```lua\n---@param buf number\n---@param bo vim.bo|{}\nSnacks.util.bo(buf, bo)\n```\n\n### `Snacks.util.color()`\n\n```lua\n---@param group string|string[] hl group to get color from\n---@param prop? string property to get. Defaults to \"fg\"\nSnacks.util.color(group, prop)\n```\n\n### `Snacks.util.debounce()`\n\n```lua\n---@generic T\n---@param fn T\n---@param opts? {ms?:number}\n---@return T\nSnacks.util.debounce(fn, opts)\n```\n\n### `Snacks.util.file_decode()`\n\nDecodes a file name to a string.\n\n```lua\n---@param str string\nSnacks.util.file_decode(str)\n```\n\n### `Snacks.util.file_encode()`\n\nEncodes a string to be used as a file name.\n\n```lua\n---@param str string\nSnacks.util.file_encode(str)\n```\n\n### `Snacks.util.get_lang()`\n\n```lua\n---@param lang string|number|nil\n---@overload fun(buf:number):string?\n---@overload fun(ft:string):string?\n---@return string?\nSnacks.util.get_lang(lang)\n```\n\n### `Snacks.util.icon()`\n\nGet an icon from `mini.icons` or `nvim-web-devicons`.\n\n```lua\n---@param name string\n---@param cat? string \"file\"|\"filetype\"|\"extension\"|\"directory\"\n---@param opts? { fallback?: {dir?:string, file?:string} }\n---@return string, string?\nSnacks.util.icon(name, cat, opts)\n```\n\n### `Snacks.util.is_float()`\n\n```lua\n---@param win? number\nSnacks.util.is_float(win)\n```\n\n### `Snacks.util.is_transparent()`\n\nCheck if the colorscheme is transparent.\n\n```lua\nSnacks.util.is_transparent()\n```\n\n### `Snacks.util.keycode()`\n\n```lua\n---@param str string\nSnacks.util.keycode(str)\n```\n\n### `Snacks.util.normkey()`\n\n```lua\n---@param key string\nSnacks.util.normkey(key)\n```\n\n### `Snacks.util.on_key()`\n\n```lua\n---@param key string\n---@param cb fun(key:string)\nSnacks.util.on_key(key, cb)\n```\n\n### `Snacks.util.on_module()`\n\nCall a function when a module is loaded.\nThe callback is called immediately if the module is already loaded.\nOtherwise, it is called when the module is loaded.\n\n```lua\n---@param modname string\n---@param cb fun(modname:string)\nSnacks.util.on_module(modname, cb)\n```\n\n### `Snacks.util.parse()`\n\nParse async when available.\n\n```lua\n---@param parser vim.treesitter.LanguageTree\n---@param range boolean|Range|nil: Parse this range in the parser's source.\n---@param on_parse fun(err?: string, trees?: table<integer, TSTree>) Function invoked when parsing completes.\nSnacks.util.parse(parser, range, on_parse)\n```\n\n### `Snacks.util.path_type()`\n\nBetter validation to check if path is a dir or a file\n\n```lua\n---@param path string\n---@return \"directory\"|\"file\"\nSnacks.util.path_type(path)\n```\n\n### `Snacks.util.redraw()`\n\nRedraw the window.\nOptimized for Neovim >= 0.10\n\n```lua\n---@param win number\nSnacks.util.redraw(win)\n```\n\n### `Snacks.util.redraw_range()`\n\nRedraw the range of lines in the window.\nOptimized for Neovim >= 0.10\n\n```lua\n---@param win number\n---@param from number -- 1-indexed, inclusive\n---@param to number -- 1-indexed, inclusive\nSnacks.util.redraw_range(win, from, to)\n```\n\n### `Snacks.util.ref()`\n\n```lua\n---@generic T\n---@param t T\n---@return { value?:T }|fun():T?\nSnacks.util.ref(t)\n```\n\n### `Snacks.util.set_hl()`\n\nEnsures the hl groups are always set, even after a colorscheme change.\n\n```lua\n---@param groups snacks.util.hl\n---@param opts? { prefix?:string, default?:boolean, managed?:boolean }\nSnacks.util.set_hl(groups, opts)\n```\n\n### `Snacks.util.spinner()`\n\n```lua\nSnacks.util.spinner()\n```\n\n### `Snacks.util.stop()`\n\n```lua\n---@param handle? uv.uv_handle_t|uv.uv_timer_t\nSnacks.util.stop(handle)\n```\n\n### `Snacks.util.throttle()`\n\n```lua\n---@generic T\n---@param fn T\n---@param opts? {ms?:number}\n---@return T\nSnacks.util.throttle(fn, opts)\n```\n\n### `Snacks.util.var()`\n\nGet a buffer or global variable.\n\n```lua\n---@generic T\n---@param buf? number\n---@param name string\n---@param default? T\n---@return T\nSnacks.util.var(buf, name, default)\n```\n\n### `Snacks.util.winhl()`\n\nMerges vim.wo.winhighlight options.\nOption values can be a string or a dictionary.\n\n```lua\n---@param ... string|table<string, string>\nSnacks.util.winhl(...)\n```\n\n### `Snacks.util.wo()`\n\nSet window-local options.\n\n```lua\n---@param win number\n---@param wo vim.wo|{}|{winhighlight: string|table<string, string>}\nSnacks.util.wo(win, wo)\n```\n"
  },
  {
    "path": "docs/win.md",
    "content": "# 🍿 win\n\nEasily create and manage floating windows or splits\n\n## 🚀 Usage\n\n```lua\nSnacks.win({\n  file = vim.api.nvim_get_runtime_file(\"doc/news.txt\", false)[1],\n  width = 0.6,\n  height = 0.6,\n  wo = {\n    spell = false,\n    wrap = false,\n    signcolumn = \"yes\",\n    statuscolumn = \" \",\n    conceallevel = 3,\n  },\n})\n```\n![image](https://github.com/user-attachments/assets/250acfbd-a624-4f42-a36b-9aab316ebf64)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    win = {\n      -- your win configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.win.Config: vim.api.keyset.win_config\n---@field style? string merges with config from `Snacks.config.styles[style]`\n---@field show? boolean Show the window immediately (default: true)\n---@field footer_keys? boolean|string[] Show keys footer. When string[], only show those keys with lhs (default: false)\n---@field height? number|fun(self:snacks.win):number Height of the window. Use <1 for relative height. 0 means full height. (default: 0.9)\n---@field width? number|fun(self:snacks.win):number Width of the window. Use <1 for relative width. 0 means full width. (default: 0.9)\n---@field min_height? number Minimum height of the window\n---@field max_height? number Maximum height of the window\n---@field min_width? number Minimum width of the window\n---@field max_width? number Maximum width of the window\n---@field col? number|fun(self:snacks.win):number Column of the window. Use <1 for relative column. (default: center)\n---@field row? number|fun(self:snacks.win):number Row of the window. Use <1 for relative row. (default: center)\n---@field minimal? boolean Disable a bunch of options to make the window minimal (default: true)\n---@field position? \"float\"|\"bottom\"|\"top\"|\"left\"|\"right\"|\"current\"\n---@field border? \"none\"|\"top\"|\"right\"|\"bottom\"|\"left\"|\"top_bottom\"|\"hpad\"|\"vpad\"|\"rounded\"|\"single\"|\"double\"|\"solid\"|\"shadow\"|\"bold\"|string[]|false|true\n---@field buf? number If set, use this buffer instead of creating a new one\n---@field file? string If set, use this file instead of creating a new buffer\n---@field enter? boolean Enter the window after opening (default: false)\n---@field backdrop? number|false|snacks.win.Backdrop Opacity of the backdrop (default: 60)\n---@field wo? vim.wo|{} window options\n---@field bo? vim.bo|{} buffer options\n---@field b? table<string, any> buffer local variables\n---@field w? table<string, any> window local variables\n---@field ft? string filetype to use for treesitter/syntax highlighting. Won't override existing filetype\n---@field scratch_ft? string filetype to use for scratch buffers\n---@field keys? table<string, false|string|fun(self: snacks.win)|snacks.win.Keys> Key mappings\n---@field on_buf? fun(self: snacks.win) Callback after opening the buffer\n---@field on_win? fun(self: snacks.win) Callback after opening the window\n---@field on_close? fun(self: snacks.win) Callback after closing the window\n---@field fixbuf? boolean don't allow other buffers to be opened in this window\n---@field text? string|string[]|fun():(string[]|string) Initial lines to set in the buffer\n---@field actions? table<string, snacks.win.Action.spec> Actions that can be used in key mappings\n---@field resize? boolean Automatically resize the window when the editor is resized\n---@field stack? boolean When enabled, multiple split windows with the same position will be stacked together (useful for terminals)\n{\n  show = true,\n  fixbuf = true,\n  relative = \"editor\",\n  position = \"float\",\n  minimal = true,\n  wo = {\n    winhighlight = \"Normal:SnacksNormal,NormalNC:SnacksNormalNC,WinBar:SnacksWinBar,WinBarNC:SnacksWinBarNC,FloatTitle:SnacksTitle,FloatFooter:SnacksFooter,WinSeparator:SnacksWinSeparator\",\n  },\n  bo = {},\n  title_pos = \"center\",\n  keys = {\n    q = \"close\",\n  },\n  footer_pos = \"center\",\n  footer_keys = false,\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `float`\n\n```lua\n{\n  position = \"float\",\n  backdrop = 60,\n  height = 0.9,\n  width = 0.9,\n  zindex = 50,\n}\n```\n\n### `help`\n\n```lua\n{\n  position = \"float\",\n  backdrop = false,\n  border = \"top\",\n  row = -1,\n  width = 0,\n  height = 0.3,\n}\n```\n\n### `minimal`\n\n```lua\n{\n  wo = {\n    cursorcolumn = false,\n    cursorline = false,\n    cursorlineopt = \"both\",\n    colorcolumn = \"\",\n    fillchars = \"eob: ,lastline:…\",\n    foldcolumn = \"0\",\n    list = false,\n    listchars = \"extends:…,tab:  \",\n    number = false,\n    relativenumber = false,\n    signcolumn = \"no\",\n    spell = false,\n    winbar = \"\",\n    statuscolumn = \"\",\n    wrap = false,\n    sidescrolloff = 0,\n  },\n}\n```\n\n### `split`\n\n```lua\n{\n  position = \"bottom\",\n  height = 0.4,\n  width = 0.4,\n}\n```\n\n## 📚 Types\n\n```lua\n---@class snacks.win.Keys: vim.api.keyset.keymap\n---@field [1]? string\n---@field [2]? string|string[]|fun(self: snacks.win): string?\n---@field mode? string|string[]\n```\n\n```lua\n---@class snacks.win.Event: vim.api.keyset.create_autocmd\n---@field buf? true\n---@field win? true\n---@field callback? fun(self: snacks.win, ev:vim.api.keyset.create_autocmd.callback_args):boolean?\n```\n\n```lua\n---@class snacks.win.Backdrop\n---@field bg? string\n---@field blend? number\n---@field transparent? boolean defaults to true\n---@field win? snacks.win.Config overrides the backdrop window config\n```\n\n```lua\n---@class snacks.win.Dim\n---@field width number width of the window, without borders\n---@field height number height of the window, without borders\n---@field row number row of the window (0-indexed)\n---@field col number column of the window (0-indexed)\n---@field border? boolean whether the window has a border\n```\n\n```lua\n---@alias snacks.win.Action.fn fun(self: snacks.win):(boolean|string?)\n---@alias snacks.win.Action.spec snacks.win.Action|snacks.win.Action.fn\n---@class snacks.win.Action\n---@field action snacks.win.Action.fn\n---@field desc? string\n```\n\n## 📦 Module\n\n```lua\n---@class snacks.win\n---@field id number\n---@field buf? number\n---@field scratch_buf? number\n---@field win? number\n---@field opts snacks.win.Config\n---@field augroup? number\n---@field backdrop? snacks.win\n---@field keys snacks.win.Keys[]\n---@field events (snacks.win.Event|{event:string|string[]})[]\n---@field meta table<string, any>\n---@field closed? boolean\nSnacks.win = {}\n```\n\n### `Snacks.win()`\n\n```lua\n---@type fun(opts? :snacks.win.Config|{}): snacks.win\nSnacks.win()\n```\n\n### `Snacks.win.is_border()`\n\n```lua\nSnacks.win.is_border(border)\n```\n\n### `Snacks.win.new()`\n\n```lua\n---@param opts? snacks.win.Config|{}\n---@return snacks.win\nSnacks.win.new(opts)\n```\n\n### `Snacks.win.zindex()`\n\nCalculate the next available zindex for snacks windows.\nNew windows open on top of existing ones.\n\n```lua\n---@param opts? { zindex?: number, tab?: number|boolean, all?: boolean, max?: number }\n---@overload fun(zindex: number): number\nSnacks.win.zindex(opts)\n```\n\n### `win:action()`\n\n```lua\n---@param actions string|string[]\n---@return (fun(): boolean|string?) action, string? desc\nwin:action(actions)\n```\n\n### `win:add_padding()`\n\n```lua\nwin:add_padding()\n```\n\n### `win:border()`\n\n```lua\nwin:border()\n```\n\n### `win:border_size()`\n\nCalculate the size of the border\n\n```lua\nwin:border_size()\n```\n\n### `win:border_text_width()`\n\n```lua\nwin:border_text_width()\n```\n\n### `win:buf_valid()`\n\n```lua\nwin:buf_valid()\n```\n\n### `win:close()`\n\n```lua\n---@param opts? { buf: boolean }\nwin:close(opts)\n```\n\n### `win:destroy()`\n\n```lua\nwin:destroy()\n```\n\n### `win:dim()`\n\n```lua\n---@param parent? snacks.win.Dim\nwin:dim(parent)\n```\n\n### `win:execute()`\n\n```lua\n---@param actions string|string[]\nwin:execute(actions)\n```\n\n### `win:fixbuf()`\n\n```lua\nwin:fixbuf()\n```\n\n### `win:focus()`\n\n```lua\nwin:focus()\n```\n\n### `win:has_border()`\n\n```lua\nwin:has_border()\n```\n\n### `win:hide()`\n\n```lua\nwin:hide()\n```\n\n### `win:hscroll()`\n\n```lua\n---@param left? boolean\nwin:hscroll(left)\n```\n\n### `win:is_floating()`\n\n```lua\nwin:is_floating()\n```\n\n### `win:line()`\n\n```lua\nwin:line(line)\n```\n\n### `win:lines()`\n\n```lua\n---@param from? number 1-indexed, inclusive\n---@param to? number 1-indexed, inclusive\nwin:lines(from, to)\n```\n\n### `win:map()`\n\n```lua\nwin:map()\n```\n\n### `win:on()`\n\n```lua\n---@param event string|string[]\n---@param cb fun(self: snacks.win, ev:vim.api.keyset.create_autocmd.callback_args):boolean?\n---@param opts? snacks.win.Event\nwin:on(event, cb, opts)\n```\n\n### `win:on_current_tab()`\n\n```lua\nwin:on_current_tab()\n```\n\n### `win:on_resize()`\n\n```lua\nwin:on_resize()\n```\n\n### `win:parent_size()`\n\n```lua\n---@return { height: number, width: number }\nwin:parent_size()\n```\n\n### `win:redraw()`\n\n```lua\nwin:redraw()\n```\n\n### `win:scratch()`\n\n```lua\nwin:scratch()\n```\n\n### `win:scroll()`\n\n```lua\n---@param up? boolean\nwin:scroll(up)\n```\n\n### `win:set_buf()`\n\n```lua\n---@param buf number\nwin:set_buf(buf)\n```\n\n### `win:set_title()`\n\n```lua\n---@param title string|{[1]:string, [2]:string}[]\n---@param pos? \"center\"|\"left\"|\"right\"\nwin:set_title(title, pos)\n```\n\n### `win:show()`\n\n```lua\nwin:show()\n```\n\n### `win:size()`\n\n```lua\n---@return { height: number, width: number }\nwin:size()\n```\n\n### `win:text()`\n\n```lua\n---@param from? number 1-indexed, inclusive\n---@param to? number 1-indexed, inclusive\nwin:text(from, to)\n```\n\n### `win:toggle()`\n\n```lua\nwin:toggle()\n```\n\n### `win:toggle_help()`\n\n```lua\n---@param opts? {col_width?: number, key_width?: number, win?: snacks.win.Config}\nwin:toggle_help(opts)\n```\n\n### `win:update()`\n\n```lua\nwin:update()\n```\n\n### `win:valid()`\n\n```lua\nwin:valid()\n```\n\n### `win:win_valid()`\n\n```lua\nwin:win_valid()\n```\n"
  },
  {
    "path": "docs/words.md",
    "content": "# 🍿 words\n\nAuto-show LSP references and quickly navigate between them\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    words = {\n      -- your words configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.words.Config\n---@field enabled? boolean\n{\n  debounce = 200, -- time in ms to wait before updating\n  notify_jump = false, -- show a notification when jumping\n  notify_end = true, -- show a notification when reaching the end\n  foldopen = true, -- open folds after jumping\n  jumplist = true, -- set jump point before jumping\n  modes = { \"n\", \"i\", \"c\" }, -- modes to show references\n  filter = function(buf) -- what buffers to enable `snacks.words`\n    return vim.g.snacks_words ~= false and vim.b[buf].snacks_words ~= false\n  end,\n}\n```\n\n## 📦 Module\n\n### `Snacks.words.clear()`\n\n```lua\nSnacks.words.clear()\n```\n\n### `Snacks.words.disable()`\n\n```lua\nSnacks.words.disable()\n```\n\n### `Snacks.words.enable()`\n\n```lua\nSnacks.words.enable()\n```\n\n### `Snacks.words.is_enabled()`\n\n```lua\n---@param opts? number|{buf?:number, modes:boolean} if modes is true, also check if the current mode is enabled\nSnacks.words.is_enabled(opts)\n```\n\n### `Snacks.words.jump()`\n\n```lua\n---@param count? number\n---@param cycle? boolean\nSnacks.words.jump(count, cycle)\n```\n"
  },
  {
    "path": "docs/zen.md",
    "content": "# 🍿 zen\n\nZen mode • distraction-free coding.\nIntegrates with `Snacks.toggle` to toggle various UI elements\nand with `Snacks.dim` to dim code out of scope.\n\nSimilar plugins:\n\n- [zen-mode.nvim](https://github.com/folke/zen-mode.nvim)\n- [true-zen.nvim](https://github.com/pocco81/true-zen.nvim)\n\n![image](https://github.com/user-attachments/assets/77c607ec-c354-4e17-bcd1-fdcd4b4c0057)\n\n<!-- docgen -->\n\n## 📦 Setup\n\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    zen = {\n      -- your zen configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n\n## ⚙️ Config\n\n```lua\n---@class snacks.zen.Config\n{\n  -- You can add any `Snacks.toggle` id here.\n  -- Toggle state is restored when the window is closed.\n  -- Toggle config options are NOT merged.\n  ---@type table<string, boolean>\n  toggles = {\n    dim = true,\n    git_signs = false,\n    mini_diff_signs = false,\n    -- diagnostics = false,\n    -- inlay_hints = false,\n  },\n  center = true, -- center the window\n  show = {\n    statusline = false, -- can only be shown when using the global statusline\n    tabline = false,\n  },\n  ---@type snacks.win.Config\n  win = { style = \"zen\" },\n  --- Callback when the window is opened.\n  ---@param win snacks.win\n  on_open = function(win) end,\n  --- Callback when the window is closed.\n  ---@param win snacks.win\n  on_close = function(win) end,\n  --- Options for the `Snacks.zen.zoom()`\n  ---@type snacks.zen.Config\n  zoom = {\n    toggles = {},\n    center = false,\n    show = { statusline = true, tabline = true },\n    win = {\n      backdrop = false,\n      width = 0, -- full width\n    },\n  },\n}\n```\n\n## 🎨 Styles\n\nCheck the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n\n### `zen`\n\n```lua\n{\n  enter = true,\n  fixbuf = false,\n  minimal = false,\n  width = 120,\n  height = 0,\n  backdrop = { transparent = true, blend = 40 },\n  keys = { q = false },\n  zindex = 40,\n  wo = {\n    winhighlight = \"NormalFloat:Normal\",\n  },\n  w = {\n    snacks_main = true,\n  },\n}\n```\n\n### `zoom_indicator`\n\nfullscreen indicator\nonly shown when the window is maximized\n\n```lua\n{\n  text = \"▍ zoom  󰊓  \",\n  minimal = true,\n  enter = false,\n  focusable = false,\n  height = 1,\n  row = 0,\n  col = -1,\n  backdrop = false,\n}\n```\n\n## 📦 Module\n\n### `Snacks.zen()`\n\n```lua\n---@type fun(opts: snacks.zen.Config): snacks.win\nSnacks.zen()\n```\n\n### `Snacks.zen.zen()`\n\n```lua\n---@param opts? snacks.zen.Config\nSnacks.zen.zen(opts)\n```\n\n### `Snacks.zen.zoom()`\n\n```lua\n---@param opts? snacks.zen.Config\nSnacks.zen.zoom(opts)\n```\n"
  },
  {
    "path": "lua/snacks/animate/easing.lua",
    "content": "-- https://github.com/EmmanuelOga/easing/blob/master/lib/easing.lua\n--\n-- Adapted from\n-- Tweener's easing functions (Penner's Easing Equations)\n-- and http://code.google.com/p/tweener/ (jstweener javascript version)\n--\n\n--[[\nDisclaimer for Robert Penner's Easing Equations license:\n\nTERMS OF USE - EASING EQUATIONS\n\nOpen source under the BSD License.\n\nCopyright © 2001 Robert Penner\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n    * Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n]]\n\n-- For all easing functions:\n-- t = elapsed time\n-- b = begin\n-- c = change == ending - beginning\n-- d = duration (total time)\n\nlocal pow = math.pow\nlocal sin = math.sin\nlocal cos = math.cos\nlocal pi = math.pi\nlocal sqrt = math.sqrt\nlocal abs = math.abs\nlocal asin = math.asin\n-- Easing functions for animations.\n\nlocal function linear(t, b, c, d)\n  return c * t / d + b\nend\n\nlocal function inQuad(t, b, c, d)\n  t = t / d\n  return c * pow(t, 2) + b\nend\n\nlocal function outQuad(t, b, c, d)\n  t = t / d\n  return -c * t * (t - 2) + b\nend\n\nlocal function inOutQuad(t, b, c, d)\n  t = t / d * 2\n  if t < 1 then\n    return c / 2 * pow(t, 2) + b\n  else\n    return -c / 2 * ((t - 1) * (t - 3) - 1) + b\n  end\nend\n\nlocal function outInQuad(t, b, c, d)\n  if t < d / 2 then\n    return outQuad(t * 2, b, c / 2, d)\n  else\n    return inQuad((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\nlocal function inCubic(t, b, c, d)\n  t = t / d\n  return c * pow(t, 3) + b\nend\n\nlocal function outCubic(t, b, c, d)\n  t = t / d - 1\n  return c * (pow(t, 3) + 1) + b\nend\n\nlocal function inOutCubic(t, b, c, d)\n  t = t / d * 2\n  if t < 1 then\n    return c / 2 * t * t * t + b\n  else\n    t = t - 2\n    return c / 2 * (t * t * t + 2) + b\n  end\nend\n\nlocal function outInCubic(t, b, c, d)\n  if t < d / 2 then\n    return outCubic(t * 2, b, c / 2, d)\n  else\n    return inCubic((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\nlocal function inQuart(t, b, c, d)\n  t = t / d\n  return c * pow(t, 4) + b\nend\n\nlocal function outQuart(t, b, c, d)\n  t = t / d - 1\n  return -c * (pow(t, 4) - 1) + b\nend\n\nlocal function inOutQuart(t, b, c, d)\n  t = t / d * 2\n  if t < 1 then\n    return c / 2 * pow(t, 4) + b\n  else\n    t = t - 2\n    return -c / 2 * (pow(t, 4) - 2) + b\n  end\nend\n\nlocal function outInQuart(t, b, c, d)\n  if t < d / 2 then\n    return outQuart(t * 2, b, c / 2, d)\n  else\n    return inQuart((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\nlocal function inQuint(t, b, c, d)\n  t = t / d\n  return c * pow(t, 5) + b\nend\n\nlocal function outQuint(t, b, c, d)\n  t = t / d - 1\n  return c * (pow(t, 5) + 1) + b\nend\n\nlocal function inOutQuint(t, b, c, d)\n  t = t / d * 2\n  if t < 1 then\n    return c / 2 * pow(t, 5) + b\n  else\n    t = t - 2\n    return c / 2 * (pow(t, 5) + 2) + b\n  end\nend\n\nlocal function outInQuint(t, b, c, d)\n  if t < d / 2 then\n    return outQuint(t * 2, b, c / 2, d)\n  else\n    return inQuint((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\nlocal function inSine(t, b, c, d)\n  return -c * cos(t / d * (pi / 2)) + c + b\nend\n\nlocal function outSine(t, b, c, d)\n  return c * sin(t / d * (pi / 2)) + b\nend\n\nlocal function inOutSine(t, b, c, d)\n  return -c / 2 * (cos(pi * t / d) - 1) + b\nend\n\nlocal function outInSine(t, b, c, d)\n  if t < d / 2 then\n    return outSine(t * 2, b, c / 2, d)\n  else\n    return inSine((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\nlocal function inExpo(t, b, c, d)\n  if t == 0 then\n    return b\n  else\n    return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001\n  end\nend\n\nlocal function outExpo(t, b, c, d)\n  if t == d then\n    return b + c\n  else\n    return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b\n  end\nend\n\nlocal function inOutExpo(t, b, c, d)\n  if t == 0 then\n    return b\n  end\n  if t == d then\n    return b + c\n  end\n  t = t / d * 2\n  if t < 1 then\n    return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005\n  else\n    t = t - 1\n    return c / 2 * 1.0005 * (-pow(2, -10 * t) + 2) + b\n  end\nend\n\nlocal function outInExpo(t, b, c, d)\n  if t < d / 2 then\n    return outExpo(t * 2, b, c / 2, d)\n  else\n    return inExpo((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\nlocal function inCirc(t, b, c, d)\n  t = t / d\n  return (-c * (sqrt(1 - pow(t, 2)) - 1) + b)\nend\n\nlocal function outCirc(t, b, c, d)\n  t = t / d - 1\n  return (c * sqrt(1 - pow(t, 2)) + b)\nend\n\nlocal function inOutCirc(t, b, c, d)\n  t = t / d * 2\n  if t < 1 then\n    return -c / 2 * (sqrt(1 - t * t) - 1) + b\n  else\n    t = t - 2\n    return c / 2 * (sqrt(1 - t * t) + 1) + b\n  end\nend\n\nlocal function outInCirc(t, b, c, d)\n  if t < d / 2 then\n    return outCirc(t * 2, b, c / 2, d)\n  else\n    return inCirc((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\nlocal function inElastic(t, b, c, d, a, p)\n  if t == 0 then\n    return b\n  end\n\n  t = t / d\n\n  if t == 1 then\n    return b + c\n  end\n\n  if not p then\n    p = d * 0.3\n  end\n\n  local s\n\n  if not a or a < abs(c) then\n    a = c\n    s = p / 4\n  else\n    s = p / (2 * pi) * asin(c / a)\n  end\n\n  t = t - 1\n\n  return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b\nend\n\n-- a: amplitud\n-- p: period\nlocal function outElastic(t, b, c, d, a, p)\n  if t == 0 then\n    return b\n  end\n\n  t = t / d\n\n  if t == 1 then\n    return b + c\n  end\n\n  if not p then\n    p = d * 0.3\n  end\n\n  local s\n\n  if not a or a < abs(c) then\n    a = c\n    s = p / 4\n  else\n    s = p / (2 * pi) * asin(c / a)\n  end\n\n  return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b\nend\n\n-- p = period\n-- a = amplitud\nlocal function inOutElastic(t, b, c, d, a, p)\n  if t == 0 then\n    return b\n  end\n\n  t = t / d * 2\n\n  if t == 2 then\n    return b + c\n  end\n\n  if not p then\n    p = d * (0.3 * 1.5)\n  end\n  if not a then\n    a = 0\n  end\n\n  local s\n\n  if not a or a < abs(c) then\n    a = c\n    s = p / 4\n  else\n    s = p / (2 * pi) * asin(c / a)\n  end\n\n  if t < 1 then\n    t = t - 1\n    return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b\n  else\n    t = t - 1\n    return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) * 0.5 + c + b\n  end\nend\n\n-- a: amplitud\n-- p: period\nlocal function outInElastic(t, b, c, d, a, p)\n  if t < d / 2 then\n    return outElastic(t * 2, b, c / 2, d, a, p)\n  else\n    return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)\n  end\nend\n\nlocal function inBack(t, b, c, d, s)\n  if not s then\n    s = 1.70158\n  end\n  t = t / d\n  return c * t * t * ((s + 1) * t - s) + b\nend\n\nlocal function outBack(t, b, c, d, s)\n  if not s then\n    s = 1.70158\n  end\n  t = t / d - 1\n  return c * (t * t * ((s + 1) * t + s) + 1) + b\nend\n\nlocal function inOutBack(t, b, c, d, s)\n  if not s then\n    s = 1.70158\n  end\n  s = s * 1.525\n  t = t / d * 2\n  if t < 1 then\n    return c / 2 * (t * t * ((s + 1) * t - s)) + b\n  else\n    t = t - 2\n    return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b\n  end\nend\n\nlocal function outInBack(t, b, c, d, s)\n  if t < d / 2 then\n    return outBack(t * 2, b, c / 2, d, s)\n  else\n    return inBack((t * 2) - d, b + c / 2, c / 2, d, s)\n  end\nend\n\nlocal function outBounce(t, b, c, d)\n  t = t / d\n  if t < 1 / 2.75 then\n    return c * (7.5625 * t * t) + b\n  elseif t < 2 / 2.75 then\n    t = t - (1.5 / 2.75)\n    return c * (7.5625 * t * t + 0.75) + b\n  elseif t < 2.5 / 2.75 then\n    t = t - (2.25 / 2.75)\n    return c * (7.5625 * t * t + 0.9375) + b\n  else\n    t = t - (2.625 / 2.75)\n    return c * (7.5625 * t * t + 0.984375) + b\n  end\nend\n\nlocal function inBounce(t, b, c, d)\n  return c - outBounce(d - t, 0, c, d) + b\nend\n\nlocal function inOutBounce(t, b, c, d)\n  if t < d / 2 then\n    return inBounce(t * 2, 0, c, d) * 0.5 + b\n  else\n    return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b\n  end\nend\n\nlocal function outInBounce(t, b, c, d)\n  if t < d / 2 then\n    return outBounce(t * 2, b, c / 2, d)\n  else\n    return inBounce((t * 2) - d, b + c / 2, c / 2, d)\n  end\nend\n\n---@enum (key) snacks.animate.easing\nlocal M = {\n  linear = linear,\n  inQuad = inQuad,\n  outQuad = outQuad,\n  inOutQuad = inOutQuad,\n  outInQuad = outInQuad,\n  inCubic = inCubic,\n  outCubic = outCubic,\n  inOutCubic = inOutCubic,\n  outInCubic = outInCubic,\n  inQuart = inQuart,\n  outQuart = outQuart,\n  inOutQuart = inOutQuart,\n  outInQuart = outInQuart,\n  inQuint = inQuint,\n  outQuint = outQuint,\n  inOutQuint = inOutQuint,\n  outInQuint = outInQuint,\n  inSine = inSine,\n  outSine = outSine,\n  inOutSine = inOutSine,\n  outInSine = outInSine,\n  inExpo = inExpo,\n  outExpo = outExpo,\n  inOutExpo = inOutExpo,\n  outInExpo = outInExpo,\n  inCirc = inCirc,\n  outCirc = outCirc,\n  inOutCirc = inOutCirc,\n  outInCirc = outInCirc,\n  inElastic = inElastic,\n  outElastic = outElastic,\n  inOutElastic = inOutElastic,\n  outInElastic = outInElastic,\n  inBack = inBack,\n  outBack = outBack,\n  inOutBack = inOutBack,\n  outInBack = outInBack,\n  inBounce = inBounce,\n  outBounce = outBounce,\n  inOutBounce = inOutBounce,\n  outInBounce = outInBounce,\n}\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/animate/init.lua",
    "content": "---@class snacks.animate\n---@overload fun(from: number, to: number, cb: snacks.animate.cb, opts?: snacks.animate.Opts): snacks.animate.Animation\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.add(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Efficient animations including over 45 easing functions _(library)_\",\n}\n\n-- All easing functions take these parameters:\n--\n-- * `t` _(time)_: should go from 0 to duration\n-- * `b` _(begin)_: starting value of the property\n-- * `c` _(change)_: ending value of the property - starting value\n-- * `d` _(duration)_: total duration of the animation\n--\n-- Some functions allow additional modifiers, like the elastic functions\n-- which also can receive an amplitud and a period parameters (defaults\n-- are included)\n---@alias snacks.animate.easing.Fn fun(t: number, b: number, c: number, d: number): number\n\n--- Duration can be specified as the total duration or the duration per step.\n--- When both are specified, the minimum of both is used.\n---@class snacks.animate.Duration\n---@field step? number duration per step in ms\n---@field total? number total duration in ms\n\n---@class snacks.animate.Config\n---@field easing? snacks.animate.easing|snacks.animate.easing.Fn\nlocal defaults = {\n  ---@type snacks.animate.Duration|number\n  duration = 20, -- ms per step\n  easing = \"linear\",\n  fps = 120, -- frames per second. Global setting for all animations\n}\n\n---@class snacks.animate.Opts: snacks.animate.Config\n---@field buf? number optional buffer to check if animations should be enabled\n---@field int? boolean interpolate the value to an integer\n---@field id? number|string unique identifier for the animation\n\n---@class snacks.animate.ctx\n---@field anim snacks.animate.Animation\n---@field prev number\n---@field done boolean\n\n---@alias snacks.animate.cb fun(value:number, ctx: snacks.animate.ctx)\n\nlocal uv = vim.uv or vim.loop\nlocal _id = 0\n\nlocal function next_id()\n  _id = _id + 1\n  return _id\nend\n\n---@type table<number|string, snacks.animate.Animation>\nlocal active = setmetatable({}, { __mode = \"v\" })\n\n---@class snacks.animate.Animation\n---@field id number|string unique identifier\n---@field opts snacks.animate.Opts\n---@field easing snacks.animate.easing.Fn\n---@field timer? uv.uv_timer_t\n---@field steps? number[]\n---@field _step? number\nlocal Animation = {}\nAnimation.__index = Animation\n\n---@param opts? snacks.animate.Opts\nfunction Animation.new(opts)\n  local id = opts and opts.id or next_id()\n\n  if active[id] then\n    active[id]:stop()\n    active[id] = nil\n  end\n\n  local self = setmetatable({}, Animation)\n  self.id = id\n  self.opts = Snacks.config.get(\"animate\", defaults, opts) --[[@as snacks.animate.Opts]]\n\n  -- resolve easing function\n  local easing = self.opts.easing or \"linear\"\n  -- easing = easing == \"linear\" and self.opts.int and \"linear_int\" or easing\n  easing = type(easing) == \"string\" and require(\"snacks.animate.easing\")[easing] or easing\n  ---@cast easing snacks.animate.easing.Fn\n  self.easing = easing\n  active[self.id] = self\n\n  return self\nend\n\n---@param from number\n---@param to number\n---@param cb snacks.animate.cb\nfunction Animation:start(from, to, cb)\n  self:stop()\n  if from == to then\n    cb(from, { anim = self, prev = from, done = true })\n    return self\n  end\n\n  -- calculate duration\n  local d = type(self.opts.duration) == \"table\" and self.opts.duration or { step = self.opts.duration }\n  ---@cast d snacks.animate.Duration\n  local duration = 0\n  if d.step then\n    duration = d.step * math.abs(to - from)\n    duration = math.min(duration, d.total or duration)\n  elseif d.total then\n    duration = d.total\n  end\n  duration = duration or 250\n  local step_duration = math.max(duration / (to - from), 1000 / self.opts.fps)\n  -- local step_duration = math.max(duration / (to - from), 1)\n  local step_count = math.max(math.floor(duration / step_duration + 0.5), 10)\n\n  local delta = 0\n  if (self.opts.easing or \"linear\") == \"linear\" and self.opts.int then\n    local one_step = math.max(1, math.floor(math.abs(to - from) / step_count + 0.5))\n    step_count = math.floor(math.abs(to - from) / one_step + 0.5)\n    delta = math.abs(to - from) - one_step * step_count\n    step_duration = duration / step_count\n  end\n\n  self.steps = {}\n  for i = 1, step_count do\n    local value = 0\n    if i == step_count then\n      value = to\n    else\n      value = self.easing(i, from, to - from - delta, step_count)\n    end\n    if self.opts.int then\n      value = math.floor(value + 0.5)\n    end\n    table.insert(self.steps, value)\n  end\n  self._step = 0\n  active[self.id] = self\n  self.timer = assert(uv.new_timer())\n  self.timer:start(0, step_duration, function()\n    vim.schedule(function()\n      self:step(cb)\n    end)\n  end)\n  return self\nend\n\n---@param cb snacks.animate.cb\nfunction Animation:step(cb)\n  if not self.steps or not self._step or self._step >= #self.steps then\n    return self:stop()\n  end\n  self._step = self._step + 1\n  local value = self.steps[self._step]\n  local done = self._step >= #self.steps\n  local prev = self.steps[self._step - 1] or value\n  cb(value, { anim = self, prev = prev, done = done })\nend\n\nfunction Animation:stop()\n  if self.timer then\n    if self.timer:is_active() then\n      self.timer:stop()\n      self.timer:close()\n      self.timer = nil\n    end\n  end\n  self.steps, self._step = nil, nil\nend\n\n--- Check if animations are enabled.\n--- Will return false if `snacks_animate` is set to false or if the buffer\n--- local variable `snacks_animate` is set to false.\n---@param opts? {buf?: number, name?: string}\nfunction M.enabled(opts)\n  opts = opts or {}\n  if opts.name and not M.enabled({ buf = opts.buf }) then\n    return false\n  end\n  local key = \"snacks_animate\" .. (opts.name and (\"_\" .. opts.name) or \"\")\n  return Snacks.util.var(opts.buf, key, true)\nend\n\n--- Add an animation\n---@param from number\n---@param to number\n---@param cb snacks.animate.cb\n---@param opts? snacks.animate.Opts\nfunction M.add(from, to, cb, opts)\n  return Animation.new(opts):start(from, to, cb)\nend\n\n--- Delete an animation\n---@param id number|string\nfunction M.del(id)\n  if active[id] then\n    active[id]:stop()\n    active[id] = nil\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/bigfile.lua",
    "content": "---@private\n---@class snacks.bigfile\nlocal M = {}\n\nM.meta = {\n  desc = \"Deal with big files\",\n  needs_setup = true,\n}\n\n---@class snacks.bigfile.Config\n---@field enabled? boolean\nlocal defaults = {\n  notify = true, -- show notification when big file detected\n  size = 1.5 * 1024 * 1024, -- 1.5MB\n  line_length = 1000, -- average line length (useful for minified files)\n  -- Enable or disable features when big file detected\n  ---@param ctx {buf: number, ft:string}\n  setup = function(ctx)\n    if vim.fn.exists(\":NoMatchParen\") ~= 0 then\n      vim.cmd([[NoMatchParen]])\n    end\n    Snacks.util.wo(0, { foldmethod = \"manual\", statuscolumn = \"\", conceallevel = 0 })\n    vim.b.completion = false\n    vim.b.minianimate_disable = true\n    vim.b.minihipatterns_disable = true\n    vim.schedule(function()\n      if vim.api.nvim_buf_is_valid(ctx.buf) then\n        vim.bo[ctx.buf].syntax = ctx.ft\n      end\n    end)\n  end,\n}\n\n---@private\nfunction M.setup()\n  local opts = Snacks.config.get(\"bigfile\", defaults)\n\n  vim.filetype.add({\n    pattern = {\n      [\".*\"] = {\n        function(path, buf)\n          if not path or not buf or vim.bo[buf].filetype == \"bigfile\" then\n            return\n          end\n          if path ~= vim.fs.normalize(vim.api.nvim_buf_get_name(buf)) then\n            return\n          end\n          local size = vim.fn.getfsize(path)\n          if size <= 0 then\n            return\n          end\n          if size > opts.size then\n            return \"bigfile\"\n          end\n          local lines = vim.api.nvim_buf_line_count(buf)\n          return (size - lines) / lines > opts.line_length and \"bigfile\" or nil\n        end,\n      },\n    },\n  })\n\n  vim.api.nvim_create_autocmd({ \"FileType\" }, {\n    group = vim.api.nvim_create_augroup(\"snacks_bigfile\", { clear = true }),\n    pattern = \"bigfile\",\n    callback = function(ev)\n      if opts.notify then\n        local path = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(ev.buf), \":p:~:.\")\n        Snacks.notify.warn({\n          (\"Big file detected `%s`.\"):format(path),\n          \"Some Neovim features have been **disabled**.\",\n        }, { title = \"Big File\" })\n      end\n      vim.api.nvim_buf_call(ev.buf, function()\n        opts.setup({\n          buf = ev.buf,\n          ft = vim.filetype.match({ buf = ev.buf }) or \"\",\n        })\n      end)\n    end,\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/bufdelete.lua",
    "content": "---@class snacks.bufdelete\n---@overload fun(buf?: number|snacks.bufdelete.Opts)\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.delete(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Delete buffers without disrupting window layout\",\n}\n\n---@class snacks.bufdelete.Opts\n---@field buf? number Buffer to delete. Defaults to the current buffer\n---@field file? string Delete buffer by file name. If provided, `buf` is ignored\n---@field force? boolean Delete the buffer even if it is modified\n---@field filter? fun(buf: number): boolean Filter buffers to delete\n---@field wipe? boolean Wipe the buffer instead of deleting it (see `:h :bwipeout`)\n\n--- Delete a buffer:\n--- - either the current buffer if `buf` is not provided\n--- - or the buffer `buf` if it is a number\n--- - or every buffer for which `buf` returns true if it is a function\n---@param opts? number|snacks.bufdelete.Opts\nfunction M.delete(opts)\n  opts = opts or {}\n  opts = type(opts) == \"number\" and { buf = opts } or opts\n  opts = type(opts) == \"function\" and { filter = opts } or opts\n  ---@cast opts snacks.bufdelete.Opts\n\n  if type(opts.filter) == \"function\" then\n    for _, b in ipairs(vim.tbl_filter(opts.filter, vim.api.nvim_list_bufs())) do\n      if vim.bo[b].buflisted then\n        M.delete(vim.tbl_extend(\"force\", {}, opts, { buf = b, filter = false }))\n      end\n    end\n    return\n  end\n\n  local buf = opts.buf or 0\n  if opts.file then\n    buf = vim.fn.bufnr(opts.file)\n    if buf == -1 then\n      return\n    end\n  end\n  buf = buf == 0 and vim.api.nvim_get_current_buf() or buf\n\n  if not vim.api.nvim_buf_is_valid(buf) then\n    return\n  end\n\n  -- Check if the buffer is modified\n  if vim.bo[buf].modified and not opts.force then\n    local ok, choice = pcall(vim.fn.confirm, (\"Save changes to %q?\"):format(vim.fn.bufname(buf)), \"&Yes\\n&No\\n&Cancel\")\n    if not ok or choice == 0 or choice == 3 then -- 0 for <Esc>/<C-c> and 3 for Cancel\n      return\n    elseif choice == 1 then -- Yes\n      vim.api.nvim_buf_call(buf, vim.cmd.write)\n    end\n  end\n\n  -- Get the most recently used listed buffer that is not the one being deleted,\n  local info = vim.fn.getbufinfo({ buflisted = 1 })\n  ---@param b vim.fn.getbufinfo.ret.item\n  info = vim.tbl_filter(function(b)\n    return b.bufnr ~= buf\n  end, info)\n  table.sort(info, function(a, b)\n    return a.lastused > b.lastused\n  end)\n\n  local new_buf = info[1] and info[1].bufnr or vim.api.nvim_create_buf(true, false)\n\n  -- replace the buffer in all windows showing it,\n  -- trying to use the alternate buffer if possible\n  for _, win in ipairs(vim.fn.win_findbuf(buf)) do\n    local win_buf = new_buf\n    vim.api.nvim_win_call(win, function() -- Try using alternate buffer\n      local alt = vim.fn.bufnr(\"#\")\n      win_buf = alt >= 0 and alt ~= buf and vim.bo[alt].buflisted and alt or win_buf\n    end)\n    vim.api.nvim_win_set_buf(win, win_buf)\n  end\n\n  if vim.api.nvim_buf_is_valid(buf) then\n    pcall(vim.cmd, (opts.wipe and \"bwipeout! \" or \"bdelete! \") .. buf)\n  end\nend\n\n--- Delete all buffers\n---@param opts? snacks.bufdelete.Opts\nfunction M.all(opts)\n  return M.delete(vim.tbl_extend(\"force\", {}, opts or {}, {\n    filter = function()\n      return true\n    end,\n  }))\nend\n\n--- Delete all buffers except the current one\n---@param opts? snacks.bufdelete.Opts\nfunction M.other(opts)\n  return M.delete(vim.tbl_extend(\"force\", {}, opts or {}, {\n    filter = function(b)\n      return b ~= vim.api.nvim_get_current_buf()\n    end,\n  }))\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/compat.lua",
    "content": "---@generic T\n---@param t T\n---@return T\nlocal function wrap(t)\n  return setmetatable({}, { __index = t })\nend\n\nlocal M = wrap(vim)\n\nM.meta = {\n  desc = \"Neovim compatibility layer\",\n  hide = true,\n}\n\nlocal is_win = jit.os:find(\"Windows\")\n\nM.islist = vim.islist or vim.tbl_islist\nM.uv = vim.uv or vim.loop\n\nif vim.fn.has(\"nvim-0.11\") == 0 then\n  M.fs = wrap(vim.fs)\n\n  ---@param path (string) Path to normalize\n  ---@param opts? vim.fs.normalize.Opts\n  ---@return (string) : Normalized path\n  function M.fs.normalize(path, opts)\n    local ret = vim.fs.normalize(path, opts)\n    return is_win and ret:gsub(\"^%a:\", string.upper) or ret\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/dashboard.lua",
    "content": "---@class snacks.dashboard\n---@overload fun(opts?: snacks.dashboard.Opts): snacks.dashboard.Class\nlocal M = setmetatable({}, {\n  __call = function(M, opts)\n    return M.open(opts)\n  end,\n})\n\nM.meta = {\n  desc = \" Beautiful declarative dashboards\",\n  needs_setup = true,\n}\n\nlocal uv = vim.uv or vim.loop\nmath.randomseed(os.time())\n\n---@class snacks.dashboard.Item\n---@field indent? number\n---@field align? \"left\" | \"center\" | \"right\"\n---@field gap? number the number of empty lines between child items\n---@field padding? number | {[1]:number, [2]:number} bottom or {bottom, top} padding\n--- The action to run when the section is selected or the key is pressed.\n--- * if it's a string starting with `:`, it will be run as a command\n--- * if it's a string, it will be executed as a keymap\n--- * if it's a function, it will be called\n---@field action? snacks.dashboard.Action\n---@field enabled? boolean|fun(opts:snacks.dashboard.Opts):boolean if false, the section will be disabled\n---@field section? string the name of a section to include. See `Snacks.dashboard.sections`\n---@field [string] any section options\n---@field key? string shortcut key\n---@field hidden? boolean when `true`, the item will not be shown, but the key will still be assigned\n---@field autokey? boolean automatically assign a numerical key\n---@field label? string\n---@field desc? string\n---@field file? string\n---@field footer? string\n---@field header? string\n---@field icon? string\n---@field title? string\n---@field text? string|snacks.dashboard.Text[]\n\n---@alias snacks.dashboard.Format.ctx {width?:number}\n---@alias snacks.dashboard.Action string|fun(self:snacks.dashboard.Class)\n---@alias snacks.dashboard.Gen fun(self:snacks.dashboard.Class):snacks.dashboard.Section?\n---@alias snacks.dashboard.Section snacks.dashboard.Item|snacks.dashboard.Gen|snacks.dashboard.Section[]\n\n---@class snacks.dashboard.Text\n---@field [1] string the text\n---@field hl? string the highlight group\n---@field width? number the width used for alignment\n---@field align? \"left\" | \"center\" | \"right\"\n\n---@private\n---@class snacks.dashboard.Item\n---@field package _? snacks.dashboard.Item._ the position of the item in the dashboard\n\n---@private\n---@class snacks.dashboard.Item._\n---@field pane number 1-indexed\n---@field row number 1-indexed\n---@field col number 0-indexed\n\n---@private\n---@class snacks.dashboard.Line\n---@field [number] snacks.dashboard.Text\n---@field width number\n\n---@private\n---@class snacks.dashboard.Block\n---@field [number] snacks.dashboard.Line\n---@field width number\n\n---@class snacks.dashboard.Config\n---@field enabled? boolean\n---@field sections snacks.dashboard.Section\n---@field formats table<string, snacks.dashboard.Text|fun(item:snacks.dashboard.Item, ctx:snacks.dashboard.Format.ctx):snacks.dashboard.Text>\nlocal defaults = {\n  width = 60,\n  row = nil, -- dashboard position. nil for center\n  col = nil, -- dashboard position. nil for center\n  pane_gap = 4, -- empty columns between vertical panes\n  autokeys = \"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\", -- autokey sequence\n  -- These settings are used by some built-in sections\n  preset = {\n    -- Defaults to a picker that supports `fzf-lua`, `telescope.nvim` and `mini.pick`\n    ---@type fun(cmd:string, opts:table)|nil\n    pick = nil,\n    -- Used by the `keys` section to show keymaps.\n    -- Set your custom keymaps here.\n    -- When using a function, the `items` argument are the default keymaps.\n    -- stylua: ignore\n    ---@type snacks.dashboard.Item[]\n    keys = {\n      { icon = \" \", key = \"f\", desc = \"Find File\", action = \":lua Snacks.dashboard.pick('files')\" },\n      { icon = \" \", key = \"n\", desc = \"New File\", action = \":ene | startinsert\" },\n      { icon = \" \", key = \"g\", desc = \"Find Text\", action = \":lua Snacks.dashboard.pick('live_grep')\" },\n      { icon = \" \", key = \"r\", desc = \"Recent Files\", action = \":lua Snacks.dashboard.pick('oldfiles')\" },\n      { icon = \" \", key = \"c\", desc = \"Config\", action = \":lua Snacks.dashboard.pick('files', {cwd = vim.fn.stdpath('config')})\" },\n      { icon = \" \", key = \"s\", desc = \"Restore Session\", section = \"session\" },\n      { icon = \"󰒲 \", key = \"L\", desc = \"Lazy\", action = \":Lazy\", enabled = package.loaded.lazy ~= nil },\n      { icon = \" \", key = \"q\", desc = \"Quit\", action = \":qa\" },\n    },\n    -- Used by the `header` section\n    header = [[\n███╗   ██╗███████╗ ██████╗ ██╗   ██╗██╗███╗   ███╗\n████╗  ██║██╔════╝██╔═══██╗██║   ██║██║████╗ ████║\n██╔██╗ ██║█████╗  ██║   ██║██║   ██║██║██╔████╔██║\n██║╚██╗██║██╔══╝  ██║   ██║╚██╗ ██╔╝██║██║╚██╔╝██║\n██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║\n╚═╝  ╚═══╝╚══════╝ ╚═════╝   ╚═══╝  ╚═╝╚═╝     ╚═╝]],\n  },\n  -- item field formatters\n  formats = {\n    icon = function(item)\n      if item.file and item.icon == \"file\" or item.icon == \"directory\" then\n        return Snacks.dashboard.icon(item.file, item.icon)\n      end\n      return { item.icon, width = 2, hl = \"icon\" }\n    end,\n    footer = { \"%s\", align = \"center\" },\n    header = { \"%s\", align = \"center\" },\n    file = function(item, ctx)\n      local fname = vim.fn.fnamemodify(item.file, \":~\")\n      fname = ctx.width and #fname > ctx.width and vim.fn.pathshorten(fname) or fname\n      if #fname > ctx.width then\n        local dir = vim.fn.fnamemodify(fname, \":h\")\n        local file = vim.fn.fnamemodify(fname, \":t\")\n        if dir and file then\n          file = file:sub(-(ctx.width - #dir - 2))\n          fname = dir .. \"/…\" .. file\n        end\n      end\n      local dir, file = fname:match(\"^(.*)/(.+)$\")\n      return dir and { { dir .. \"/\", hl = \"dir\" }, { file, hl = \"file\" } } or { { fname, hl = \"file\" } }\n    end,\n  },\n  sections = {\n    { section = \"header\" },\n    { section = \"keys\", gap = 1, padding = 1 },\n    { section = \"startup\" },\n  },\n  debug = false,\n}\n\n-- The default style for the dashboard.\n-- When opening the dashboard during startup, only the `bo` and `wo` options are used.\n-- The other options are used with `:lua Snacks.dashboard()`\nSnacks.config.style(\"dashboard\", {\n  zindex = 10,\n  height = 0,\n  width = 0,\n  bo = {\n    bufhidden = \"wipe\",\n    buftype = \"nofile\",\n    buflisted = false,\n    filetype = \"snacks_dashboard\",\n    swapfile = false,\n    undofile = false,\n  },\n  wo = {\n    colorcolumn = \"\",\n    cursorcolumn = false,\n    cursorline = false,\n    foldmethod = \"manual\",\n    list = false,\n    number = false,\n    relativenumber = false,\n    sidescrolloff = 0,\n    signcolumn = \"no\",\n    spell = false,\n    statuscolumn = \"\",\n    statusline = \"\",\n    winbar = \"\",\n    winhighlight = \"Normal:SnacksDashboardNormal,NormalFloat:SnacksDashboardNormal\",\n    wrap = false,\n  },\n})\n\nM.ns = vim.api.nvim_create_namespace(\"snacks_dashboard\")\n\nlocal links = {\n  Desc = \"Special\",\n  File = \"Special\",\n  Dir = \"NonText\",\n  Footer = \"Title\",\n  Header = \"Title\",\n  Icon = \"Special\",\n  Key = \"Number\",\n  Normal = \"Normal\",\n  Terminal = \"SnacksDashboardNormal\",\n  Special = \"Special\",\n  Title = \"Title\",\n}\nlocal hl_groups = {} ---@type table<string, string>\nfor group in pairs(links) do\n  hl_groups[group:lower()] = \"SnacksDashboard\" .. group\nend\nSnacks.util.set_hl(links, { prefix = \"SnacksDashboard\", default = true })\n\n---@class snacks.dashboard.Opts: snacks.dashboard.Config\n---@field buf? number the buffer to use. If not provided, a new buffer will be created\n---@field win? number the window to use. If not provided, a new floating window will be created\n\n---@class snacks.dashboard.Class\n---@field opts snacks.dashboard.Opts\n---@field buf number\n---@field win number\n---@field _size? {width:number, height:number}\n---@field items snacks.dashboard.Item[]\n---@field row? number\n---@field col? number\n---@field panes? snacks.dashboard.Item[][]\n---@field lines? string[]\n---@field augroup integer\nlocal D = {}\n\n---@param opts? snacks.dashboard.Opts\n---@return snacks.dashboard.Class\nfunction M.open(opts)\n  local self = setmetatable({}, { __index = D })\n  self.opts = Snacks.config.get(\"dashboard\", defaults, opts) --[[@as snacks.dashboard.Opts]]\n  self.buf = self.opts.buf or vim.api.nvim_create_buf(false, true)\n  self.buf = self.buf == 0 and vim.api.nvim_get_current_buf() or self.buf\n  self.win = self.opts.win or Snacks.win({ style = \"dashboard\", buf = self.buf, enter = true }).win --[[@as number]]\n  self.win = self.win == 0 and vim.api.nvim_get_current_win() or self.win\n  self.augroup = vim.api.nvim_create_augroup(\"snacks_dashboard\", { clear = true })\n  self:init()\n  self:update()\n  self.fire(\"Opened\")\n  return self\nend\n\n---@param name? string\nfunction D:trace(name)\n  return self.opts.debug and Snacks.debug.trace(name and (\"dashboard:\" .. name) or nil)\nend\n\nfunction D:init()\n  vim.api.nvim_win_set_buf(self.win, self.buf)\n  vim.o.ei = \"all\"\n  Snacks.util.wo(self.win, Snacks.config.styles.dashboard.wo)\n  Snacks.util.bo(self.buf, Snacks.config.styles.dashboard.bo)\n  vim.b[self.buf].snacks_main = true\n  vim.o.ei = \"\"\n  if self:is_float() then\n    vim.keymap.set(\"n\", \"<esc>\", \"<cmd>bd<cr>\", { silent = true, buffer = self.buf })\n  end\n  vim.keymap.set(\"n\", \"q\", \"<cmd>bd<cr>\", { silent = true, buffer = self.buf })\n  vim.api.nvim_create_autocmd({ \"WinResized\", \"VimResized\" }, {\n    group = self.augroup,\n    callback = function()\n      -- only re-render if the size has changed\n      if not vim.deep_equal(self._size, self:size()) then\n        self:update()\n      end\n    end,\n  })\n  vim.api.nvim_create_autocmd({ \"BufWipeout\", \"BufDelete\" }, {\n    buffer = self.buf,\n    callback = function()\n      self.fire(\"Closed\")\n      vim.api.nvim_del_augroup_by_id(self.augroup)\n    end,\n  })\n  vim.api.nvim_create_autocmd(\"WinEnter\", {\n    group = self.augroup,\n    callback = function(ev)\n      if ev.buf == self.buf and not vim.api.nvim_win_is_valid(self.win) then\n        self.win = vim.fn.bufwinid(self.buf)\n        self:update()\n      end\n    end,\n  })\n  self.on(\"Update\", function()\n    self:update()\n  end, self.augroup)\nend\n\n---@return {width:number, height:number}\nfunction D:size()\n  return {\n    width = vim.api.nvim_win_get_width(self.win),\n    height = vim.api.nvim_win_get_height(self.win) + (vim.o.laststatus >= 2 and 1 or 0),\n  }\nend\n\nfunction D:is_float()\n  return vim.api.nvim_win_get_config(self.win).relative ~= \"\"\nend\n\n---@param action snacks.dashboard.Action\nfunction D:action(action)\n  -- close the window before running the action if it's floating\n  if self:is_float() then\n    vim.api.nvim_win_close(self.win, true)\n    self.win = nil\n  end\n  if type(action) == \"string\" then\n    if action:find(\"^:\") then\n      return vim.cmd(action:sub(2))\n    else\n      local keys = vim.api.nvim_replace_termcodes(action, true, true, true)\n      return vim.api.nvim_feedkeys(keys, \"tm\", true)\n    end\n  end\n  action(self)\nend\n\n---@param item snacks.dashboard.Item\n---@param field string\n---@param width? number\n---@return snacks.dashboard.Text|snacks.dashboard.Text[]\nfunction D:format_field(item, field, width)\n  if type(item[field]) == \"table\" then\n    return item[field]\n  end\n  local format = self.opts.formats[field]\n  if format == nil then\n    return { item[field], hl = field }\n  elseif type(format) == \"function\" then\n    return format(item, { width = width })\n  else\n    local text = format and vim.deepcopy(format) or { \"%s\" }\n    text.hl = text.hl or field\n    text[1] = text[1] == \"%s\" and item[field] or text[1]:format(item[field])\n    return text\n  end\nend\n\n---@param item snacks.dashboard.Text|snacks.dashboard.Line\n---@param width? number\n---@param align? \"left\"|\"center\"|\"right\"\nfunction D:align(item, width, align)\n  local len = 0\n  if type(item[1]) == \"string\" then ---@cast item snacks.dashboard.Text\n    width, align, len = width or item.width, align or item.align, vim.api.nvim_strwidth(item[1])\n  else ---@cast item snacks.dashboard.Line\n    if #item == 1 then -- only one text, so align that instead\n      self:align(item[1], width, align)\n      item.width = item[1].width\n      return\n    end\n    len = item.width\n  end\n\n  if not width or width <= 0 or width == len then\n    item.width = math.max(width or 0, len)\n    return\n  end\n\n  align = align or \"left\"\n  local before = align == \"center\" and math.floor((width - len) / 2) or align == \"right\" and width - len or 0\n  local after = align == \"center\" and width - len - before or align == \"left\" and width - len or 0\n\n  if type(item[1]) == \"string\" then ---@cast item snacks.dashboard.Text\n    item[1] = (\" \"):rep(before) .. item[1] .. (\" \"):rep(after)\n    item.width = math.max(width, len)\n  else ---@cast item snacks.dashboard.Line\n    if before > 0 then\n      table.insert(item, 1, { (\" \"):rep(before) })\n    end\n    if after > 0 then\n      table.insert(item, { (\" \"):rep(after) })\n    end\n    item.width = math.max(width, len)\n  end\nend\n\n---@param texts snacks.dashboard.Text[]|snacks.dashboard.Text|string\nfunction D:texts(texts)\n  texts = type(texts) == \"string\" and { { texts } } or texts\n  texts = type(texts[1]) == \"string\" and { texts } or texts\n  return texts --[[ @as snacks.dashboard.Text[] ]]\nend\n\n--- Create a block from a list of texts (possibly with newlines)\n---@param texts snacks.dashboard.Text[]\nfunction D:block(texts)\n  local ret = { { width = 0 }, width = 0 } ---@type snacks.dashboard.Block\n  for _, text in ipairs(texts) do\n    -- PERF: only split lines when needed\n    local lines = text[1]:find(\"\\n\", 1, true) and vim.split(text[1], \"\\n\", { plain = true }) or { text[1] }\n    for l, line in ipairs(lines) do\n      if l > 1 then\n        ret[#ret + 1] = { width = 0 }\n      end\n      local child = setmetatable({ line }, { __index = text })\n      self:align(child)\n      ret[#ret].width = ret[#ret].width + vim.api.nvim_strwidth(child[1])\n      ret.width = math.max(ret.width, ret[#ret].width)\n      table.insert(ret[#ret], child)\n    end\n  end\n  return ret\nend\n\n---@param item snacks.dashboard.Item\nfunction D:format(item)\n  local width = item.indent or 0\n\n  ---@param fields string[]\n  ---@param opts {align?:\"left\"|\"center\"|\"right\", padding?:number, flex?:boolean, multi?:boolean}\n  local function find(fields, opts)\n    local flex = opts.flex and math.max(0, self.opts.width - width) or nil\n    local texts = {} ---@type snacks.dashboard.Text[]\n    for _, k in ipairs(fields) do\n      if item[k] then\n        vim.list_extend(texts, self:texts(self:format_field(item, k, flex)))\n        if not opts.multi then\n          break\n        end\n      end\n    end\n    if #texts == 0 then\n      return { width = 0 }\n    end\n    local block = self:block(texts)\n    block.width = block.width + (opts.padding or 0)\n    width = width + block.width\n    return block\n  end\n\n  local block = item.text and self:block(self:texts(item.text))\n  local left = block and { width = 0 } or find({ \"icon\" }, { align = \"left\", padding = 1 })\n  local right = block and { width = 0 } or find({ \"label\", \"key\" }, { align = \"right\", padding = 1 })\n  local center = block or find({ \"header\", \"footer\", \"title\", \"desc\", \"file\" }, { flex = true, multi = true })\n\n  local padding = self:padding(item)\n  local ret = { width = self.opts.width } ---@type snacks.dashboard.Block\n  for l = 1, math.max(#left, #center, #right, 1) + padding[1] do\n    ret[l] = { width = 0 }\n    left[l] = left[l] or { width = 0 }\n    right[l] = right[l] or { width = 0 }\n    center[l] = center[l] or { width = 0 }\n    self:align(left[l], left.width, \"left\")\n    if item.indent then\n      self:align(left[l], left[l].width + item.indent, \"right\")\n    end\n    self:align(right[l], right.width, \"right\")\n    self:align(center[l], self.opts.width - left[l].width - right[l].width, item.align)\n    vim.list_extend(ret[l], left[l])\n    vim.list_extend(ret[l], center[l])\n    vim.list_extend(ret[l], right[l])\n    ret[l].width = left[l].width + center[l].width + right[l].width\n  end\n  for _ = 1, padding[2] do\n    table.insert(ret, 1, { width = self.opts.width })\n  end\n  return ret\nend\n\n---@param item snacks.dashboard.Item\nfunction D:enabled(item)\n  local e = item.enabled\n  if type(e) == \"function\" then\n    return e(self.opts)\n  end\n  return e == nil or e\nend\n\n---@param item snacks.dashboard.Section?\n---@param results? snacks.dashboard.Item[]\n---@param parent? snacks.dashboard.Item\nfunction D:resolve(item, results, parent)\n  results = results or {}\n  if not item then\n    return results\n  end\n  if type(item) == \"table\" and vim.tbl_isempty(item) then\n    return results\n  end\n  if type(item) == \"table\" and parent then -- inherit parent properties\n    for _, prop in ipairs({ \"indent\", \"align\", \"pane\" }) do\n      item[prop] = item[prop] or parent[prop]\n    end\n  end\n\n  if type(item) == \"function\" then\n    return self:resolve(item(self), results, parent)\n  elseif type(item) == \"table\" and self:enabled(item) then\n    if not item.section and not item[1] then\n      table.insert(results, item)\n      return results\n    end\n    local first_child = #results + 1\n    if item.section then -- add section items\n      self:trace(\"resolve.\" .. item.section)\n      local items = M.sections[item.section](item) ---@type snacks.dashboard.Section?\n      self:resolve(items, results, item)\n      self:trace()\n    end\n    if item[1] then -- add child items\n      for _, child in ipairs(item) do\n        self:resolve(child, results, item)\n      end\n    end\n\n    -- add the title if there are child items\n    if #results >= first_child and item.title then\n      table.insert(results, first_child, {\n        title = item.title,\n        icon = item.icon,\n        pane = item.pane,\n        action = item.action,\n        key = item.key,\n        label = item.label,\n      })\n      item.action = nil\n      item.label = nil\n      item.key = nil\n      first_child = first_child + 1\n    end\n\n    -- correct first/last taking hidden items into account\n    local first, last = first_child, #results\n    for c = first_child, #results do\n      first = first or not results[c].hidden and c or nil\n      last = not results[c].hidden and c or last\n    end\n\n    if item.gap then -- add padding between child items\n      for i = first, last - 1 do\n        results[i].padding = item.gap\n      end\n    end\n    if item.padding then -- add padding to the first and last child items\n      local padding = self:padding(item)\n      if padding[2] > 0 and results[first] then\n        results[first].padding = { 0, padding[2] }\n      end\n      if padding[1] > 0 and results[last] then\n        results[last].padding = { padding[1], 0 }\n      end\n    end\n  elseif type(item) ~= \"table\" then\n    Snacks.notify.error(\"Invalid item:\\n```lua\\n\" .. vim.inspect(item) .. \"\\n```\", { title = \"Dashboard\" })\n  end\n  return results\nend\n\n---@return {[1]: number, [2]: number}\nfunction D:padding(item)\n  return item.padding and (type(item.padding) == \"table\" and item.padding or { item.padding, 0 }) or { 0, 0 }\nend\n\nfunction D.fire(event)\n  vim.api.nvim_exec_autocmds(\"User\", { pattern = \"SnacksDashboard\" .. event, modeline = false })\nend\n\n---@param event string|string[]\n---@param cb fun()\n---@param group? string|integer\nfunction D.on(event, cb, group)\n  return vim.api.nvim_create_autocmd(\"User\", { pattern = \"SnacksDashboard\" .. event, callback = cb, group = group })\nend\n\n---@param pos {[1]:number, [2]:number}\n---@param from? {[1]:number, [2]:number}\nfunction D:find(pos, from)\n  from = from or pos\n  local line = self.lines[pos[1]]\n  local char = vim.fn.charidx(line, pos[2]) -- map col to charachter index\n\n  local pane = math.floor((char - self.col) / (self.opts.width + self.opts.pane_gap)) + 1\n  pane = math.max(1, math.min(pane, #self.panes))\n  if pos[1] == from[1] then\n    if pos[2] == from[2] - 1 then\n      pane = pane - 1\n    elseif pos[2] == from[2] + 1 then\n      pane = pane + 1\n    end\n  end\n  pane = math.max(1, math.min(pane, #self.panes))\n\n  local ret ---@type snacks.dashboard.Item?\n  for _, item in ipairs(self.items) do\n    if item._ and item._.pane == pane and item.action then\n      if ret and pos[1] < from[1] and item._.row > pos[1] then\n        break\n      end\n      ret = item\n      if pos[1] >= from[1] and item._.row >= pos[1] then\n        break\n      end\n    end\n  end\n  return ret\nend\n\n-- Layout in panes\nfunction D:layout()\n  local max_panes =\n    math.max(1, math.floor((self._size.width + self.opts.pane_gap) / (self.opts.width + self.opts.pane_gap)))\n  self.panes = {} ---@type snacks.dashboard.Item[][]\n  for _, item in ipairs(self.items) do\n    if not item.hidden then\n      local pane = item.pane or 1\n      pane = math.fmod(pane - 1, max_panes) + 1 -- distribute panes evenly\n      self.panes[pane] = self.panes[pane] or {}\n      table.insert(self.panes[pane], item)\n    end\n  end\n  for p = 1, math.max(unpack(vim.tbl_keys(self.panes))) or 1 do\n    self.panes[p] = self.panes[p] or {}\n  end\nend\n\n-- Format and render the dashboard\nfunction D:render()\n  -- horizontal position\n  self.col = self.opts.col\n    or math.floor(self._size.width - (self.opts.width * #self.panes + self.opts.pane_gap * (#self.panes - 1))) / 2\n\n  self.lines = {} ---@type string[]\n  local extmarks = {} ---@type {row:number, col:number, opts:vim.api.keyset.set_extmark}[]\n  for p, pane in ipairs(self.panes) do\n    local indent = (\" \"):rep(p == 1 and self.col or self.opts.pane_gap)\n    local row = 0\n    for _, item in ipairs(pane or {}) do\n      for l, line in ipairs(self:format(item)) do\n        row = row + 1\n        if p > 1 and not self.lines[row] then -- add lines for empty panes\n          self.lines[row] = (\" \"):rep(self.col + (self.opts.width + self.opts.pane_gap) * (p - 1))\n        elseif p == 1 and line.width > self.opts.width then\n          self.lines[row] = (\" \"):rep(self.col - math.floor((line.width - self.opts.width) / 2))\n        else\n          self.lines[row] = (self.lines[row] or \"\") .. indent\n        end\n        if l == 1 then\n          item._ = { pane = p, row = row, col = #self.lines[row] - 1 }\n        end\n        ---@cast line snacks.dashboard.Line\n        for _, text in ipairs(line) do\n          self.lines[row] = self.lines[row] .. text[1]\n          if text.hl then\n            table.insert(extmarks, {\n              row = row - 1,\n              col = #self.lines[row] - #text[1],\n              opts = { hl_group = hl_groups[text.hl] or text.hl, end_col = #self.lines[row] },\n            })\n          end\n        end\n      end\n    end\n  end\n\n  -- vertical position\n  self.row = self.opts.row or math.max(math.floor((self._size.height - #self.lines) / 2), 0)\n  for _ = 1, self.row do\n    table.insert(self.lines, 1, \"\")\n  end\n\n  -- fix item positions\n  for _, item in ipairs(self.items) do\n    if item._ then\n      item._.row = item._.row + self.row\n      if item.render then\n        item.render(self, { item._.row, item._.col })\n      end\n    end\n  end\n\n  self:render_buf(extmarks)\nend\n\n---@param extmarks {row:number, col:number, opts:vim.api.keyset.set_extmark}[]\nfunction D:render_buf(extmarks)\n  -- set lines\n  vim.bo[self.buf].modifiable = true\n  vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, self.lines)\n  vim.bo[self.buf].modifiable = false\n\n  -- extmarks\n  vim.api.nvim_buf_clear_namespace(self.buf, M.ns, 0, -1)\n  for _, extmark in ipairs(extmarks) do\n    vim.api.nvim_buf_set_extmark(self.buf, M.ns, extmark.row + self.row, extmark.col, extmark.opts)\n  end\nend\n\nfunction D:keys()\n  local autokeys = self.opts.autokeys:gsub(\"[hjklq]\", \"\")\n  for _, item in ipairs(self.items) do\n    if item.key and not item.autokey then\n      autokeys = autokeys:gsub(vim.pesc(item.key), \"\", 1)\n    end\n  end\n  for _, item in ipairs(self.items) do\n    if item.autokey then\n      item.key, autokeys = autokeys:sub(1, 1), autokeys:sub(2)\n    end\n    if item.key then\n      vim.keymap.set(\"n\", item.key, function()\n        self:action(item.action)\n      end, { buffer = self.buf, nowait = not item.autokey, desc = \"Dashboard action\" })\n    end\n  end\nend\n\nfunction D:update()\n  if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then\n    return\n  end\n  self.fire(\"UpdatePre\")\n  self._size = self:size()\n\n  self.items = self:resolve(self.opts.sections)\n\n  self:layout()\n  self:keys()\n  self:render()\n\n  -- actions on enter\n  vim.keymap.set(\"n\", \"<cr>\", function()\n    local item = self:find(vim.api.nvim_win_get_cursor(self.win))\n    return item and item.action and self:action(item.action)\n  end, { buffer = self.buf, nowait = true, desc = \"Dashboard action\" })\n\n  -- cursor movement\n  local last = { 1, 0 }\n  local function update_cursor()\n    local item = self:find(vim.api.nvim_win_get_cursor(self.win), last)\n    -- can happen for panes without actionable items\n    item = item or vim.tbl_filter(function(it)\n      return it.action and it._\n    end, self.items)[1]\n    if item then\n      local col = self.lines[item._.row]:find(\"[%w%d%p]\", item._.col + 1)\n      col = col or (item._.col + 1 + (item.indent and (item.indent + 1) or 0))\n      last = { item._.row, (col or item._.col + 1) - 1 }\n    end\n    vim.api.nvim_win_set_cursor(self.win, last)\n  end\n  vim.api.nvim_create_autocmd(\"CursorMoved\", {\n    group = vim.api.nvim_create_augroup(\"snacks_dashboard_cursor\", { clear = true }),\n    buffer = self.buf,\n    callback = update_cursor,\n  })\n  update_cursor()\n  self.fire(\"UpdatePost\")\nend\n\n-- Get an icon\n---@param name string\n---@param cat? string\n---@return snacks.dashboard.Text\nfunction M.icon(name, cat)\n  local icon, hl = Snacks.util.icon(name, cat)\n  return { icon or \" \", hl = hl or \"icon\", width = 2 }\nend\n\n-- Used by the default preset to pick something\n---@param cmd? string\nfunction M.pick(cmd, opts)\n  cmd = cmd or \"files\"\n  local config = Snacks.config.get(\"dashboard\", defaults, opts)\n  local picker = Snacks.picker.config.get()\n  -- stylua: ignore\n  local try = {\n    function() return config.preset.pick(cmd, opts) end,\n    function() return require(\"fzf-lua\")[cmd](opts) end,\n    function() return require(\"telescope.builtin\")[cmd == \"files\" and \"find_files\" or cmd](opts) end,\n    function() return require(\"mini.pick\").builtin[cmd](opts) end,\n    function() return Snacks.picker(cmd, opts) end,\n  }\n  if picker.enabled then\n    table.insert(try, 2, table.remove(try, #try))\n  end\n  for _, fn in ipairs(try) do\n    if pcall(fn) then\n      return\n    end\n  end\n  Snacks.notify.error(\"No picker found for \" .. cmd)\nend\n\n-- Checks if the plugin is installed.\n-- Only works with [lazy.nvim](https://github.com/folke/lazy.nvim)\n---@param name string\nfunction M.have_plugin(name)\n  return package.loaded.lazy and require(\"lazy.core.config\").spec.plugins[name] ~= nil\nend\n\n---@param opts? {filter?: table<string, boolean>}\n---@return fun():string?\nfunction M.oldfiles(opts)\n  opts = vim.tbl_deep_extend(\"force\", {\n    filter = {\n      [vim.fn.stdpath(\"data\")] = false,\n      [vim.fn.stdpath(\"cache\")] = false,\n      [vim.fn.stdpath(\"state\")] = false,\n    },\n  }, opts or {})\n  ---@cast opts {filter:table<string, boolean>}\n\n  local filter = {} ---@type {path:string, want:boolean}[]\n  for path, want in pairs(opts.filter or {}) do\n    table.insert(filter, { path = svim.fs.normalize(path), want = want })\n  end\n  local done = {} ---@type table<string, boolean>\n  local i = 1\n  local oldfiles = vim.v.oldfiles\n  return function()\n    while oldfiles[i] do\n      local file = svim.fs.normalize(oldfiles[i], { _fast = true, expand_env = false })\n      local want = not done[file]\n      if want then\n        done[file] = true\n        for _, f in ipairs(filter) do\n          local matches = file:sub(1, #f.path) == f.path\n            and (file == f.path or file:sub(#f.path + 1, #f.path + 1):find(\"[/\\\\]\") ~= nil)\n          if matches ~= f.want then\n            want = false\n            break\n          end\n        end\n      end\n      i = i + 1\n      if want and uv.fs_stat(file) then\n        return file\n      end\n    end\n  end\nend\n\nM.sections = {}\n\n-- Adds a section to restore the session if any of the supported plugins are installed.\n---@param item? snacks.dashboard.Item\n---@return snacks.dashboard.Item?\nfunction M.sections.session(item)\n  local plugins = {\n    { \"persistence.nvim\", \":lua require('persistence').load()\" },\n    { \"persisted.nvim\", \":lua require('persisted').load()\" },\n    { \"neovim-session-manager\", \":SessionManager load_current_dir_session\" },\n    { \"possession.nvim\", \":PossessionLoadCwd\" },\n    { \"mini.sessions\", \":lua require('mini.sessions').read()\" },\n    { \"mini.nvim\", \":lua require('mini.sessions').read()\" },\n    { \"auto-session\", \":AutoSession restore\" },\n  }\n  for _, plugin in pairs(plugins) do\n    if M.have_plugin(plugin[1]) then\n      return setmetatable({ -- add the action and disable the section\n        action = plugin[2],\n        section = false,\n      }, { __index = item })\n    end\n  end\nend\n\n--- Get the most recent files, optionally filtered by the\n--- current working directory or a custom directory.\n---@param opts? {limit?:number, cwd?:string|boolean, filter?:fun(file:string):boolean?}\n---@return snacks.dashboard.Gen\nfunction M.sections.recent_files(opts)\n  return function()\n    opts = opts or {}\n    local limit = opts.limit or 5\n    local root = opts.cwd and svim.fs.normalize(opts.cwd == true and vim.fn.getcwd() or opts.cwd) or nil\n    -- Only filter by directory when root is specified. If nil, M.oldfiles will use default filters only (excludes stdpath data/cache/state).\n    local oldfiles_opts = root and { filter = { [root] = true } } or nil\n    local ret = {} ---@type snacks.dashboard.Section\n    for file in M.oldfiles(oldfiles_opts) do\n      if not opts.filter or opts.filter(file) then\n        ret[#ret + 1] = {\n          file = file,\n          icon = \"file\",\n          action = \":e \" .. vim.fn.fnameescape(file),\n          autokey = true,\n        }\n        if #ret >= limit then\n          break\n        end\n      end\n    end\n    return ret\n  end\nend\n\n--- Get the most recent projects based on git roots of recent files.\n--- The default action will change the directory to the project root,\n--- try to restore the session and open the picker if the session is not restored.\n--- You can customize the behavior by providing a custom action.\n--- Use `opts.dirs` to provide a list of directories to use instead of the git roots.\n---@param opts? {limit?:number, dirs?:(string[]|fun():string[]), pick?:boolean, session?:boolean, action?:fun(dir), filter?:fun(dir:string):boolean?}\nfunction M.sections.projects(opts)\n  opts = vim.tbl_extend(\"force\", { pick = true, session = true }, opts or {})\n  local limit = opts.limit or 5\n  local dirs = opts.dirs or {}\n  dirs = type(dirs) == \"function\" and dirs() or dirs --[[ @as string[] ]]\n  dirs = vim.list_slice(dirs, 1, limit)\n\n  if not opts.dirs then\n    for file in M.oldfiles() do\n      local dir = Snacks.git.get_root(file)\n      if dir and not vim.tbl_contains(dirs, dir) then\n        if not opts.filter or opts.filter(dir) then\n          table.insert(dirs, dir)\n          if #dirs >= limit then\n            break\n          end\n        end\n      end\n    end\n  end\n\n  local ret = {} ---@type snacks.dashboard.Item[]\n  for _, dir in ipairs(dirs) do\n    if not opts.filter or opts.filter(dir) then\n      ret[#ret + 1] = {\n        file = dir,\n        icon = \"directory\",\n        action = function(self)\n          if opts.action then\n            return opts.action(dir)\n          end\n          vim.fn.chdir(dir)\n          local session = M.sections.session()\n        -- stylua: ignore\n        if opts.session and session then\n          local session_loaded = false\n          vim.api.nvim_create_autocmd(\"SessionLoadPost\", { once = true, callback = function() session_loaded = true end })\n          vim.defer_fn(function() if not session_loaded and opts.pick then M.pick() end end, 100)\n          self:action(session.action)\n        elseif opts.pick then\n          M.pick()\n        end\n        end,\n        autokey = true,\n      }\n    end\n  end\n  return ret\nend\n\n---@return snacks.dashboard.Gen\nfunction M.sections.header()\n  return function(self)\n    return { header = self.opts.preset.header, padding = 2 }\n  end\nend\n\n---@return snacks.dashboard.Gen\nfunction M.sections.keys()\n  return function(self)\n    return vim.deepcopy(self.opts.preset.keys)\n  end\nend\n\n---@param opts {cmd:string|string[], ttl?:number, height?:number, width?:number, random?:number}|snacks.dashboard.Item\n---@return snacks.dashboard.Gen\nfunction M.sections.terminal(opts)\n  return function(self)\n    local cmd = opts.cmd or 'echo \"No `cmd` provided\"'\n    if type(cmd) == \"string\" and vim.fn.has(\"linux\") == 1 and not vim.o.shell:find(\"nu\") then\n      -- work-around for https://github.com/folke/snacks.nvim/issues/1706\n      -- jobstart+pty sometimes doesn't flush the full output before exiting\n      cmd = cmd .. \"; sleep .1\"\n    end\n\n    local ttl = opts.ttl or 3600\n    local height, width = opts.height or 10, opts.width or (self.opts.width - (opts.indent or 0))\n    local hl = opts.hl and hl_groups[opts.hl] or opts.hl or \"SnacksDashboardTerminal\"\n    local cache_buf, term_buf, win ---@type integer?, integer?, integer?\n\n    local cache_parts = {\n      table.concat(type(cmd) == \"table\" and cmd or { cmd }, \" \"),\n      uv.cwd(),\n      opts.random and math.random(1, opts.random) or \"\",\n    }\n    local cache_dir = vim.fn.stdpath(\"cache\") .. \"/snacks\"\n    local cache_file = (\"%s/%s.txt\"):format(cache_dir, vim.fn.sha256(table.concat(cache_parts, \".\")))\n    local stat = uv.fs_stat(cache_file)\n    local has_cache = stat and stat.type == \"file\" and stat.size > 0\n    local is_expired = has_cache and stat and os.time() - stat.mtime.sec >= ttl\n\n    if has_cache and stat then -- show cached output\n      cache_buf = vim.api.nvim_create_buf(false, true)\n      vim.bo[cache_buf].buftype = \"nofile\"\n      local fin = assert(uv.fs_open(cache_file, \"r\", 438))\n      vim.api.nvim_chan_send(vim.api.nvim_open_term(cache_buf, {}), uv.fs_read(fin, stat.size, 0) or \"\")\n      uv.fs_close(fin)\n      -- -- HACK: without this, some lines may not show up in the terminal buffer\n      vim.bo[cache_buf].scrollback = 9999\n      vim.bo[cache_buf].scrollback = 9998\n    end\n\n    ---@param buf integer\n    local function show(buf)\n      if win and vim.api.nvim_win_is_valid(win) then\n        vim.api.nvim_win_set_buf(win, buf)\n        Snacks.util.wo(win, { winhighlight = \"TermCursorNC:\" .. hl .. \",NormalFloat:\" .. hl })\n        Snacks.util.bo(buf, { filetype = Snacks.config.styles.dashboard.bo.filetype })\n      end\n    end\n\n    local job ---@type snacks.Job?\n    if not has_cache or is_expired then\n      term_buf = vim.api.nvim_create_buf(false, true)\n      local term_ready = false\n      local output = {} ---@type string[]\n\n      local recording = vim.defer_fn(function()\n        output = {}\n        show(term_buf)\n      end, 5000) --[[@as uv.uv_timer_t]]\n\n      local Job = require(\"snacks.util.job\")\n      job = Job.new(\n        term_buf,\n        cmd,\n        Snacks.config.merge({}, {\n          start = false,\n          term = true,\n          width = width,\n          height = height,\n          on_stdout = function(_, data)\n            if recording:is_active() then\n              table.insert(output, table.concat(data, \"\\n\"))\n            end\n            if not term_ready and job then\n              local non_empty = #vim.tbl_filter(function(line)\n                return line:match(\"%S\")\n              end, job.lines)\n              if non_empty >= 3 then\n                term_ready = true\n                show(term_buf)\n              end\n            end\n          end,\n          on_exit = function(_, code)\n            if job and job.killed then\n              return\n            end\n            show(term_buf)\n            if recording:is_active() and code == 0 and ttl > 0 then -- save the output\n              vim.fn.mkdir(cache_dir, \"p\")\n              local fout = assert(uv.fs_open(cache_file, \"w\", 438))\n              local data = table.concat(output, \"\")\n              uv.fs_write(fout, data, 0)\n              uv.fs_close(fout)\n            end\n          end,\n        })\n      )\n    end\n    return {\n      action = not opts.title and opts.action or nil,\n      key = not opts.title and opts.key or nil,\n      label = not opts.title and opts.label or nil,\n      render = function(_, pos)\n        self:trace(\"terminal.render\")\n        -- open the window with the terminal buffer if available.\n        -- This is to ensure it starts with the correct window size.\n        win = vim.api.nvim_open_win(assert(term_buf or cache_buf), false, {\n          bufpos = { pos[1] - 1, pos[2] + 1 },\n          col = opts.indent or 0,\n          focusable = false,\n          height = height,\n          noautocmd = true,\n          relative = \"win\",\n          row = 0,\n          zindex = Snacks.config.styles.dashboard.zindex + 1,\n          style = \"minimal\",\n          width = width,\n          win = self.win,\n          border = \"none\",\n        })\n        if job then -- start the job if needed\n          job:start()\n        end\n        show(assert(cache_buf or term_buf)) -- set the correct buffer\n        local close = vim.schedule_wrap(function()\n          if job then\n            job:stop()\n          end\n          pcall(vim.api.nvim_win_close, win, true)\n          pcall(vim.api.nvim_buf_delete, cache_buf, { force = true })\n          pcall(vim.api.nvim_buf_delete, term_buf, { force = true })\n          return true\n        end)\n        self.on(\"UpdatePre\", close, self.augroup)\n        self.on(\"Closed\", close, self.augroup)\n        self:trace()\n      end,\n      text = (\"\\n\"):rep(height - 1),\n    }\n  end\nend\n\n--- Add the startup section\n---@param opts? {icon?:string}\n---@return snacks.dashboard.Section?\nfunction M.sections.startup(opts)\n  opts = opts or {}\n  M.lazy_stats = M.lazy_stats and M.lazy_stats.startuptime > 0 and M.lazy_stats or require(\"lazy.stats\").stats()\n  local ms = (math.floor(M.lazy_stats.startuptime * 100 + 0.5) / 100)\n  local icon = opts.icon or \"⚡ \"\n  return {\n    align = \"center\",\n    text = {\n      { icon .. \"Neovim loaded \", hl = \"footer\" },\n      { M.lazy_stats.loaded .. \"/\" .. M.lazy_stats.count, hl = \"special\" },\n      { \" plugins in \", hl = \"footer\" },\n      { ms .. \"ms\", hl = \"special\" },\n    },\n  }\nend\n\nM.status = {\n  did_setup = false,\n  opened = false,\n  reason = nil, ---@type string?\n}\n\n--- Check if the dashboard should be opened\nfunction M.setup()\n  local explorer = Snacks.config.get(\"explorer\", defaults).enabled == true\n\n  M.status.did_setup = true\n  local buf = 1\n\n  local skip = false\n  if explorer and vim.fn.argc(-1) == 1 then\n    local arg = vim.fn.argv(0) --[[@as string]]\n    if arg ~= \"\" and vim.fn.isdirectory(arg) == 1 then\n      skip = true\n    end\n  end\n\n  -- don't open the dashboard if there are any arguments\n  if not skip and vim.fn.argc(-1) > 0 then\n    M.status.reason = \"argc(-1) > 0\"\n    return\n  end\n\n  -- don't open dashboard if Neovim was invoked for example `nvim +'Octo issue edit 1'`\n  if not skip and vim.api.nvim_buf_get_name(0) ~= \"\" then\n    M.status.reason = \"buffer has a name\"\n    return\n  end\n\n  -- there should be only one non-floating window and it should be the first buffer\n  local wins = vim.tbl_filter(function(win)\n    local b = vim.api.nvim_win_get_buf(win)\n    return vim.api.nvim_win_get_config(win).relative == \"\" and not vim.bo[b].filetype:find(\"snacks\")\n  end, vim.api.nvim_list_wins())\n  if #wins ~= 1 then\n    M.status.reason = \"more than one non-floating window\"\n    return\n  elseif vim.api.nvim_win_get_buf(wins[1]) ~= buf then\n    M.status.reason = \"window does not contain the first buffer\"\n    return\n  end\n\n  if vim.bo[buf].modified then\n    M.status.reason = \"buffer is modified\"\n    return\n  end\n\n  local uis = vim.api.nvim_list_uis()\n\n  -- check for headless\n  if #uis == 0 then\n    M.status.reason = \"headless\"\n    return\n  end\n\n  -- don't open the dashboard if in TUI and input is piped\n  if uis[1].stdout_tty and not uis[1].stdin_tty then\n    M.status.reason = \"stdin is not a tty\"\n    return\n  end\n\n  -- don't open the dashboard if there is any text in the buffer\n  if vim.api.nvim_buf_line_count(buf) > 1 or #(vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] or \"\") > 0 then\n    M.status.reason = \"buffer is not empty\"\n    return\n  end\n  M.status.opened = true\n\n  if Snacks.config.dashboard.debug then\n    Snacks.debug.tracemod(\"dashboard\", M)\n    Snacks.debug.tracemod(\"dashboard\", D, \":\")\n  end\n\n  local options = { showtabline = vim.o.showtabline, laststatus = vim.o.laststatus }\n  vim.o.showtabline, vim.o.laststatus = 0, 0\n  local dashboard = M.open({ buf = buf, win = wins[1] })\n\n  local function restore()\n    local view = vim.fn.winsaveview()\n    for k, v in pairs(options) do\n      if vim.o[k] == 0 and v ~= 0 then\n        vim.o[k] = v\n      end\n    end\n    options = {}\n    vim.fn.winrestview(view)\n  end\n  restore = vim.schedule_wrap(restore)\n\n  D.on(\"Closed\", restore, dashboard.augroup)\n\n  vim.api.nvim_create_autocmd(\"WinEnter\", {\n    group = dashboard.augroup,\n    callback = function()\n      local win = vim.api.nvim_get_current_win()\n      local is_float = vim.api.nvim_win_get_config(win).relative ~= \"\"\n      if win ~= dashboard.win and not is_float then\n        restore()\n      end\n    end,\n  })\n\n  if Snacks.config.dashboard.debug then\n    Snacks.debug.stats({ min = 0.2 })\n  end\nend\n\n-- Update the dashboard\nfunction M.update()\n  D.fire(\"Update\")\nend\n\nfunction M.health()\n  if Snacks.config.dashboard.enabled then\n    if M.status.did_setup then\n      Snacks.health.ok(\"setup ran\")\n      if M.status.opened then\n        Snacks.health.ok(\"dashboard opened\")\n      else\n        Snacks.health.warn(\"dashboard did not open: `\" .. M.status.reason .. \"`\")\n      end\n    else\n      Snacks.health.error(\"setup did not run\")\n    end\n    local modnames = { \"alpha\", \"dashboard\", \"mini.starter\" }\n    for _, modname in ipairs(modnames) do\n      if package.loaded[modname] then\n        Snacks.health.error(\"`\" .. modname .. \"` conflicts with `Snacks.dashboard`\")\n      end\n    end\n  end\nend\n\nM.Dashboard = D\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/debug.lua",
    "content": "---@class snacks.debug\n---@overload fun(...)\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.inspect(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Pretty inspect & backtraces for debugging\",\n}\n\n---@class snacks.debug.cmd\n---@field cmd string|string[]\n---@field level? snacks.notifier.level|vim.log.levels\n---@field title? string\n---@field args? string[]\n---@field cwd? string\n---@field group? boolean\n---@field notify? boolean\n---@field footer? string\n---@field header? string\n---@field props? table<string, string|boolean|number|nil>\n\nlocal uv = vim.uv or vim.loop\n\nlocal MAX_INSPECT_LINES = 2000\n\nvim.schedule(function()\n  Snacks.util.set_hl({\n    Indent = \"LineNr\",\n    Print = \"NonText\",\n  }, { prefix = \"SnacksDebug\", default = true })\nend)\n\n-- Show a notification with a pretty printed dump of the object(s)\n-- with lua treesitter highlighting and the location of the caller\nfunction M.inspect(...)\n  local len = select(\"#\", ...) ---@type number\n  local obj = { ... } ---@type unknown[]\n  local caller = debug.getinfo(1, \"S\")\n  for level = 2, 10 do\n    local info = debug.getinfo(level, \"S\")\n    if\n      info\n      and info.source ~= caller.source\n      and info.what ~= \"C\"\n      and info.source ~= \"lua\"\n      and info.source ~= \"@\" .. (os.getenv(\"MYVIMRC\") or \"\")\n    then\n      caller = info\n      break\n    end\n  end\n  vim.schedule(function()\n    local title = \"Debug: \" .. vim.fn.fnamemodify(caller.source:sub(2), \":~:.\") .. \":\" .. caller.linedefined\n    local lines = vim.split(vim.inspect(len == 1 and obj[1] or len > 0 and obj or nil), \"\\n\")\n    if #lines > MAX_INSPECT_LINES then\n      local c = #lines\n      lines = vim.list_slice(lines, 1, MAX_INSPECT_LINES)\n      lines[#lines + 1] = \"\"\n      lines[#lines + 1] = (c - MAX_INSPECT_LINES) .. \" more lines have been truncated …\"\n    end\n    Snacks.notify.warn(lines, { title = title, ft = \"lua\" })\n  end)\nend\n\n--- Run the current buffer or a range of lines.\n--- Shows the output of `print` inlined with the code.\n--- Any error will be shown as a diagnostic.\n---@param opts? {name?:string, buf?:number, print?:boolean}\nfunction M.run(opts)\n  local ns = vim.api.nvim_create_namespace(\"snacks_debug\")\n  opts = vim.tbl_extend(\"force\", { print = true }, opts or {})\n  local buf = opts.buf or 0\n  buf = buf == 0 and vim.api.nvim_get_current_buf() or buf\n  local name = opts.name or vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), \":t\")\n\n  -- Get the lines to run\n  local lines ---@type string[]\n  local mode = vim.fn.mode()\n  if mode:find(\"[vV]\") then\n    if mode == \"v\" then\n      vim.cmd(\"normal! v\")\n    elseif mode == \"V\" then\n      vim.cmd(\"normal! V\")\n    end\n    local from = vim.api.nvim_buf_get_mark(buf, \"<\")\n    local to = vim.api.nvim_buf_get_mark(buf, \">\")\n\n    -- for some reason, sometimes the column is off by one\n    -- see: https://github.com/folke/snacks.nvim/issues/190\n    local col_to = math.min(to[2] + 1, #vim.api.nvim_buf_get_lines(buf, to[1] - 1, to[1], false)[1])\n\n    lines = vim.api.nvim_buf_get_text(buf, from[1] - 1, from[2], to[1] - 1, col_to, {})\n    -- Insert empty lines to keep the line numbers\n    for _ = 1, from[1] - 1 do\n      table.insert(lines, 1, \"\")\n    end\n    vim.fn.feedkeys(\"gv\", \"nx\")\n  elseif mode == \"\\22\" then\n    -- Yank the visual selection to handle irregularly shaped blocks\n    local tmp = vim.fn.getreginfo(\"*\")\n    vim.cmd('normal! \"*y')\n    lines = vim.fn.getreginfo(\"*\").regcontents\n    vim.fn.setreg(\"*\", tmp.regcontents, tmp.regtype)\n\n    -- Insert empty lines to keep the line numbers\n    local from = vim.api.nvim_buf_get_mark(buf, \"<\")\n    for _ = 1, from[1] - 1 do\n      table.insert(lines, 1, \"\")\n    end\n\n    -- Restore the selection\n    vim.fn.feedkeys(\"gv\", \"nx\")\n  else\n    lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)\n  end\n\n  -- Clear diagnostics and extmarks\n  local function reset()\n    vim.diagnostic.reset(ns, buf)\n    vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)\n  end\n  reset()\n  vim.api.nvim_create_autocmd({ \"TextChanged\", \"TextChangedI\" }, {\n    group = vim.api.nvim_create_augroup(\"snacks_debug_run_\" .. buf, { clear = true }),\n    buffer = buf,\n    callback = reset,\n  })\n\n  -- Get the line number from the msg or stack\n  local function get_line(msg)\n    local line = msg and msg:match(\"^\" .. vim.pesc(name) .. \":(%d+):\")\n    if line then\n      return line\n    end\n    for level = 2, 20 do\n      local info = debug.getinfo(level, \"Sln\")\n      if info and info.source == \"@\" .. name then\n        return info.currentline\n      end\n    end\n  end\n\n  -- Error handler\n  local function on_error(err)\n    local line = get_line(err)\n    if line then\n      vim.diagnostic.set(ns, buf, {\n        { col = 0, lnum = line - 1, message = err, severity = vim.diagnostic.severity.ERROR },\n      })\n    end\n    M.backtrace({ err, \"\" }, { title = \"Error in \" .. name, level = vim.log.levels.ERROR })\n  end\n\n  -- Print handler\n  local function on_print(...)\n    local str = table.concat(\n      vim.tbl_map(function(v)\n        return type(v) == \"string\" and v or vim.inspect(v)\n      end, { ... }),\n      \" \"\n    )\n    ---@type string[][][]\n    local virt_lines = {}\n    for _, line in ipairs(vim.split(str, \"\\n\", { plain = true })) do\n      table.insert(virt_lines, { { \"  │ \", \"SnacksDebugIndent\" }, { line, \"SnacksDebugPrint\" } })\n    end\n\n    local line = (get_line() or 1) - 1\n    vim.schedule(function()\n      vim.api.nvim_buf_set_extmark(buf, ns, line, 0, {\n        virt_lines = virt_lines,\n      })\n    end)\n  end\n\n  -- Load the code\n  local chunk, err = load(table.concat(lines, \"\\n\"), \"@\" .. name)\n  if not chunk then\n    return on_error(err)\n  end\n\n  -- Setup the env\n  local env = { print = opts.print and on_print or nil }\n  package.seeall(env)\n  setfenv(chunk, env)\n  xpcall(chunk, function(e)\n    on_error(e)\n  end)\nend\n\n-- Show a notification with a pretty backtrace\n---@param msg? string|string[]\n---@param opts? snacks.notify.Opts\nfunction M.backtrace(msg, opts)\n  opts = vim.tbl_deep_extend(\"force\", {\n    level = vim.log.levels.WARN,\n    title = \"Backtrace\",\n  }, opts or {})\n  ---@type string[]\n  local trace = type(msg) == \"table\" and msg or type(msg) == \"string\" and { msg } or {}\n  for level = 2, 20 do\n    local info = debug.getinfo(level, \"Sln\")\n    if info and info.what ~= \"C\" and info.source ~= \"lua\" and not info.source:find(\"snacks[/\\\\]debug\") then\n      local line = \"- `\" .. vim.fn.fnamemodify(info.source:sub(2), \":p:~:.\") .. \"`:\" .. info.currentline\n      if info.name then\n        line = line .. \" _in_ **\" .. info.name .. \"**\"\n      end\n      table.insert(trace, line)\n    end\n  end\n  Snacks.notify(#trace > 0 and (table.concat(trace, \"\\n\")) or \"\", opts)\nend\n\n-- Very simple function to profile a lua function.\n-- * **flush**: set to `true` to use `jit.flush` in every iteration.\n-- * **count**: defaults to 100\n---@param fn fun()\n---@param opts? {count?: number, flush?: boolean, title?: string}\nfunction M.profile(fn, opts)\n  opts = vim.tbl_extend(\"force\", { count = 100, flush = true }, opts or {})\n  local start = uv.hrtime()\n  for _ = 1, opts.count, 1 do\n    if opts.flush then\n      jit.flush(fn, true)\n    end\n    fn()\n  end\n  Snacks.notify(((uv.hrtime() - start) / 1e6 / opts.count) .. \"ms\", { title = opts.title or \"Profile\" })\nend\n\n-- Log a message to the file `./debug.log`.\n-- - a timestamp will be added to every message.\n-- - accepts multiple arguments and pretty prints them.\n-- - if the argument is not a string, it will be printed using `vim.inspect`.\n-- - if the message is smaller than 120 characters, it will be printed on a single line.\n--\n-- ```lua\n-- Snacks.debug.log(\"Hello\", { foo = \"bar\" }, 42)\n-- -- 2024-11-08 08:56:52 Hello { foo = \"bar\" } 42\n-- ```\nfunction M.log(...)\n  local file = \"./debug.log\"\n  local fd = io.open(file, \"a+\")\n  if not fd then\n    error((\"Could not open file %s for writing\"):format(file))\n  end\n  local c = select(\"#\", ...)\n  local parts = {} ---@type string[]\n  for i = 1, c do\n    local v = select(i, ...)\n    parts[i] = type(v) == \"string\" and v or vim.inspect(v)\n  end\n  local msg = table.concat(parts, \" \")\n  msg = #msg < 120 and msg:gsub(\"%s+\", \" \") or msg\n  fd:write(os.date(\"%Y-%m-%d %H:%M:%S \") .. msg)\n  fd:write(\"\\n\")\n  fd:close()\nend\n\n---@alias snacks.debug.Trace {name: string, time: number, [number]:snacks.debug.Trace}\n---@alias snacks.debug.Stat {name:string, time:number, count?:number, depth?:number}\n\n---@type snacks.debug.Trace[]\nM._traces = { { name = \"__TOP__\", time = 0 } }\n\n---@param name string?\nfunction M.trace(name)\n  if name then\n    local entry = { name = name, time = uv.hrtime() } ---@type snacks.debug.Trace\n    table.insert(M._traces[#M._traces], entry)\n    table.insert(M._traces, entry)\n    return entry\n  else\n    local entry = assert(table.remove(M._traces), \"trace not ended?\") ---@type snacks.debug.Trace\n    entry.time = uv.hrtime() - entry.time\n    return entry\n  end\nend\n\n---@param modname string\n---@param mod? table\n---@param suffix? string\nfunction M.tracemod(modname, mod, suffix)\n  mod = mod or require(modname)\n  suffix = suffix or \".\"\n  for k, v in pairs(mod) do\n    if type(v) == \"function\" and k ~= \"trace\" then\n      mod[k] = function(...)\n        M.trace(modname .. suffix .. k)\n        local ok, ret = pcall(v, ...)\n        M.trace()\n        return ok == false and error(ret) or ret\n      end\n    end\n  end\nend\n\n---@param opts? {min?: number, show?:boolean}\n---@return {summary:table<string, snacks.debug.Stat>, trace:snacks.debug.Stat[], traces:snacks.debug.Trace[]}\nfunction M.stats(opts)\n  opts = opts or {}\n  local stack, lines, trace = {}, {}, {} ---@type string[], string[], snacks.debug.Stat[]\n  local summary = {} ---@type table<string, snacks.debug.Stat>\n  ---@param stat snacks.debug.Trace\n  local function collect(stat)\n    if #stack > 0 then\n      local recursive = vim.list_contains(stack, stat.name)\n      summary[stat.name] = summary[stat.name] or { time = 0, count = 0, name = stat.name }\n      summary[stat.name].time = summary[stat.name].time + (recursive and 0 or stat.time)\n      summary[stat.name].count = summary[stat.name].count + 1\n      table.insert(trace, { name = stat.name, time = stat.time or 0, depth = #stack - 1 })\n    end\n    table.insert(stack, stat.name)\n    for _, entry in ipairs(stat) do\n      collect(entry)\n    end\n    table.remove(stack)\n  end\n  collect(M._traces[1])\n\n  ---@param entries snacks.debug.Stat[]\n  local function add(entries)\n    for _, stat in ipairs(entries) do\n      local ms = math.floor(stat.time / 1e4) / 1e2\n      if ms >= (opts.min or 0) then\n        local line = (\"%s- `%s`: **%.2f**ms\"):format((\"  \"):rep(stat.depth or 0), stat.name, ms)\n        table.insert(lines, line .. (stat.count and (\" ([%d])\"):format(stat.count) or \"\"))\n      end\n    end\n  end\n\n  if opts.show ~= false then\n    lines[#lines + 1] = \"# Summary\"\n    summary = vim.tbl_values(summary)\n    table.sort(summary, function(a, b)\n      return a.time > b.time\n    end)\n    add(summary)\n    lines[#lines + 1] = \"\\n# Trace\"\n    add(trace)\n    Snacks.notify.warn(lines, { title = \"Traces\" })\n  end\n  return { summary = summary, trace = trace, tree = M._traces }\nend\n\nfunction M.size(bytes)\n  local sizes = { \"B\", \"KB\", \"MB\", \"GB\", \"TB\" }\n  local s = 1\n  while bytes > 1024 and s < #sizes do\n    bytes = bytes / 1024\n    s = s + 1\n  end\n  return (\"%.2f%s\"):format(bytes, sizes[s])\nend\n\nfunction M.metrics()\n  collectgarbage(\"collect\")\n  local lines = {} ---@type string[]\n  local function add(name, value)\n    lines[#lines + 1] = (\"- **%s**: %s\"):format(name, value)\n  end\n\n  add(\"lua\", M.size(collectgarbage(\"count\") * 1024))\n\n  for _, stat in ipairs({ \"get_total_memory\", \"get_free_memory\", \"get_available_memory\", \"resident_set_memory\" }) do\n    add(stat:gsub(\"get_\", \"\"):gsub(\"_\", \" \"), M.size(uv[stat]()))\n  end\n  lines[#lines + 1] = (\"```lua\\n%s\\n```\"):format(vim.inspect(uv.getrusage()))\n  Snacks.notify.warn(lines, { title = \"Metrics\" })\nend\n\n---@param opts snacks.debug.cmd\nfunction M.cmd(opts)\n  local cmd = opts.cmd\n  local args = vim.deepcopy(opts.args or {})\n  if type(cmd) == \"table\" then\n    vim.list_extend(args, cmd, 2)\n    cmd = cmd[1]\n  end\n  args = vim.tbl_map(tostring, args)\n  ---@cast cmd string\n  local lines = { cmd } ---@type string[]\n  for _, arg in ipairs(args or {}) do\n    arg = arg:find(\"[%$%s%?]\") and vim.fn.shellescape(arg) or arg\n    if #arg + #lines[#lines] > 40 then\n      lines[#lines] = lines[#lines] .. \" \\\\\"\n      table.insert(lines, \"  \" .. arg)\n    else\n      lines[#lines] = lines[#lines] .. \" \" .. arg\n    end\n  end\n  local props = vim.deepcopy(opts.props or {})\n  props.cwd = props.cwd or vim.fn.fnamemodify(opts.cwd or uv.cwd() or \".\", \":~\")\n  local prop_keys = vim.tbl_keys(props) ---@type string[]\n  table.sort(prop_keys)\n  local prop_lines = {} ---@type string[]\n  for _, key in ipairs(prop_keys) do\n    table.insert(prop_lines, (\"- **%s**: %s\"):format(key, props[key]))\n  end\n\n  local id = cmd or \"cmd\"\n  lines = {\n    opts.header or \"\",\n    table.concat(prop_lines, \"\\n\"),\n    \"```sh\",\n    table.concat(lines, \" \\n\"),\n    \"```\",\n    opts.footer or \"\",\n  }\n  if opts.title and not opts.notify then\n    table.insert(lines, 1, (\"# %s\\n\"):format(opts.title))\n  end\n  local msg = vim.trim(table.concat(lines, \"\\n\")):gsub(\"\\n\\n+\", \"\\n\\n\")\n  if opts.notify ~= false then\n    Snacks.notify(msg, {\n      id = opts.group and (\"snacks.debug.cmd.\" .. id) or nil,\n      level = opts.level or vim.log.levels.INFO,\n      title = opts.title or \"Cmd Debug\",\n    })\n  end\n  return msg\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/dim.lua",
    "content": "---@class snacks.dim\n---@overload fun(opts: snacks.dim.Config)\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.enable(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Focus on the active scope by dimming the rest\",\n}\n\n---@class snacks.dim.Config\nlocal defaults = {\n  ---@type snacks.scope.Config\n  scope = {\n    min_size = 5,\n    max_size = 20,\n    siblings = true,\n  },\n  -- animate scopes. Enabled by default for Neovim >= 0.10\n  -- Works on older versions but has to trigger redraws during animation.\n  ---@type snacks.animate.Config|{enabled?: boolean}\n  animate = {\n    enabled = vim.fn.has(\"nvim-0.10\") == 1,\n    easing = \"outQuad\",\n    duration = {\n      step = 20, -- ms per step\n      total = 300, -- maximum duration\n    },\n  },\n  -- what buffers to dim\n  filter = function(buf)\n    return vim.g.snacks_dim ~= false and vim.b[buf].snacks_dim ~= false and vim.bo[buf].buftype == \"\"\n  end,\n}\n\nM.enabled = false\nlocal ns = vim.api.nvim_create_namespace(\"snacks_dim\")\nlocal scopes ---@type snacks.scope.Listener?\nlocal scopes_anim = {} ---@type table<number, {from:number, to:number, buf:number}>\n\nSnacks.util.set_hl({\n  [\"\"] = \"DiagnosticUnnecessary\",\n}, { prefix = \"SnacksDim\", default = true })\n\n--- Called during every redraw cycle, so it should be fast.\n--- Everything that can be cached should be cached.\n---@param win number\n---@param buf number\n---@param top number -- 1-indexed\n---@param bottom number -- 1-indexed\n---@private\nfunction M.on_win(win, buf, top, bottom)\n  local scope = scopes and scopes:get(win)\n  if not scope then\n    return\n  end\n  local function add(l)\n    vim.api.nvim_buf_set_extmark(buf, ns, l - 1, 0, {\n      end_row = l,\n      end_col = 0,\n      hl_group = \"SnacksDim\",\n      ephemeral = true,\n    })\n  end\n  local animating = Snacks.animate.enabled({ buf = buf, name = \"dim\" })\n  local from = animating and scopes_anim[win] and scopes_anim[win].from or scope.from\n  local to = animating and scopes_anim[win] and scopes_anim[win].to or scope.to\n  for l = top, math.min(from - 1, bottom) do\n    add(l)\n  end\n  for l = math.max(to + 1, top), bottom do\n    add(l)\n  end\nend\n\n---@param opts? snacks.dim.Config\nfunction M.enable(opts)\n  if M.enabled then\n    return\n  end\n  opts = Snacks.config.get(\"dim\", defaults, opts)\n\n  M.enabled = true\n\n  vim.g.snacks_animate_dim = opts.animate.enabled\n\n  -- setup decoration provider\n  vim.api.nvim_set_decoration_provider(ns, {\n    on_win = function(_, win, buf, top, bottom)\n      if M.enabled and opts.filter(buf) then\n        M.on_win(win, buf, top + 1, bottom + 1)\n      end\n    end,\n  })\n\n  scopes = scopes\n    or Snacks.scope.attach(function(win, buf, scope)\n      if not Snacks.animate.enabled({ buf = buf, name = \"dim\" }) then\n        Snacks.util.redraw(win)\n      else\n        if not (scopes_anim[win] and scopes_anim[win].buf == buf) then\n          local info = vim.fn.getwininfo(win)[1]\n          scopes_anim[win] = {\n            from = info.topline,\n            to = info.botline,\n            buf = buf,\n          }\n        end\n        if scope == nil then\n          return\n        end\n        Snacks.animate(scopes_anim[win].from, scope.from, function(v)\n          if not scopes_anim[win] or not vim.api.nvim_win_is_valid(win) then\n            return\n          end\n          scopes_anim[win].from = v\n          Snacks.util.redraw(win)\n        end, vim.tbl_extend(\"keep\", { int = true, id = \"snacks_dim_from_\" .. win, buf = buf }, opts.animate))\n\n        Snacks.animate(scopes_anim[win].to, scope.to, function(v)\n          if not scopes_anim[win] or not vim.api.nvim_win_is_valid(win) then\n            return\n          end\n          scopes_anim[win].to = v\n          Snacks.util.redraw(win)\n        end, vim.tbl_extend(\"keep\", { int = true, id = \"snacks_dim_to_\" .. win, buf = buf }, opts.animate))\n      end\n    end, opts.scope)\n  if not scopes.enabled then\n    scopes:enable()\n  end\nend\n\n-- Disable dimming\nfunction M.disable()\n  if not M.enabled then\n    return\n  end\n  M.enabled = false\n  if scopes and scopes.enabled then\n    scopes:disable()\n  end\n  scopes_anim = {}\n  vim.cmd([[redraw!]])\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/explorer/actions.lua",
    "content": "local Git = require(\"snacks.explorer.git\")\nlocal Tree = require(\"snacks.explorer.tree\")\n\n---@class snacks.explorer.diagnostic.Action: snacks.picker.Action\n---@field severity? number\n---@field up? boolean\n\nlocal uv = vim.uv or vim.loop\n\nlocal M = {}\n\n---@param path string\nfunction M.get_trash_cmds(path)\n  ---@type string[][]\n  local ret = {\n    { \"trash\", path }, -- trash-cli (Python or Node.js)\n    { \"gio\", \"trash\", path }, -- Most universally available on modern Linux\n    { \"kioclient5\", \"move\", path, \"trash:/\" }, -- KDE Plasma 5\n    { \"kioclient\", \"move\", path, \"trash:/\" }, -- KDE Plasma 6\n  }\n  if vim.fn.has(\"win32\") == 1 then\n    ret[#ret + 1] = {\n      \"powershell\",\n      \"-NoProfile\",\n      \"-Command\",\n      (\n        \"Add-Type -AssemblyName Microsoft.VisualBasic; \"\n        .. \"[Microsoft.VisualBasic.FileIO.FileSystem]::\"\n        .. (vim.fn.isdirectory(path) == 0 and \"DeleteFile\" or \"DeleteDirectory\")\n        .. \"('%s','OnlyErrorDialogs', 'SendToRecycleBin')\"\n      ):format(path:gsub(\"\\\\\", \"\\\\\\\\\"):gsub(\"'\", \"''\")),\n    }\n  end\n  return ret\nend\n\n---@param path string\nfunction M.trash(path)\n  if Snacks.explorer.config.trash then\n    for _, cmd in ipairs(M.get_trash_cmds(path)) do\n      if vim.fn.executable(cmd[1]) == 1 then\n        local ok, ret = pcall(vim.fn.system, cmd)\n        if not ok or vim.v.shell_error ~= 0 then\n          return false,\n            (\"- cmd: `%s`\\n- error: %s\"):format(\n              table.concat(cmd, \" \"),\n              type(ret) == \"string\" and ret or \"Unknown error\"\n            )\n        end\n        return true\n      end\n    end\n  end\n\n  -- Fallback to delete\n  local ok, ret = pcall(vim.fn.delete, path, \"rf\")\n  if not ok or ret ~= 0 then\n    return false, type(ret) == \"string\" and ret or \"Unknown error\"\n  end\n  return true\nend\n\n---@param picker snacks.Picker\n---@param path string\nfunction M.reveal(picker, path)\n  if picker.closed then\n    return\n  end\n  for item, idx in picker:iter() do\n    if item.file == path then\n      picker.list:view(idx)\n      return true\n    end\n  end\nend\n\n---@param picker snacks.Picker\n---@param opts? {target?: boolean|string, refresh?: boolean}\nfunction M.update(picker, opts)\n  opts = opts or {}\n  local cwd = picker:cwd()\n  local target = type(opts.target) == \"string\" and opts.target or nil --[[@as string]]\n  local refresh = opts.refresh or Tree:is_dirty(cwd, picker.opts)\n  if target and not Tree:is_visible(cwd, target) then\n    Tree:open(target)\n    refresh = true\n  end\n\n  -- when searching, restore explorer view first\n  if picker.input.filter.meta.searching then\n    picker.input:set(\"\", \"\")\n    picker.list.win:focus()\n    refresh = true\n  end\n\n  if not refresh and target then\n    return M.reveal(picker, target)\n  end\n  if opts.target ~= false then\n    picker.list:set_target()\n  end\n  picker:find({\n    on_done = function()\n      if target then\n        M.reveal(picker, target)\n      end\n    end,\n  })\nend\n\n---@class snacks.explorer.actions\n---@field [string] snacks.picker.Action.spec\nM.actions = {}\n\nfunction M.actions.explorer_focus(picker)\n  picker:set_cwd(picker:dir())\n  picker:find()\nend\n\nfunction M.actions.explorer_open(_, item)\n  if item then\n    local _, err = vim.ui.open(item.file)\n    if err then\n      Snacks.notify.error(\"Failed to open `\" .. item.file .. \"`:\\n- \" .. err)\n    end\n  end\nend\n\nfunction M.actions.explorer_yank(picker)\n  local files = {} ---@type string[]\n  if vim.fn.mode():find(\"^[vV]\") then\n    picker.list:select()\n  end\n  for _, item in ipairs(picker:selected({ fallback = true })) do\n    table.insert(files, Snacks.picker.util.path(item))\n  end\n  picker.list:set_selected() -- clear selection\n  local value = table.concat(files, \"\\n\")\n  vim.fn.setreg(vim.v.register or \"+\", value, \"l\")\n  Snacks.notify.info(\"Yanked \" .. #files .. \" files\")\nend\n\nfunction M.actions.explorer_up(picker)\n  picker:set_cwd(vim.fs.dirname(picker:cwd()))\n  picker:find()\nend\n\nfunction M.actions.explorer_close(picker, item)\n  if not item then\n    return\n  end\n  local dir = picker:dir()\n  if item.dir and not item.open then\n    dir = vim.fs.dirname(dir)\n  end\n  Tree:close(dir)\n  M.update(picker, { target = dir, refresh = true })\nend\n\nfunction M.actions.explorer_update(picker)\n  Tree:refresh(picker:cwd())\n  M.update(picker)\nend\n\nfunction M.actions.explorer_close_all(picker)\n  Tree:close_all(picker:cwd())\n  M.update(picker, { refresh = true })\nend\n\nfunction M.actions.explorer_git_next(picker, item)\n  local node = Git.next(picker:cwd(), item and item.file)\n  if node then\n    M.update(picker, { target = node.path })\n  end\nend\n\nfunction M.actions.explorer_paste(picker)\n  local files = vim.split(vim.fn.getreg(vim.v.register or \"+\") or \"\", \"\\n\", { plain = true })\n  files = vim.tbl_filter(function(file)\n    return file ~= \"\" and vim.fn.filereadable(file) == 1\n  end, files)\n\n  if #files == 0 then\n    return Snacks.notify.warn((\"The `%s` register does not contain any files\"):format(vim.v.register or \"+\"))\n  end\n  local dir = picker:dir()\n  Snacks.picker.util.copy(files, dir)\n  Tree:refresh(dir)\n  Tree:open(dir)\n  M.update(picker, { target = dir })\nend\n\nfunction M.actions.explorer_git_prev(picker, item)\n  local node = Git.next(picker:cwd(), item and item.file, true)\n  if node then\n    M.update(picker, { target = node.path })\n  end\nend\n\nfunction M.actions.explorer_add(picker)\n  Snacks.input({\n    prompt = 'Add a new file or directory (directories end with a \"/\")',\n  }, function(value)\n    if not value or value:find(\"^%s$\") then\n      return\n    end\n    local path = svim.fs.normalize(picker:dir() .. \"/\" .. value)\n    local is_file = value:sub(-1) ~= \"/\"\n    local dir = is_file and vim.fs.dirname(path) or path\n    if is_file and uv.fs_stat(path) then\n      Snacks.notify.warn(\"File already exists:\\n- `\" .. path .. \"`\")\n      return\n    end\n    vim.fn.mkdir(dir, \"p\")\n    if is_file then\n      io.open(path, \"w\"):close()\n    end\n    Tree:open(dir)\n    Tree:refresh(dir)\n    M.update(picker, { target = path })\n  end)\nend\n\nfunction M.actions.explorer_rename(picker, item)\n  if not item then\n    return\n  end\n  Snacks.rename.rename_file({\n    from = item.file,\n    on_rename = function(new, old)\n      Tree:refresh(vim.fs.dirname(old))\n      Tree:refresh(vim.fs.dirname(new))\n      M.update(picker, { target = new })\n    end,\n  })\nend\n\nfunction M.actions.explorer_move(picker)\n  ---@type string[]\n  local paths = vim.tbl_map(Snacks.picker.util.path, picker:selected())\n  if #paths == 0 then\n    Snacks.notify.warn(\"No files selected to move. Renaming instead.\")\n    return M.actions.explorer_rename(picker, picker:current())\n  end\n  local target = picker:dir()\n  local what = #paths == 1 and vim.fn.fnamemodify(paths[1], \":p:~:.\") or #paths .. \" files\"\n  local t = vim.fn.fnamemodify(target, \":p:~:.\")\n\n  Snacks.picker.util.confirm(\"Move \" .. what .. \" to \" .. t .. \"?\", function()\n    for _, from in ipairs(paths) do\n      local to = target .. \"/\" .. vim.fn.fnamemodify(from, \":t\")\n      Snacks.rename.rename_file({ from = from, to = to })\n      Tree:refresh(vim.fs.dirname(from))\n    end\n    Tree:refresh(target)\n    picker.list:set_selected() -- clear selection\n    M.update(picker, { target = target })\n  end)\nend\n\nfunction M.actions.explorer_copy(picker, item)\n  if not item then\n    return\n  end\n  ---@type string[]\n  local paths = vim.tbl_map(Snacks.picker.util.path, picker:selected())\n  -- Copy selection\n  if #paths > 0 then\n    local dir = picker:dir()\n    Snacks.picker.util.copy(paths, dir)\n    picker.list:set_selected() -- clear selection\n    Tree:refresh(dir)\n    Tree:open(dir)\n    M.update(picker, { target = dir })\n    return\n  end\n  Snacks.input({\n    prompt = \"Copy to\",\n  }, function(value)\n    if not value or value:find(\"^%s$\") then\n      return\n    end\n    local dir = vim.fs.dirname(item.file)\n    local to = svim.fs.normalize(dir .. \"/\" .. value)\n    if uv.fs_stat(to) then\n      Snacks.notify.warn(\"File already exists:\\n- `\" .. to .. \"`\")\n      return\n    end\n    Snacks.picker.util.copy_path(item.file, to)\n    Tree:refresh(vim.fs.dirname(to))\n    M.update(picker, { target = to })\n  end)\nend\n\nfunction M.actions.explorer_del(picker)\n  ---@type string[]\n  local paths = vim.tbl_map(Snacks.picker.util.path, picker:selected({ fallback = true }))\n  if #paths == 0 then\n    return\n  end\n  local what = #paths == 1 and vim.fn.fnamemodify(paths[1], \":p:~:.\") or #paths .. \" files\"\n  Snacks.picker.util.confirm(\"Delete \" .. what .. \"?\", function()\n    for _, path in ipairs(paths) do\n      local ok, err = M.trash(path)\n      if ok then\n        Snacks.bufdelete({ file = path, force = true })\n      else\n        Snacks.notify.error(\"Failed to delete `\" .. path .. \"`:\\n\" .. err)\n      end\n      Tree:refresh(vim.fs.dirname(path))\n    end\n    picker.list:set_selected() -- clear selection\n    M.update(picker)\n  end)\nend\n\nfunction M.actions.confirm(picker, item, action)\n  if not item then\n    return\n  elseif picker.input.filter.meta.searching then\n    M.update(picker, { target = item.file })\n  elseif item.dir then\n    Tree:toggle(item.file)\n    M.update(picker, { refresh = true })\n  else\n    Snacks.picker.actions.jump(picker, item, action)\n  end\nend\n\nfunction M.actions.explorer_diagnostic(picker, item, action)\n  ---@cast action snacks.explorer.diagnostic.Action\n  local node = Tree:next(picker:cwd(), function(node)\n    if not node.severity then\n      return false\n    end\n    return action.severity == nil or node.severity == action.severity\n  end, { up = action.up, path = item and item.file })\n  if node then\n    M.update(picker, { target = node.path })\n  end\nend\n\nM.actions.explorer_diagnostic_next = { action = \"explorer_diagnostic\" }\nM.actions.explorer_diagnostic_prev = { action = \"explorer_diagnostic\", up = true }\nM.actions.explorer_warn_next = { action = \"explorer_diagnostic\", severity = vim.diagnostic.severity.WARN }\nM.actions.explorer_warn_prev = { action = \"explorer_diagnostic\", severity = vim.diagnostic.severity.WARN, up = true }\nM.actions.explorer_error_next = { action = \"explorer_diagnostic\", severity = vim.diagnostic.severity.ERROR }\nM.actions.explorer_error_prev = { action = \"explorer_diagnostic\", severity = vim.diagnostic.severity.ERROR, up = true }\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/explorer/diagnostics.lua",
    "content": "---@diagnostic disable: missing-fields\nlocal M = {}\n\n---@param cwd string\nfunction M.update(cwd)\n  local Tree = require(\"snacks.explorer.tree\")\n  local node = Tree:find(cwd)\n\n  local snapshot = Tree:snapshot(node, { \"severity\" })\n\n  Tree:walk(node, function(n)\n    n.severity = nil\n  end, { all = true })\n\n  local diags = vim.diagnostic.get()\n\n  ---@param path string\n  ---@param diag vim.Diagnostic\n  local function add(path, diag)\n    local n = Tree:find(path)\n    local severity = tonumber(diag.severity) or vim.diagnostic.severity.INFO\n    n.severity = math.min(n.severity or severity, severity)\n  end\n\n  for _, diag in ipairs(diags) do\n    local path = diag.bufnr and vim.api.nvim_buf_get_name(diag.bufnr)\n    path = path and path ~= \"\" and svim.fs.normalize(path) or nil\n    if path then\n      add(path, diag)\n      add(cwd, diag)\n      for dir in Snacks.picker.util.parents(path, cwd) do\n        add(dir, diag)\n      end\n    end\n  end\n\n  return Tree:changed(node, snapshot)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/explorer/git.lua",
    "content": "---@diagnostic disable: missing-fields\nlocal M = {}\n\n---@class snacks.explorer.git.Status\n---@field status string\n---@field file string\n\nlocal uv = vim.uv or vim.loop\n\nlocal CACHE_TTL = 15 * 60 -- 15 minutes\n\nM.state = {} ---@type table<string, {tick: number, last: number}>\n\n---@param path string\nfunction M.refresh(path)\n  for root in pairs(M.state) do\n    if path == root or path:find(root .. \"/\", 1, true) == 1 then\n      M.state[root].last = 0\n    end\n  end\nend\n\n---@param cwd string\nfunction M.is_dirty(cwd)\n  local root = Snacks.git.get_root(cwd)\n  if not root then\n    return false\n  end\n  return M.state[root] == nil or M.state[root].last == 0\nend\n\n---@param cwd string\n---@param opts? {on_update?: fun(), ttl?: number, force?: boolean, untracked?: boolean}\nfunction M.update(cwd, opts)\n  opts = opts or {}\n  local ttl = opts.ttl or CACHE_TTL\n  if opts.force then\n    ttl = 0\n  end\n  local root = Snacks.git.get_root(cwd)\n\n  if not root then\n    return M._update(cwd, {})\n  end\n  local now = os.time()\n  M.state[root] = M.state[root] or { tick = 0, last = 0 }\n  local state = M.state[root]\n  if now - state.last < ttl then\n    return\n  end\n  state.last = now\n  state.tick = state.tick + 1\n  local tick = state.tick\n\n  local output = \"\"\n  local stdout = assert(uv.new_pipe())\n  local handle ---@type uv.uv_process_t\n  handle = uv.spawn(\"git\", {\n    stdio = { nil, stdout, nil },\n    cwd = root,\n    hide = true,\n    args = {\n      \"--no-pager\",\n      \"--no-optional-locks\",\n      \"status\",\n      \"--porcelain=v1\",\n      \"--ignored=matching\",\n      \"-z\",\n      opts.untracked and \"-unormal\" or \"-uno\",\n    },\n  }, function()\n    handle:close()\n  end)\n\n  if not handle then\n    return M._update(cwd, {})\n  end\n\n  local function process()\n    if not M.state[root] or M.state[root].tick ~= tick then\n      return\n    end\n    local ret = {} ---@type snacks.explorer.git.Status[]\n    for _, line in ipairs(vim.split(output, \"\\0\")) do\n      if line ~= \"\" then\n        local status, file = line:match(\"^(..) (.+)$\")\n        if status then\n          ret[#ret + 1] = {\n            status = status,\n            file = root .. \"/\" .. file,\n          }\n        end\n      end\n    end\n    if M._update(cwd, ret) and opts and opts.on_update then\n      vim.schedule(opts.on_update)\n    end\n  end\n\n  stdout:read_start(function(err, data)\n    assert(not err, err)\n    if data then\n      output = output .. data\n    else\n      process()\n      stdout:close()\n    end\n  end)\nend\n\n---@param cwd string\n---@param results snacks.explorer.git.Status[]\nfunction M._update(cwd, results)\n  local Tree = require(\"snacks.explorer.tree\")\n  local Git = require(\"snacks.picker.source.git\")\n  local node = Tree:find(cwd)\n\n  local snapshot = Tree:snapshot(node, { \"status\", \"ignored\" })\n\n  Tree:walk(node, function(n)\n    n.status = nil\n    n.ignored = nil\n  end, { all = true })\n\n  ---@param path string\n  ---@param status string\n  local function add_git_status(path, status)\n    local n = Tree:find(path)\n    n.status = n.status and Git.merge_status(n.status, status) or status\n    if status:sub(1, 1) == \"!\" then\n      n.ignored = true\n    end\n  end\n\n  if vim.fn.isdirectory(cwd .. \"/.git\") == 1 then\n    add_git_status(cwd .. \"/.git\", \"!!\")\n  end\n\n  for _, s in ipairs(results) do\n    local is_dir = s.file:sub(-1) == \"/\"\n    local path = is_dir and s.file:sub(1, -2) or s.file\n    local deleted = s.status:find(\"D\") and s.status ~= \"UD\"\n    if not deleted then\n      add_git_status(path, s.status)\n    end\n    if is_dir then\n      local n = Tree:find(path)\n      n.dir_status = s.status\n    end\n    if s.status:sub(1, 1) ~= \"!\" then -- don't propagate ignored status\n      add_git_status(cwd, s.status)\n      for dir in Snacks.picker.util.parents(path, cwd) do\n        if not s.status:find(\"^.D$\") or vim.fn.isdirectory(dir) == 1 then\n          -- only propagate if not deleted or still exists\n          add_git_status(dir, s.status)\n        end\n      end\n    end\n  end\n  return Tree:changed(node, snapshot)\nend\n\n---@param cwd string\n---@param path? string\n---@param up? boolean\nfunction M.next(cwd, path, up)\n  local Tree = require(\"snacks.explorer.tree\")\n  return Tree:next(cwd, function(node)\n    return node.status ~= nil\n  end, { up = up, path = path })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/explorer/init.lua",
    "content": "---@class snacks.explorer\n---@overload fun(opts?: snacks.picker.explorer.Config): snacks.Picker\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.open(...)\n  end,\n})\n\nM.meta = {\n  desc = \"A file explorer (picker in disguise)\",\n  needs_setup = true,\n}\n\n--- These are just the general explorer settings.\n--- To configure the explorer picker, see `snacks.picker.explorer.Config`\n---@class snacks.explorer.Config\nlocal defaults = {\n  replace_netrw = true, -- Replace netrw with the snacks explorer\n  trash = true, -- Use the system trash when deleting files\n}\n\nM.config = Snacks.config.get(\"explorer\", defaults)\n\n---@private\n---@param event? vim.api.keyset.create_autocmd.callback_args\nfunction M.setup(event)\n  if M.config.replace_netrw then\n    -- Disable netrw\n    pcall(vim.api.nvim_del_augroup_by_name, \"FileExplorer\")\n\n    local group = vim.api.nvim_create_augroup(\"snacks.explorer\", { clear = true })\n\n    local function handle(ev)\n      if ev.file ~= \"\" and vim.fn.isdirectory(ev.file) == 1 then\n        local picker = M.open({ cwd = ev.file })\n        if picker and vim.v.vim_did_enter == 0 then\n          -- clear bufname so we don't try loading this one again\n          vim.api.nvim_buf_set_name(ev.buf, \"\")\n          picker:show()\n          local ref = picker:ref()\n          -- focus on UIEnter, since focusing before doesn't work\n          vim.api.nvim_create_autocmd(\"UIEnter\", {\n            once = true,\n            group = group,\n            callback = function()\n              local p = ref()\n              if p then\n                p:focus()\n              end\n            end,\n          })\n        else\n          -- after vim has entered, we also need to delete the directory buffer\n          -- use bufdelete to keep the window layout\n          Snacks.bufdelete.delete(ev.buf)\n        end\n      end\n    end\n\n    -- event from snacks loader\n    if event then\n      handle(event)\n    end\n\n    -- Open the explorer when opening a directory\n    vim.api.nvim_create_autocmd(\"BufEnter\", {\n      group = group,\n      callback = handle,\n    })\n  end\nend\n\n--- Shortcut to open the explorer picker\n---@param opts? snacks.picker.explorer.Config|{}\nfunction M.open(opts)\n  return Snacks.picker.explorer(opts)\nend\n\n--- Reveals the given file/buffer or the current buffer in the explorer\n---@param opts? {file?:string, buf?:number}\nfunction M.reveal(opts)\n  local Actions = require(\"snacks.explorer.actions\")\n  local Tree = require(\"snacks.explorer.tree\")\n  opts = opts or {}\n  local file = svim.fs.normalize(opts.file or vim.api.nvim_buf_get_name(opts.buf or 0))\n  local explorer = Snacks.picker.get({ source = \"explorer\" })[1]\n\n  local function reveal()\n    local cwd = explorer:cwd()\n    if not Tree:in_cwd(cwd, file) then\n      for parent in vim.fs.parents(file) do\n        if Tree:in_cwd(parent, cwd) then\n          explorer:set_cwd(parent)\n          break\n        end\n      end\n    end\n    Tree:open(file)\n    Actions.update(explorer, { target = file, refresh = true })\n  end\n\n  if explorer then\n    reveal()\n  else\n    explorer = M.open({ on_show = reveal })\n  end\n  return explorer\nend\n\nfunction M.health()\n  local cmds = require(\"snacks.explorer.actions\").get_trash_cmds(\"test\")\n\n  if M.config.trash == false then\n    Snacks.health.ok(\"System trash disabled in config\")\n  else\n    local tools = vim.tbl_map(function(cmd)\n      return cmd[1]\n    end, cmds)\n    if Snacks.health.have_tool(tools) then\n      Snacks.health.ok(\"System trash command found\")\n    else\n      Snacks.health.warn(\"No system trash command found; deleting files will be permanent\")\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/explorer/tree.lua",
    "content": "---@class snacks.picker.explorer.Node\n---@field path string\n---@field name string\n---@field hidden? boolean\n---@field status? string merged git status\n---@field dir_status? string git status of the directory\n---@field ignored? boolean\n---@field type \"file\"|\"directory\"|\"link\"|\"fifo\"|\"socket\"|\"char\"|\"block\"|\"unknown\"\n---@field dir? boolean\n---@field open? boolean wether the node should be expanded (only for directories)\n---@field expanded? boolean wether the node is expanded (only for directories)\n---@field parent? snacks.picker.explorer.Node\n---@field last? boolean child of the parent\n---@field utime? number\n---@field children table<string, snacks.picker.explorer.Node>\n---@field severity? number\n\n---@class snacks.picker.explorer.Filter\n---@field hidden? boolean show hidden files\n---@field ignored? boolean show ignored files\n---@field exclude? string[] globs to exclude\n---@field include? string[] globs to exclude\n\n---@alias snacks.picker.explorer.Snapshot {fields: string[], state:table<snacks.picker.explorer.Node, any[]>}\n\nlocal uv = vim.uv or vim.loop\n\nlocal function norm(path)\n  return svim.fs.normalize(path):gsub(\"/$\", \"\"):gsub(\"^$\", \"/\")\nend\n\nlocal function assert_dir(path)\n  assert(vim.fn.isdirectory(path) == 1, \"Not a directory: \" .. path)\nend\n\n-- local function assert_file(path)\n--   assert(vim.fn.filereadable(path) == 1, \"Not a file: \" .. path)\n-- end\n\n---@class snacks.picker.explorer.Tree\n---@field root snacks.picker.explorer.Node\n---@field nodes table<string, snacks.picker.explorer.Node>\nlocal Tree = {}\nTree.__index = Tree\n\nfunction Tree.new()\n  local self = setmetatable({}, Tree)\n  self.root = { name = \"\", children = {}, dir = true, type = \"directory\", path = \"\" }\n  self.nodes = {}\n  return self\nend\n\n---@param path string\n---@return snacks.picker.explorer.Node?\nfunction Tree:node(path)\n  path = norm(path)\n  return self.nodes[norm(path)]\nend\n\n---@param path string\nfunction Tree:find(path)\n  path = norm(path)\n  if self.nodes[path] then\n    return self.nodes[path]\n  end\n\n  local node = self.root\n  local parts = vim.split(path, \"/\", { plain = true })\n  local is_dir = vim.fn.isdirectory(path) == 1\n  for p, part in ipairs(parts) do\n    node = self:child(node, part, (is_dir or p < #parts) and \"directory\" or \"file\")\n  end\n  return node\nend\n\n---@param node snacks.picker.explorer.Node\n---@param name string\n---@param type string\nfunction Tree:child(node, name, type)\n  if not node.children[name] then\n    local path = node.path .. \"/\" .. name\n    path = node == self.root and name or path\n    node.children[name] = {\n      name = name,\n      path = path,\n      parent = node,\n      children = {},\n      type = type,\n      dir = type == \"directory\" or (type == \"link\" and vim.fn.isdirectory(path) == 1),\n      hidden = name:sub(1, 1) == \".\",\n    }\n    self.nodes[path] = node.children[name]\n  end\n  return node.children[name]\nend\n\n---@param path string\nfunction Tree:open(path)\n  local dir = self:dir(path)\n  local node = self:find(dir)\n  while node do\n    node.open = true\n    node = node.parent\n  end\nend\n\n---@param path string\nfunction Tree:toggle(path)\n  local dir = self:dir(path)\n  local node = self:find(dir)\n  if node.open then\n    self:close(dir)\n  else\n    self:open(dir)\n  end\nend\n\n---@param path string\nfunction Tree:show(path)\n  self:open(vim.fs.dirname(path))\nend\n\n---@param path string\nfunction Tree:close(path)\n  local dir = self:dir(path)\n  local node = self:find(dir)\n  node.open = false\n  node.expanded = false -- clear expanded state\nend\n\n---@param node snacks.picker.explorer.Node\nfunction Tree:expand(node)\n  if node.expanded then\n    return\n  end\n  local found = {} ---@type table<string, boolean>\n  assert(node.dir, \"Can only expand directories\")\n  local fs = uv.fs_scandir(node.path)\n  while fs do\n    local name, t = uv.fs_scandir_next(fs)\n    if not name then\n      break\n    end\n    t = t or Snacks.util.path_type(node.path .. \"/\" .. name)\n    found[name] = true\n    local child = self:child(node, name, t)\n    child.type = t\n    child.dir = t == \"directory\" or (t == \"link\" and vim.fn.isdirectory(child.path) == 1)\n  end\n  for name in pairs(node.children) do\n    if not found[name] then\n      node.children[name] = nil\n    end\n  end\n  node.expanded = true\n  node.utime = uv.hrtime()\nend\n\n---@param path string\nfunction Tree:dir(path)\n  return vim.fn.isdirectory(path) == 1 and path or vim.fs.dirname(path)\nend\n\n---@param path string\nfunction Tree:refresh(path)\n  local dir = self:dir(path)\n  require(\"snacks.explorer.git\").refresh(dir)\n  local root = self:node(dir)\n  if not root then\n    return\n  end\n  self:walk(root, function(node)\n    node.expanded = nil\n  end, { all = true })\nend\n\n---@param node snacks.picker.explorer.Node\n---@param fn fun(node: snacks.picker.explorer.Node):boolean? return `false` to not process children, `true` to abort\n---@param opts? {all?: boolean}\nfunction Tree:walk(node, fn, opts)\n  local abort = false ---@type boolean?\n  abort = fn(node)\n  if abort ~= nil then\n    return abort\n  end\n  local children = vim.tbl_values(node.children) ---@type snacks.picker.explorer.Node[]\n  table.sort(children, function(a, b)\n    if a.dir ~= b.dir then\n      return a.dir\n    end\n    return a.name < b.name\n  end)\n  for c, child in ipairs(children) do\n    child.last = c == #children\n    abort = false\n    if child.dir and (child.open or (opts and opts.all)) then\n      abort = self:walk(child, fn, opts)\n    else\n      abort = fn(child)\n    end\n    if abort then\n      return true\n    end\n  end\n  return false\nend\n\n---@param filter snacks.picker.explorer.Filter\nfunction Tree:filter(filter)\n  local exclude = filter.exclude and #filter.exclude > 0 and Snacks.picker.util.globber(filter.exclude)\n  local include = filter.include and #filter.include > 0 and Snacks.picker.util.globber(filter.include)\n  return function(node)\n    -- takes precedence over all other filters\n    if include and include(node.path) then\n      return true\n    end\n    if node.hidden and not filter.hidden then\n      return false\n    end\n    if node.ignored and not filter.ignored then\n      return false\n    end\n    if exclude and exclude(node.path) then\n      return false\n    end\n    return true\n  end\nend\n\n---@param cwd string\n---@param cb fun(node: snacks.picker.explorer.Node)\n---@param opts? {expand?: boolean}|snacks.picker.explorer.Filter\nfunction Tree:get(cwd, cb, opts)\n  opts = opts or {}\n  assert_dir(cwd)\n  local node = self:find(cwd)\n  node.open = true\n  local filter = self:filter(opts)\n  self:walk(node, function(n)\n    if n ~= node then\n      if not filter(n) then\n        return false\n      end\n    end\n    if n.dir and n.open and not n.expanded and opts.expand ~= false then\n      self:expand(n)\n    end\n    cb(n)\n  end)\nend\n\n---@param cwd string\n---@param opts? snacks.picker.explorer.Filter\nfunction Tree:is_dirty(cwd, opts)\n  opts = opts or {}\n  if require(\"snacks.explorer.git\").is_dirty(cwd) then\n    return true\n  end\n  local dirty = false\n  self:get(cwd, function(n)\n    if n.dir and n.open and not n.expanded then\n      dirty = true\n    end\n  end, { hidden = opts.hidden, ignored = opts.ignored, exclude = opts.exclude, include = opts.include, expand = false })\n  return dirty\nend\n\n---@param cwd string\n---@param path string\nfunction Tree:in_cwd(cwd, path)\n  local dir = vim.fs.dirname(path)\n  return dir == cwd or dir:find(cwd .. \"/\", 1, true) == 1\nend\n\n---@param cwd string\n---@param path string\nfunction Tree:is_visible(cwd, path)\n  assert_dir(cwd)\n  if cwd == path then\n    return true\n  end\n  local dir = vim.fs.dirname(path)\n  if not self:in_cwd(cwd, path) then\n    return false\n  end\n  local node = self:node(dir)\n  while node do\n    if node.path == cwd then\n      return true\n    elseif not node.open then\n      return false\n    end\n    node = node.parent\n  end\n  return false\nend\n\n---@param cwd string\nfunction Tree:close_all(cwd)\n  self:walk(self:find(cwd), function(node)\n    node.open = false\n  end, { all = true })\nend\n\n---@param cwd string\n---@param filter fun(node: snacks.picker.explorer.Node):boolean?\n---@param opts? {up?: boolean, path?: string}\nfunction Tree:next(cwd, filter, opts)\n  opts = opts or {}\n  local path = opts.path or cwd\n  local root = self:node(cwd) or nil\n  if not root then\n    return\n  end\n  local first ---@type snacks.picker.explorer.Node?\n  local last ---@type snacks.picker.explorer.Node?\n  local prev ---@type snacks.picker.explorer.Node?\n  local next ---@type snacks.picker.explorer.Node?\n  local found = false\n  self:walk(root, function(node)\n    local want = not node.dir and filter(node) and not node.ignored\n    if node.path == path then\n      found = true\n    end\n    if want then\n      first, last = first or node, node\n      next = next or (found and node.path ~= path and node) or nil\n      prev = not found and node or prev\n    end\n  end, { all = true })\n  if opts.up then\n    return prev or last\n  end\n  return next or first\nend\n\n---@param node snacks.picker.explorer.Node\n---@param snapshot snacks.picker.explorer.Snapshot\nfunction Tree:changed(node, snapshot)\n  local old = snapshot.state\n  local current = self:snapshot(node, snapshot.fields).state\n  if vim.tbl_count(current) ~= vim.tbl_count(old) then\n    return true\n  end\n  for n, data in pairs(current) do\n    local prev = old[n]\n    if not prev then\n      return true\n    end\n    if not vim.deep_equal(prev, data) then\n      return true\n    end\n  end\n  return false\nend\n\n---@param node snacks.picker.explorer.Node\n---@param fields string[]\nfunction Tree:snapshot(node, fields)\n  ---@type snacks.picker.explorer.Snapshot\n  local ret = {\n    state = {},\n    fields = fields,\n  }\n  Tree:walk(node, function(n)\n    local data = {} ---@type any[]\n    for f, field in ipairs(fields) do\n      data[f] = n[field]\n    end\n    ret.state[n] = data\n  end, { all = true })\n  return ret\nend\n\nreturn Tree.new()\n"
  },
  {
    "path": "lua/snacks/explorer/watch.lua",
    "content": "local M = {}\n\nlocal Git = require(\"snacks.explorer.git\")\nlocal Tree = require(\"snacks.explorer.tree\")\n\nM._watches = {} ---@type table<string, uv.uv_fs_event_t>\n\nlocal uv = vim.uv or vim.loop\nlocal timer = assert(uv.new_timer())\n\n---@param path string\n---@param cb? fun(file:string, events: uv.fs_event_start.callback.events)\nfunction M.start(path, cb)\n  if M._watches[path] ~= nil then\n    return\n  end\n  local handle = assert(vim.uv.new_fs_event())\n  local ok, err = handle:start(path, {}, function(_, file, events)\n    if cb then\n      -- Handle nil filename (FreeBSD kqueue bug where filename may be unavailable)\n      -- In that case, we just pass the path being watched\n      cb(file and (path .. \"/\" .. file) or path, events)\n    else\n      Tree:refresh(path)\n      M.refresh()\n    end\n  end)\n  M._watches[path] = handle\n  if not ok then\n    Snacks.notify.error(\"Failed to watch \" .. path .. \": \" .. err)\n    if not handle:is_closing() then\n      handle:close()\n    end\n    return\n  end\nend\n\n---@param path string\nfunction M.stop(path)\n  local handle = M._watches[path]\n  if handle then\n    if not handle:is_closing() then\n      handle:close()\n    end\n    M._watches[path] = nil\n  end\nend\n\n-- Stop all watches\nfunction M.abort()\n  for path in pairs(M._watches) do\n    M.stop(path)\n  end\nend\n\n-- batch updates and give explorer the time to update before the watcher\nfunction M.refresh()\n  timer:start(\n    100,\n    0,\n    vim.schedule_wrap(function()\n      local pickers = Snacks.picker.get({ source = \"explorer\", tab = false })\n      for _, picker in ipairs(pickers) do\n        if picker and not picker.closed and Tree:is_dirty(picker:cwd(), picker.opts) then\n          picker.list:set_target()\n          vim.schedule(function()\n            if not picker or picker.closed then\n              return\n            end\n            picker:find()\n          end)\n        end\n      end\n    end)\n  )\nend\n\nfunction M.watch()\n  -- Track used watches\n  local used = {} ---@type table<string, boolean>\n\n  local pickers = Snacks.picker.get({ source = \"explorer\", tab = false })\n  local cwds = {} ---@type table<string, boolean>\n  for _, picker in ipairs(pickers) do\n    cwds[picker:cwd()] = true\n  end\n\n  for cwd in pairs(cwds) do\n    -- Watch git index\n    local root = Snacks.git.get_root(cwd)\n    if root then\n      used[root .. \"/.git\"] = true\n      M.start(root .. \"/.git\", function(file)\n        if vim.fs.basename(file) == \"index\" then\n          Git.refresh(root)\n          M.refresh()\n        end\n      end)\n    end\n\n    -- Watch open directories\n    Tree:walk(Tree:find(cwd), function(node)\n      if node.dir and node.open then\n        used[node.path] = true\n        M.start(node.path)\n      end\n    end)\n  end\n\n  -- Stop unused watches\n  for path in pairs(M._watches) do\n    if not used[path] then\n      M.stop(path)\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gh/actions.lua",
    "content": "local Api = require(\"snacks.gh.api\")\nlocal config = require(\"snacks.gh\").config()\n\nlocal M = {}\n\n---@class snacks.gh.action.ctx\n---@field items snacks.picker.gh.Item[]\n---@field picker? snacks.Picker\n---@field main? number\n---@field action? snacks.picker.Action\n\n---@class snacks.gh.cli.Action.ctx\n---@field item snacks.picker.gh.Item\n---@field args string[]\n---@field opts snacks.gh.cli.Action\n---@field picker? snacks.Picker\n---@field scratch? snacks.win\n---@field main? number\n---@field input? string\n\n---@alias snacks.gh.action.fn fun(item?: snacks.picker.gh.Item, ctx: snacks.gh.action.ctx)\n\n---@class snacks.gh.Action\n---@field action snacks.gh.action.fn\n---@field desc? string\n---@field name? string\n---@field priority? number\n---@field title? string -- for items\n---@field type? \"pr\" | \"issue\"\n---@field enabled? fun(item: snacks.picker.gh.Item, ctx: snacks.gh.action.ctx): boolean\n\n---@param item snacks.picker.gh.Item\n---@param ctx snacks.gh.action.ctx\nlocal function update_main(item, ctx)\n  local gh = { repo = item.repo, number = item.number, type = item.type }\n  if ctx.main and vim.api.nvim_win_is_valid(ctx.main) then\n    local buf = vim.api.nvim_win_get_buf(ctx.main)\n    if vim.deep_equal(vim.b[buf].snacks_gh or {}, gh) then\n      return ctx.main, buf\n    end\n  end\n  local win = vim.api.nvim_get_current_win()\n  local buf = vim.api.nvim_win_get_buf(win)\n  if vim.deep_equal(vim.b[buf].snacks_gh or {}, gh) then\n    ctx.main = win\n    return ctx.main, buf\n  end\nend\n\n---@param item snacks.picker.gh.Item\n---@param ctx snacks.gh.action.ctx\nlocal function get_meta(item, ctx)\n  local win, buf = update_main(item, ctx)\n  if not win or not buf then\n    return\n  end\n  local meta = Snacks.picker.highlight.meta(buf)\n  ---@type {comment_id?: number, diff?: snacks.diff.Meta}?\n  local m = meta and meta[vim.api.nvim_win_get_cursor(win)[1]] or nil\n  return m, meta, buf, win\nend\n\n---@class snacks.gh.actions: {[string]:snacks.gh.Action}\nM.actions = setmetatable({}, {\n  __index = function(_, key)\n    if type(key) ~= \"string\" then\n      return nil\n    end\n    local action = M.cli_actions[key]\n    if action then\n      local ret = M.cli_action(action)\n      rawset(M.actions, key, ret)\n      return ret\n    end\n  end,\n})\n\nM.actions.gh_diff = {\n  desc = \"View PR diff\",\n  icon = \" \",\n  priority = 100,\n  type = \"pr\",\n  title = \"View diff for PR #{number}\",\n  action = function(item, ctx)\n    if not item then\n      return\n    end\n    Snacks.picker.gh_diff({\n      show_delay = 0,\n      repo = item.repo,\n      pr = item.number,\n    })\n  end,\n}\n\nM.actions.gh_open = {\n  desc = \"Open in buffer\",\n  icon = \" \",\n  priority = 100,\n  title = \"Open {type} #{number} in buffer\",\n  action = function(item, ctx)\n    if ctx.picker then\n      return Snacks.picker.actions.jump(ctx.picker, item, ctx.action)\n    end\n  end,\n}\n\nM.actions.gh_actions = {\n  desc = \"Show available actions\",\n  action = function(item, ctx)\n    -- NOTE: this forwards split/vsplit/tab/drop actions to jump\n    if ctx.action and ctx.action.cmd then\n      return Snacks.picker.actions.jump(ctx.picker, item, ctx.action)\n    end\n    update_main(item, ctx)\n    local actions = M.get_actions(item, ctx)\n    actions.gh_actions = nil -- remove this action\n    actions.gh_perform_action = nil -- remove this action\n    Snacks.picker.gh_actions({\n      item = item,\n      layout = {\n        config = function(layout)\n          -- Fit list height to number of items, up to 10\n          for _, box in ipairs(layout.layout) do\n            if box.win == \"list\" and not box.height then\n              box.height = math.max(math.min(vim.tbl_count(actions), vim.o.lines * 0.8 - 10), 3)\n            end\n          end\n        end,\n      },\n      ---@param it snacks.picker.gh.Action\n      confirm = function(picker, it, action)\n        if not it then\n          return\n        end\n        ctx.action = action\n        if ctx.picker then\n          ctx.picker.visual = ctx.picker.visual or picker.visual or nil\n          ctx.picker:focus()\n        end\n        update_main(item, ctx)\n        it.action.action(item, ctx)\n        picker:close()\n      end,\n    })\n  end,\n}\n\nM.actions.gh_perform_action = {\n  action = function(item, ctx)\n    if not item then\n      return\n    end\n    -- pass a new context, since we're doing the action on a single item\n    item.action.action(item.item, { items = { item.item } })\n    ctx.picker:close()\n  end,\n}\n\nM.actions.gh_browse = {\n  desc = \"Open in web browser\",\n  title = \"Open {type} #{number} in web browser\",\n  icon = \" \",\n  action = function(_, ctx)\n    for _, item in ipairs(ctx.items) do\n      Api.cmd(function()\n        Snacks.notify.info((\"Opened #%s in web browser\"):format(item.number))\n      end, {\n        args = { item.type, \"view\", tostring(item.number), \"--web\" },\n        repo = item.repo,\n      })\n    end\n    if ctx.picker then\n      ctx.picker.list:set_selected() -- clear selection\n    end\n  end,\n}\n\nM.actions.gh_react = {\n  desc = \"Add reaction\",\n  icon = \" \",\n  action = function(item, ctx)\n    local reactions = { \"+1\", \"-1\", \"laugh\", \"hooray\", \"confused\", \"heart\", \"rocket\", \"eyes\" }\n    Snacks.picker.pick(\"gh_reactions\", {\n      number = item.number,\n      repo = item.repo,\n      layout = {\n        config = function(layout)\n          -- Fit list height to number of items, up to 10\n          for _, box in ipairs(layout.layout) do\n            if box.win == \"list\" and not box.height then\n              box.height = math.max(math.min(#reactions, vim.o.lines * 0.8 - 10), 3)\n            end\n          end\n        end,\n      },\n      confirm = function(picker)\n        local items = picker:selected({ fallback = true })\n        for i, it in ipairs(items) do\n          if it.added then\n            M.run(item, {\n              api = {\n                endpoint = \"/repos/{repo}/issues/{number}/reactions/\" .. it.id,\n                method = \"DELETE\",\n              },\n              refresh = i == #items,\n            }, ctx)\n          else\n            M.run(item, {\n              api = {\n                endpoint = \"/repos/{repo}/issues/{number}/reactions\",\n                fields = { content = it.reaction },\n              },\n              refresh = i == #items,\n            }, ctx)\n          end\n        end\n        picker:close()\n      end,\n    })\n  end,\n}\n\nM.actions.gh_label = {\n  desc = \"Add/Remove labels\",\n  icon = \"󰌕 \",\n  action = function(item, ctx)\n    Snacks.picker.pick(\"gh_labels\", {\n      number = item.number,\n      repo = item.repo,\n      type = item.type,\n      confirm = function(picker)\n        local labels = {} ---@type table<string, boolean>\n        for _, label in ipairs(item.item.labels or {}) do\n          labels[label.name] = true\n        end\n        for _, it in ipairs(picker:selected({ fallback = true })) do\n          labels[it.label] = not it.added or nil\n        end\n        M.run(item, {\n          api = {\n            endpoint = \"/repos/{repo}/issues/{number}/labels\",\n            method = \"PUT\",\n            input = { labels = vim.tbl_keys(labels) },\n          },\n        }, ctx)\n        picker:close()\n      end,\n    })\n  end,\n}\n\nM.actions.gh_yank = {\n  desc = \"Yank URL(s) to clipboard\",\n  icon = \" \",\n  action = function(_, ctx)\n    if vim.fn.mode():find(\"^[vV]\") and ctx.picker then\n      ctx.picker.list:select()\n    end\n    ---@param it snacks.picker.gh.Item\n    local urls = vim.tbl_map(function(it)\n      return it.url\n    end, ctx.items)\n    if ctx.picker then\n      ctx.picker.list:set_selected() -- clear selection\n    end\n    local value = table.concat(urls, \"\\n\")\n    vim.fn.setreg(vim.v.register or \"+\", value, \"l\")\n    Snacks.notify.info(\"Yanked \" .. #urls .. \" URL(s)\")\n  end,\n}\n\nM.actions.gh_reply_to_comment = {\n  desc = \"Reply to comment\",\n  title = \"Reply to comment on {type} #{number}\",\n  priority = 150,\n  icon = \" \",\n  enabled = function(item, ctx)\n    local m = get_meta(item, ctx)\n    return m and m.comment_id ~= nil or false\n  end,\n  action = function(item, ctx)\n    local action = vim.deepcopy(M.cli_actions.gh_comment)\n    local m = get_meta(item, ctx)\n    if not (m and m.comment_id) then\n      Snacks.notify.error(\"No comment found to reply to\")\n      return\n    end\n    action.title = \"Reply to comment on {type} #{number}\"\n    action.api = {\n      endpoint = \"/repos/{repo}/pulls/{number}/comments\",\n      input = { in_reply_to = m.comment_id },\n    }\n    M.run(item, action, ctx)\n  end,\n}\n\nM.actions.gh_diff_comment = {\n  desc = \"Add diff comment\",\n  title = \"Comment on diff in {type} #{number}\",\n  priority = 150,\n  icon = \" \",\n  enabled = function(item, ctx)\n    local m = get_meta(item, ctx)\n    return m and m.diff ~= nil or false\n  end,\n  action = function(item, ctx)\n    local m, meta, buf = get_meta(item, ctx)\n    if not (meta and buf and m and m.diff) then\n      Snacks.notify.error(\"No diff hunk found to comment on\")\n      return\n    end\n\n    local action = vim.deepcopy(M.cli_actions.gh_comment)\n    local visual = ctx.picker and ctx.picker.visual or Snacks.picker.util.visual()\n    visual = visual and visual.buf == buf and visual or nil\n    local line = m.diff.line ---@type number\n    local start_line ---@type number?\n    if visual then\n      local from, to = math.min(visual.pos[1], visual.end_pos[1]), math.max(visual.pos[1], visual.end_pos[1])\n      local line_diff = vim.tbl_get(meta, to, \"diff\") or m.diff --[[@as snacks.diff.Meta]]\n      local start_diff = vim.tbl_get(meta, from, \"diff\") or m.diff --[[@as snacks.diff.Meta]]\n      if line_diff.file ~= start_diff.file then\n        Snacks.notify.error(\"Cannot add comment: visual selection spans multiple files\")\n        return\n      end\n      local code = {} ---@type string[]\n      for i = from, to do\n        code[#code + 1] = vim.tbl_get(meta, i, \"diff\", \"code\") or \"\"\n      end\n      line, start_line = line_diff.line, start_diff.line\n      local ft = vim.filetype.match({ filename = m.diff.file }) or \"\"\n      local code_header = \"```\" .. (ft == \"\" and \"\" or (ft .. \" \")) .. \"suggestion\\n\"\n      action.template = (\"\\n%s%s\\n```\\n\"):format(code_header, table.concat(code, \"\\n\"))\n      action.on_submit = function(body)\n        local s, e = body:find(action.template, 1, true)\n        if s and e then -- suggestion not edited, so remove it\n          body = body:sub(1, s - 1) .. body:sub(e + 1)\n        end\n        body = body:gsub(code_header, \"```suggestion\\n\") -- remove ft from suggestion\n        return body\n      end\n    end\n    start_line = start_line ~= line and start_line or nil\n    if start_line then\n      action.title = (\"Comment on lines %s%d to %s%d\"):format(\n        m.diff.side:sub(1, 1):upper(),\n        start_line or line,\n        m.diff.side:sub(1, 1):upper(),\n        line\n      )\n    else\n      action.title = (\"Comment on line %s%d\"):format(m.diff.side:sub(1, 1):upper(), line)\n    end\n    action.api = {\n      endpoint = \"/repos/{repo}/pulls/{number}/comments\",\n      input = {\n        commit_id = item.headRefOid,\n        path = m.diff.file,\n        side = m.diff.side:upper(), -- \"RIGHT\" or \"LEFT\" (uppercase)\n        line = line,\n        start_line = start_line,\n      },\n    }\n    if item.pendingReview then\n      action.api = {\n        endpoint = \"graphql\",\n        input = {\n          -- inject: graphql\n          query = [[\n            mutation($reviewId: ID!, $body: String!, $path: String!, $line: Int!, $side: DiffSide!, $startLine: Int, $startSide: DiffSide) {\n              addPullRequestReviewThread(input: {\n                pullRequestReviewId: $reviewId\n                body: $body\n                path: $path\n                line: $line\n                side: $side\n                startLine: $startLine\n                startSide: $startSide\n              }) {\n                thread { id }\n              }\n            }\n          ]],\n          variables = {\n            reviewId = item.pendingReview.id,\n            path = m.diff.file,\n            side = m.diff.side:upper(), -- \"RIGHT\" or \"LEFT\"\n            line = line,\n            startLine = start_line,\n            startSide = start_line and m.diff.side:upper() or nil,\n          },\n        },\n      }\n    end\n    M.run(item, action, ctx)\n  end,\n}\n\nM.actions.gh_comment = {\n  desc = \"Add comment\",\n  title = \"Comment on {type} #{number}\",\n  icon = \" \",\n  action = function(item, ctx)\n    local m = get_meta(item, ctx)\n    if m and m.comment_id then\n      return M.actions.gh_reply_to_comment.action(item, ctx)\n    elseif m and m.diff then\n      return M.actions.gh_diff_comment.action(item, ctx)\n    end\n    local action = vim.deepcopy(M.cli_actions.gh_comment)\n    M.run(item, action, ctx)\n  end,\n}\n\nM.actions.gh_update_branch = {\n  icon = \"󰚰 \",\n  title = \"Update branch of PR #{number}\",\n  type = \"pr\",\n  enabled = function(item)\n    return item.state == \"open\"\n  end,\n  action = function(item, ctx)\n    Snacks.picker.select(\n      { \"1. Yes using the rebase method\", \"2. Yes using the merge method\", \"3. Cancel\" },\n      { title = \"Are you sure you want to update the brnch of PR #\" .. item.id .. \"?\" },\n      function(choice, idx)\n        if idx == 3 then\n          return\n        end\n\n        local action = vim.deepcopy(M.cli_actions.gh_update_branch)\n        if idx == 1 then\n          action.args = { \"--rebase\" }\n        end\n        M.run(item, action, ctx)\n      end\n    )\n  end,\n}\n\n-- Start a new review\nM.actions.gh_start_review = {\n  desc = \"Start a review\",\n  type = \"pr\",\n  icon = \" \",\n  priority = 100,\n  enabled = function(item)\n    return item.pendingReview == nil\n  end,\n  action = function(item, ctx)\n    M.run(item, {\n      api = {\n        endpoint = \"/repos/{repo}/pulls/{number}/reviews\",\n        input = { commit_id = item.headRefOid },\n      },\n      success = \"Started pending review for PR #{number}\",\n    }, ctx)\n  end,\n}\n\n-- Submit pending review\nM.actions.gh_submit_review = {\n  desc = \"Submit pending review\",\n  type = \"pr\",\n  icon = \" \",\n  priority = 200,\n  enabled = function(item)\n    return item.pendingReview ~= nil\n  end,\n  action = function(item, ctx)\n    local review_id = item.pendingReview.databaseId\n\n    -- Ask user: APPROVE, REQUEST_CHANGES, or COMMENT\n    Snacks.picker.select(\n      { \"Approve\", \"Request Changes\", \"Comment\" },\n      { title = \"Submit review for PR #\" .. item.number },\n      function(choice, idx)\n        if not choice then\n          return\n        end\n        local events = { \"APPROVE\", \"REQUEST_CHANGES\", \"COMMENT\" }\n        M.run(item, {\n          title = \"Submit review for PR #{number}\",\n          api = {\n            endpoint = \"/repos/{repo}/pulls/{number}/reviews/\" .. review_id .. \"/events\",\n            input = { event = events[idx] },\n          },\n          edit = \"body-file\", -- Optional summary\n          success = \"Submitted review for PR #{number}\",\n        }, ctx)\n      end\n    )\n  end,\n}\n\n---@type table<string, snacks.gh.cli.Action>\nM.cli_actions = {\n  gh_comment = {\n    cmd = \"comment\",\n    icon = \" \",\n    title = \"Comment on {type} #{number}\",\n    success = \"Commented on {type} #{number}\",\n    edit = \"body-file\",\n  },\n  gh_update_branch = {\n    cmd = \"update-branch\",\n    title = \"Update branch of PR #{number}\",\n    success = \"Branch of PR #{number} updated\",\n    type = \"pr\",\n  },\n  gh_checkout = {\n    cmd = \"checkout\",\n    icon = \" \",\n    type = \"pr\",\n    confirm = \"Are you sure you want to checkout PR #{number}?\",\n    title = \"Checkout PR #{number}\",\n    success = \"Checked out PR #{number}\",\n  },\n  gh_close = {\n    edit = \"comment\",\n    icon = config.icons.crossmark,\n    cmd = \"close\",\n    title = \"Close {type} #{number}\",\n    success = \"Closed {type} #{number}\",\n    enabled = function(item)\n      return item.state == \"open\"\n    end,\n  },\n  gh_edit = {\n    cmd = \"edit\",\n    icon = \" \",\n    fields = {\n      { arg = \"title\", prop = \"title\", name = \"Title\" },\n    },\n    success = \"Edited {type} #{number}\",\n    edit = \"body-file\",\n    template = \"{body}\",\n    title = \"Edit {type} #{number}\",\n  },\n  gh_squash = {\n    cmd = \"merge\",\n    icon = config.icons.pr.merged,\n    type = \"pr\",\n    success = \"Squashed and merged PR #{number}\",\n    args = { \"--squash\" },\n    fields = {\n      { arg = \"subject\", prop = \"title\", name = \"Title\" },\n    },\n    edit = \"body-file\",\n    confirm = \"Are you sure you want to squash and merge PR #{number}?\",\n    template = \"{body}\",\n    title = \"Squash and merge PR #{number}\",\n    enabled = function(item)\n      return item.state == \"open\"\n    end,\n  },\n  gh_merge_rebase = {\n    cmd = \"merge\",\n    icon = config.icons.pr.merged,\n    type = \"pr\",\n    success = \"Rebased and merged PR #{number}\",\n    args = { \"--rebase\" },\n    confirm = \"Are you sure you want to rebase and merge PR #{number}?\",\n    title = \"Rebase and merge PR #{number}\",\n    enabled = function(item)\n      return item.state == \"open\"\n    end,\n  },\n  gh_merge = {\n    cmd = \"merge\",\n    icon = config.icons.pr.merged,\n    type = \"pr\",\n    success = \"Merged PR #{number}\",\n    args = { \"--merge\" },\n    title = \"Merge PR #{number}\",\n    confirm = \"Are you sure you want to merge PR #{number}?\",\n    enabled = function(item)\n      return item.state == \"open\"\n    end,\n  },\n  gh_close_not_planned = {\n    cmd = \"close\",\n    icon = config.icons.crossmark,\n    type = \"issue\",\n    success = \"Closed issue #{number} as not planned\",\n    args = { \"--reason\", \"not planned\" },\n    edit = \"comment\",\n    title = \"Close issue #{number} as not planned\",\n    enabled = function(item)\n      return item.state == \"open\"\n    end,\n  },\n  gh_reopen = {\n    cmd = \"reopen\",\n    icon = \" \",\n    edit = \"comment\",\n    title = \"Reopen {type} #{number}\",\n    success = \"Reopened {type} #{number}\",\n    enabled = function(item)\n      return item.state == \"closed\"\n    end,\n  },\n  gh_ready = {\n    cmd = \"ready\",\n    icon = config.icons.pr.open,\n    type = \"pr\",\n    title = \"Mark PR #{number} as ready for review\",\n    success = \"Marked PR #{number} as ready for review\",\n    enabled = function(item)\n      return item.state == \"open\" and item.isDraft\n    end,\n  },\n  gh_draft = {\n    cmd = \"ready\",\n    args = { \"--undo\" },\n    icon = config.icons.pr.draft,\n    type = \"pr\",\n    title = \"Mark PR #{number} as draft\",\n    success = \"Marked PR #{number} as draft\",\n    enabled = function(item)\n      return item.state == \"open\" and not item.isDraft\n    end,\n  },\n  gh_approve = {\n    cmd = \"review\",\n    icon = config.icons.checkmark,\n    type = \"pr\",\n    args = { \"--approve\" },\n    edit = \"body-file\", -- optional review summary\n    title = \"Review: approve PR #{number}\",\n    success = \"Approved PR #{number}\",\n    enabled = function(item)\n      return item.state == \"open\" and not item.pendingReview\n    end,\n  },\n  gh_request_changes = {\n    cmd = \"review\",\n    type = \"pr\",\n    icon = \" \",\n    args = { \"--request-changes\" },\n    edit = \"body-file\", -- explain what needs fixing\n    title = \"Review: request changes on PR #{number}\",\n    success = \"Requested changes on PR #{number}\",\n    enabled = function(item)\n      return item.state == \"open\" and not item.pendingReview\n    end,\n  },\n  gh_review = {\n    cmd = \"review\",\n    type = \"pr\",\n    icon = \" \",\n    args = { \"--comment\" },\n    edit = \"body-file\", -- general feedback\n    title = \"Review: comment on PR #{number}\",\n    success = \"Commented on PR #{number}\",\n    enabled = function(item)\n      return item.state == \"open\" and not item.pendingReview\n    end,\n  },\n}\n\n---@param opts snacks.gh.cli.Action\nfunction M.cli_action(opts)\n  ---@type snacks.gh.Action\n  return setmetatable({\n    desc = opts.desc or opts.title,\n    ---@type snacks.gh.action.fn\n    action = function(item, ctx)\n      M.run(item, opts, ctx)\n    end,\n  }, { __index = opts })\nend\n\n---@param str string\n---@param ... table<string, any>\nfunction M.tpl(str, ...)\n  local data = { ... }\n  return Snacks.picker.util.tpl(\n    str,\n    setmetatable({}, {\n      __index = function(_, key)\n        for _, d in ipairs(data) do\n          if d[key] ~= nil then\n            local ret = d[key]\n            return ret == \"pr\" and \"PR\" or ret\n          end\n        end\n      end,\n    })\n  )\nend\n\n---@param item snacks.picker.gh.Item\n---@param ctx snacks.gh.action.ctx\nfunction M.get_actions(item, ctx)\n  local ret = {} ---@type table<string, snacks.gh.Action>\n  local keys = vim.tbl_keys(M.actions) ---@type string[]\n  vim.list_extend(keys, vim.tbl_keys(M.cli_actions))\n  for _, name in ipairs(keys) do\n    local action = M.actions[name]\n    local enabled = action.type == nil or action.type == item.type\n    enabled = enabled and (action.enabled == nil or action.enabled(item, ctx))\n    if enabled then\n      local a = setmetatable({}, { __index = action })\n      local ca = M.cli_actions[name] or {}\n      a.desc = a.title and M.tpl(a.title or name, item, ca) or a.desc\n      a.name = name\n      ret[name] = a\n    end\n  end\n  return ret\nend\n\n--- Executes a gh cli action\n---@param item snacks.picker.gh.Item\n---@param action snacks.gh.cli.Action\n---@param ctx snacks.gh.action.ctx\nfunction M.run(item, action, ctx)\n  local args = action.cmd and { item.type, action.cmd, tostring(item.number) } or {}\n  vim.list_extend(args, action.args or {})\n  if action.api then\n    action.api.endpoint = M.tpl(action.api.endpoint, item, action)\n  end\n  ---@type snacks.gh.cli.Action.ctx\n  local cli_ctx = {\n    item = item,\n    args = args,\n    opts = action,\n    picker = ctx.picker,\n    main = ctx.main,\n  }\n  if action.edit then\n    return M.edit(cli_ctx)\n  else\n    return M._run(cli_ctx)\n  end\nend\n\n--- Parses frontmatter fields from body and appends them to ctx.args\n---@param body string\n---@param ctx snacks.gh.cli.Action.ctx\nfunction M.parse(body, ctx)\n  if not ctx.opts.fields then\n    return body\n  end\n\n  local fields = {} ---@type table<string, snacks.gh.Field>\n  for _, f in ipairs(ctx.opts.fields) do\n    fields[f.name] = f\n  end\n\n  local values = {} ---@type table<string, string>\n  --- parse markdown frontmatter for fields\n  body = body:gsub(\"^(%-%-%-\\n.-\\n%-%-%-\\n%s*)\", function(fm)\n    fm = fm:gsub(\"^%-%-%-\\n\", \"\"):gsub(\"\\n%-%-%-\\n%s*$\", \"\") --[[@as string]]\n    local lines = vim.split(fm, \"\\n\")\n    for _, line in ipairs(lines) do\n      local field, value = line:match(\"^(%w+):%s*(.-)%s*$\")\n      if field and fields[field] then\n        values[field] = value\n      else\n        Snacks.notify.warn((\"Unknown field `%s` in frontmatter\"):format(field or line))\n      end\n    end\n    return \"\"\n  end) --[[@as string]]\n\n  for _, field in ipairs(ctx.opts.fields) do\n    local value = values[field.name]\n    if value then\n      if ctx.opts.api then\n        ctx.opts.api.fields = ctx.opts.api.fields or {}\n        ctx.opts.api.fields[field.arg] = value\n      else\n        vim.list_extend(ctx.args, { \"--\" .. field.arg, value })\n      end\n    else\n      Snacks.notify.error((\"Missing required field `%s` in frontmatter\"):format(field.name))\n      return\n    end\n  end\n  return body\nend\n\n--- Executes the action CLI command\n---@param ctx snacks.gh.cli.Action.ctx\nfunction M._run(ctx, force)\n  if not force and ctx.opts.confirm then\n    Snacks.picker.util.confirm(M.tpl(ctx.opts.confirm, ctx.item, ctx.opts), function()\n      M._run(ctx, true)\n    end)\n    return\n  end\n\n  local spinner = require(\"snacks.picker.util.spinner\").loading()\n  local cb = function()\n    vim.schedule(function()\n      spinner:stop()\n\n      -- success message\n      if ctx.opts.success then\n        Snacks.notify.info(M.tpl(ctx.opts.success, ctx.item, ctx.opts))\n      end\n\n      -- refresh item and picker\n      if ctx.opts.refresh ~= false then\n        vim.schedule(function()\n          Api.refresh(ctx.item)\n          if ctx.picker and not ctx.picker.closed then\n            ctx.picker:refresh()\n            vim.cmd.startinsert()\n          end\n        end)\n        if ctx.picker and not ctx.picker.closed then\n          ctx.picker:focus()\n        end\n      end\n\n      -- clean up scratch buffer\n      if ctx.scratch then\n        local buf = assert(ctx.scratch.buf)\n        local fname = vim.api.nvim_buf_get_name(buf)\n        ctx.scratch:on(\"WinClosed\", function()\n          vim.schedule(function()\n            pcall(vim.api.nvim_buf_delete, buf, { force = true })\n            os.remove(fname)\n            os.remove(fname .. \".meta\")\n          end)\n        end, { buf = true })\n        ctx.scratch:close()\n      end\n    end)\n  end\n\n  if ctx.opts.api then\n    Api.request(\n      cb,\n      Snacks.config.merge(ctx.opts.api or {}, {\n        args = ctx.args,\n        on_error = function()\n          spinner:stop()\n        end,\n      })\n    )\n  else\n    Api.cmd(cb, {\n      input = ctx.input,\n      args = ctx.args,\n      repo = ctx.item.repo or ctx.opts.repo,\n      on_error = function()\n        spinner:stop()\n      end,\n    })\n  end\nend\n\n--- Edit action body in scratch buffer\n---@param ctx snacks.gh.cli.Action.ctx\nfunction M.edit(ctx)\n  ---@param s? string\n  local function tpl(s)\n    return s and M.tpl(s, ctx.item, ctx.opts) or nil\n  end\n\n  local template = ctx.opts.template or \"\"\n  if not vim.tbl_isempty(ctx.opts.fields or {}) then\n    local fm = { \"---\" }\n    for _, f in ipairs(ctx.opts.fields) do\n      fm[#fm + 1] = (\"%s: {%s}\"):format(f.name, f.prop)\n    end\n    fm[#fm + 1] = \"---\\n\\n\"\n    template = table.concat(fm, \"\\n\") .. template\n  end\n\n  local preview = ctx.picker and ctx.picker.preview and ctx.picker.preview.win:valid() and ctx.picker.preview.win or nil\n  local actions = preview and preview.opts.actions or {}\n  local parent = ctx.main or preview and preview.win or vim.api.nvim_get_current_win()\n\n  local height = config.scratch.height or 15\n  local opts = Snacks.win.resolve({\n    relative = \"win\",\n    width = 0,\n    backdrop = false,\n    height = height,\n    actions = {\n      cycle_win = actions.cycle_win,\n      preview_scroll_up = actions.preview_scroll_up,\n      preview_scroll_down = actions.preview_scroll_down,\n    },\n    win = parent,\n    wo = { winhighlight = \"NormalFloat:Normal,FloatTitle:SnacksGhScratchTitle,FloatBorder:SnacksGhScratchBorder\" },\n    border = \"top_bottom\",\n    row = function(win)\n      local border = win:border_size()\n      return win:parent_size().height - height - border.top - border.bottom\n    end,\n    on_win = function(win)\n      if vim.api.nvim_win_is_valid(parent) then\n        local parent_row = vim.api.nvim_win_call(parent, vim.fn.winline) ---@type number\n        parent_row = parent_row + vim.wo[parent].scrolloff -- adjust for scrolloff\n        local row = vim.api.nvim_win_get_height(parent) - win:size().height\n        if parent_row > row then\n          vim.api.nvim_win_call(parent, function()\n            vim.cmd((\"normal! %d%s\"):format(parent_row - row, Snacks.util.keycode(\"<C-e>\")))\n          end)\n        end\n      end\n      vim.g.snacks_picker_cycle_win = win.win\n      vim.schedule(function()\n        vim.cmd.startinsert()\n      end)\n    end,\n    footer_keys = { \"<c-s>\", \"R\" },\n    keys = {\n      submit = {\n        \"<c-s>\",\n        function(win)\n          ctx.scratch = win\n          M.submit(ctx)\n        end,\n        desc = \"Submit\",\n        mode = { \"n\", \"i\" },\n      },\n    },\n  }, preview and {\n    keys = {\n      [\"<a-w>\"] = { \"cycle_win\", mode = { \"i\", \"n\" } },\n      [\"<c-b>\"] = { \"preview_scroll_up\", mode = { \"i\", \"n\" } },\n      [\"<c-f>\"] = { \"preview_scroll_down\", mode = { \"i\", \"n\" } },\n    },\n  } or nil)\n  Snacks.scratch({\n    ft = \"markdown\",\n    icon = config.icons.logo,\n    name = tpl(ctx.opts.title or \"{cmd} {type} #{number}\"),\n    template = tpl(template),\n    filekey = {\n      cwd = false,\n      branch = false,\n      count = false,\n      id = tpl(\"{repo}/{type}/{cmd}\"),\n    },\n    win = opts,\n  })\nend\n\n--- Submit edited body\n---@param ctx snacks.gh.cli.Action.ctx\nfunction M.submit(ctx)\n  local edit = assert(ctx.opts.edit, \"Submit called for action that doesn't need edit?\")\n  local win = assert(ctx.scratch, \"Submit not called from scratch window?\")\n  ctx = setmetatable({\n    args = vim.deepcopy(ctx.args),\n  }, { __index = ctx }) -- shallow copy to avoid mutation\n  local body = M.parse(win:text(), ctx)\n\n  if not body then\n    return -- error already shown in M.parse\n  end\n\n  if ctx.opts.on_submit then\n    body = ctx.opts.on_submit(body, ctx) or body\n  end\n\n  if body:find(\"%S\") then\n    if edit == \"body-file\" then\n      if ctx.opts.api then\n        ctx.opts.api.input = ctx.opts.api.input or {}\n        if ctx.opts.api.input.variables then\n          ctx.opts.api.input.variables.body = body\n        else\n          ctx.opts.api.input.body = body\n        end\n      else\n        ctx.input = body\n        vim.list_extend(ctx.args, { \"--body-file\", \"-\" })\n      end\n    else\n      if ctx.opts.api then\n        ctx.opts.api.fields = ctx.opts.api.fields or {}\n        ctx.opts.api.fields[edit] = body\n      else\n        vim.list_extend(ctx.args, { \"--\" .. edit, body })\n      end\n    end\n  end\n\n  vim.cmd.stopinsert()\n  vim.schedule(function()\n    M._run(ctx)\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gh/api.lua",
    "content": "local Async = require(\"snacks.picker.util.async\")\nlocal Item = require(\"snacks.gh.item\")\nlocal Proc = require(\"snacks.util.spawn\")\n\n---@class snacks.gh.api\nlocal M = {}\n\n---@type table<string, snacks.picker.gh.Item>\nlocal cache = setmetatable({}, { __mode = \"v\" })\nlocal pr_cache = {} ---@type table<string, snacks.picker.gh.Item>\n\n---@type table<string, snacks.gh.api.Config|{}>\nlocal config = {\n  base = {\n    list = {\n      \"author\",\n      \"closedAt\",\n      \"createdAt\",\n      \"id\",\n      \"body\",\n      \"labels\",\n      \"number\",\n      \"reactionGroups\",\n      \"state\",\n      \"title\",\n      \"updatedAt\",\n      \"url\",\n    },\n    view = { \"comments\" },\n    text = { \"author\", \"hash\", \"label\", \"title\" },\n    options = { \"app\", \"assignee\", \"author\", \"jq\", \"label\", \"repo\", \"search\", \"state\" },\n  },\n  api = {\n    options = { \"cache\", \"jq\", \"method\", \"paginate\", \"silent\", \"slurp\" },\n  },\n  issue = {\n    list = { \"stateReason\" },\n    options = { \"mention\", \"milestone\" },\n    ---@param item snacks.picker.gh.Item\n    transform = function(item)\n      item.status = item.state == \"closed\" and item.state_reason or item.state\n      return item\n    end,\n  },\n  pr = {\n    options = { \"base\", \"draft\" },\n    list = {\n      \"mergedAt\",\n      \"changedFiles\",\n      \"mergeable\",\n      \"mergeStateStatus\",\n      \"isDraft\",\n    },\n    view = {\n      \"additions\",\n      \"baseRefName\",\n      \"deletions\",\n      \"headRefName\",\n      \"headRefOid\",\n      \"mergedAt\",\n      \"statusCheckRollup\",\n      \"reviews\",\n    },\n    ---@param item snacks.picker.gh.Item\n    transform = function(item)\n      item.status = item.draft and \"draft\" or item.state\n      return item\n    end,\n  },\n}\n\n---@param item snacks.gh.api.View\nlocal function cache_get(item)\n  return cache[Item.to_uri(item)]\nend\n\n---@param item snacks.picker.gh.Item\nlocal function cache_set(item)\n  cache[item.uri] = item\n  return item\nend\n\n---@generic T\n---@param fn fun(cb:fun(proc:snacks.spawn.Proc, data?:any), opts:T): snacks.spawn.Proc\n---@return fun(opts:T): any?\nlocal function wrap_sync(fn)\n  ---@async\n  return function(opts)\n    local ret ---@type any\n    fn(function(_, data)\n      ret = data\n    end, opts):wait()\n    return ret\n  end\nend\n\n--- Cleanup GraphQL internal nodes and reaction groups\n---@param ret table<string, any>\nlocal function clean_graphql(ret)\n  for k, v in pairs(ret) do\n    if type(v) == \"table\" then\n      clean_graphql(v)\n    end\n    if k == \"reactionGroups\" and type(v) == \"table\" then\n      ---@param r snacks.gh.Reaction\n      ret[k] = vim.tbl_filter(function(r)\n        return r.users and r.users.totalCount and r.users.totalCount > 0\n      end, v)\n      ret[k] = #ret[k] > 0 and ret[k] or nil\n    elseif type(v) == \"table\" and type(v.nodes) == \"table\" and vim.tbl_count(v) == 1 then\n      ret[k] = v.nodes\n    elseif v == vim.NIL then\n      ret[k] = nil\n    end\n  end\n  return ret\nend\n\n---@param what \"issue\" | \"pr\"\n---@param key \"list\" | \"view\"\nlocal function get_opts(what, key)\n  local base = vim.deepcopy(config.base)\n  local specific = vim.deepcopy(config[what] or {})\n  base.type = what\n  base.fields = vim.list_extend(base.list or {}, specific.list or {})\n  if key ~= \"list\" then\n    base.fields = vim.list_extend(base.fields, base[key] or {})\n    base.fields = vim.list_extend(base.fields, specific[key] or {})\n  end\n  base.text = vim.list_extend(base.text, specific.text or {})\n  base.options = vim.list_extend(base.options, specific.options or {})\n  base.transform = specific.transform\n  return base\nend\n\n---@param args string[]\n---@param options string[]\n---@param opts table<string, string|boolean|nil>\nlocal function set_options(args, options, opts)\n  for _, option in ipairs(options or {}) do\n    local value = opts[option] ---@type string|boolean|nil\n    if type(value) == \"boolean\" and value then\n      args[#args + 1] = \"--\" .. option\n    elseif value and value ~= \"\" then\n      vim.list_extend(args, { \"--\" .. option, tostring(value) })\n    end\n  end\nend\n\n---@param cb fun(proc: snacks.spawn.Proc, data?: string)\n---@param opts snacks.gh.api.Cmd\nfunction M.cmd(cb, opts)\n  opts = opts or {}\n  local args = vim.deepcopy(opts.args)\n  if opts.repo then\n    vim.list_extend(args, { \"--repo\", opts.repo })\n  end\n  local Spawn = require(\"snacks.util.spawn\")\n  local async = Async.running()\n  local ret ---@type snacks.spawn.Proc\n\n  if async then\n    async:on(\"abort\", function()\n      if ret and ret:running() then\n        ret:kill()\n      end\n    end)\n  end\n  ret = Spawn.new({\n    cmd = \"gh\",\n    args = args,\n    input = opts.input,\n    timeout = 10000,\n    -- debug = true,\n    on_exit = function(proc, err)\n      if err then\n        vim.schedule(function()\n          if not proc.aborted then\n            if opts.notify ~= false then\n              Snacks.debug.cmd({\n                header = \"GH Error\",\n                cmd = { \"gh\", unpack(args) },\n                footer = proc:err(),\n                level = vim.log.levels.ERROR,\n                props = { input = opts.input },\n              })\n            end\n            if opts.on_error then\n              opts.on_error(proc, proc:err())\n            end\n          end\n        end)\n        return\n      end\n      return cb(proc, not err and proc:out() or nil)\n    end,\n  })\n  return ret\nend\nM.cmd_sync = wrap_sync(M.cmd)\n\n---@param cb fun(proc: snacks.spawn.Proc, data?: unknown)\n---@param opts snacks.gh.api.Fetch\nfunction M.fetch(cb, opts)\n  local args = vim.deepcopy(opts.args)\n  vim.list_extend(args, { \"--json\", table.concat(opts.fields, \",\") })\n  return M.cmd(function(proc, data)\n    cb(proc, data and proc:json() or nil)\n  end, {\n    args = args,\n    repo = opts.repo,\n    notify = opts.notify,\n  })\nend\nM.fetch_sync = wrap_sync(M.fetch)\n\n---@param cb fun(proc: snacks.spawn.Proc, data?: table)\n---@param opts snacks.gh.api.Api\nfunction M.request(cb, opts)\n  local args = { \"api\", opts.endpoint }\n  set_options(args, config.api.options or {}, opts)\n  if opts.input then\n    vim.list_extend(args, { \"--input\", \"-\" })\n  end\n  for k, v in pairs(opts.fields or {}) do\n    vim.list_extend(args, { \"--raw-field\", (\"%s=%s\"):format(k, tostring(v)) })\n  end\n  for k, v in pairs(opts.params or {}) do\n    vim.list_extend(args, { \"--field\", (\"%s=%s\"):format(k, tostring(v)) })\n  end\n  for k, v in pairs(opts.header or {}) do\n    vim.list_extend(args, { \"--header\", (\"%s:%s\"):format(k, tostring(v)) })\n  end\n  return M.cmd(function(proc, data)\n    cb(proc, data and data:find(\"%S\") and proc:json() or nil)\n  end, {\n    args = args,\n    input = opts.input and vim.json.encode(opts.input) or nil,\n    on_error = opts.on_error,\n  })\nend\nM.request_sync = wrap_sync(M.request)\n\n---@param cb fun(proc: snacks.spawn.Proc, data?: table)\n---@param opts snacks.gh.api.GraphQL\nfunction M.graphql(cb, opts)\n  opts = Snacks.config.merge(vim.deepcopy(opts), {\n    endpoint = \"graphql\",\n    fields = {\n      query = opts.query,\n    },\n  })\n  return M.request(function(proc, data)\n    if not data then\n      return\n    end\n    if data.errors then\n      local msgs = {} ---@type string[]\n      for _, err in ipairs(data.errors) do\n        msgs[#msgs + 1] = err.message\n      end\n      vim.schedule(function()\n        Snacks.debug.cmd({\n          header = \"GH GraphQL Error\",\n          cmd = { \"gh\", \"api\", \"graphql\" },\n          footer = table.concat(msgs, \"\\n\"),\n          level = vim.log.levels.ERROR,\n        })\n        if opts.on_error then\n          opts.on_error(proc, table.concat(msgs, \"\\n\"))\n        end\n      end)\n      return\n    end\n    cb(proc, clean_graphql(data.data))\n  end, opts)\nend\nM.graphql_sync = wrap_sync(M.graphql)\n\n---@async\nfunction M.user()\n  ---@type snacks.gh.User\n  return M.request_sync({\n    endpoint = \"/user\",\n  })\nend\n\n---@param what \"issue\" | \"pr\"\n---@param cb fun(items?: snacks.picker.gh.Item[])\n---@param opts? snacks.picker.gh.Config\nfunction M.list(what, cb, opts)\n  opts = opts or {}\n  local api_opts = get_opts(what, \"list\")\n  local args = { what, \"list\" }\n\n  vim.list_extend(args, { \"--limit\", tostring(opts.limit or 50) })\n  set_options(args, api_opts.options, opts)\n\n  ---@param data? snacks.gh.Item[]\n  return M.fetch(function(_, data)\n    if not data then\n      return cb()\n    end\n    ---@param item snacks.gh.Item\n    return cb(vim.tbl_map(function(item)\n      return cache_set(Item.new(item, api_opts))\n    end, data))\n  end, {\n    args = args,\n    fields = api_opts.fields,\n    repo = opts.repo,\n  })\nend\n\n---@param cb fun(item?: snacks.picker.gh.Item, updated?: boolean)\n---@param item snacks.gh.api.View|{number?: number}\n---@param opts? { fields?: string[], force?: boolean }\nfunction M.view(cb, item, opts)\n  opts = opts or {}\n  local api_opts = get_opts(item.type, \"view\")\n  if opts.fields then\n    api_opts.fields = vim.list_extend(api_opts.fields, opts.fields)\n  end\n\n  item = M.get_cached(item)\n  local todo = Item.is(item) and item:need(api_opts.fields) or api_opts.fields\n  if opts.force or item.dirty then\n    todo = api_opts.fields\n  end\n\n  if #todo == 0 then\n    cb(item, false)\n    return\n  end\n\n  local args = { item.type, \"view\", tostring(item.number) }\n  local need_reviews = item.type == \"pr\" and vim.tbl_contains(todo, \"comments\")\n  local it ---@type snacks.gh.Item?\n  local completed = 0\n  local fetch_comments = false\n  local procs = {} ---@type snacks.spawn.Proc[]\n\n  ---@param data? snacks.gh.Item|{}\n  local function handler(data)\n    it = data and vim.tbl_extend(\"force\", it or {}, data or {}) or it\n    if fetch_comments then\n      fetch_comments = false\n      item.repo = it and Item.get_repo(it.url) or nil\n      procs[#procs + 1] = M.comments(item, handler)\n    end\n    completed = completed + 1\n    if completed < #procs then\n      return\n    end\n    if not it then\n      return cb()\n    end\n    item = Item.new(item, api_opts)\n    item:update(it, todo)\n    item.dirty = false\n    cb(cache_set(item), true)\n  end\n\n  if need_reviews then\n    todo = vim.tbl_filter(function(f)\n      return f ~= \"comments\" and f ~= \"reviews\"\n    end, todo)\n    if item.repo then\n      procs[#procs + 1] = M.comments(item, handler)\n    else\n      -- fetch comments once we fetched the item\n      fetch_comments = true\n    end\n  end\n\n  if #todo > 0 then\n    ---@param data? snacks.gh.Item\n    procs[#procs + 1] = M.fetch(function(_, data)\n      handler(data)\n    end, {\n      args = args,\n      fields = todo,\n      repo = item.repo or api_opts.repo,\n    })\n  end\n\n  ---@type snacks.picker.Waitable\n  return {\n    ---@async\n    wait = function()\n      for _, proc in ipairs(procs) do\n        proc:wait()\n      end\n    end,\n  }\nend\n\n---@param item snacks.gh.api.View\n---@param opts? { fields?: string[], force?: boolean }\n---@async\nfunction M.get(item, opts)\n  local ret ---@type snacks.picker.gh.Item?\n  local procs = M.view(function(it)\n    ret = it\n  end, item, opts)\n  if procs then\n    procs:wait()\n  end\n  return ret\nend\n\n---@param item snacks.gh.api.View\nfunction M.get_cached(item)\n  return not Item.is(item) and cache_get(item) or item\nend\n\n---@param item snacks.picker.gh.Item\nfunction M.refresh(item)\n  item.dirty = true\n  cache_set(item)\n  for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n    if vim.api.nvim_buf_is_loaded(buf) then\n      if vim.api.nvim_buf_get_name(buf) == item.uri then\n        require(\"snacks.gh.buf\").attach(buf, item)\n      end\n    end\n  end\nend\n\n---@param cb fun(data?: {comments: snacks.gh.Comment[], reviews: snacks.gh.Review[]})\n---@param item snacks.gh.api.View\nfunction M.comments(item, cb)\n  local owner, name = item.repo:match(\"^(.-)/(.-)$\")\n  return M.graphql(function(_, data)\n    if not data then\n      return cb()\n    end\n    cb(data.repository.pullRequest)\n  end, {\n    -- comment\n    params = {\n      owner = owner,\n      name = name,\n      number = item.number,\n    },\n\n    -- inject: graphql\n    query = [[\n      query($owner: String!, $name: String!, $number: Int!) {\n        repository(owner: $owner, name: $name) {\n          pullRequest(number: $number) {\n            reviewThreads(first: 100) {\n              nodes {\n                id\n                diffSide\n                comments(first: 50) {\n                  nodes {\n                    id\n                  }\n                }\n              }\n            }\n            reviews(first: 100) {\n              nodes {\n                id\n                databaseId\n                author { login }\n                authorAssociation\n                body\n                state\n                commit { oid }\n                submittedAt\n                createdAt\n                viewerDidAuthor\n                reactionGroups {\n                  content\n                  users { totalCount }\n                }\n                comments(first: 50) {\n                  nodes {\n                    id\n                    databaseId\n                    body\n                    path\n                    diffHunk\n                    line\n                    startLine\n                    originalLine\n                    originalStartLine\n                    createdAt\n                    subjectType\n                    author { login }\n                    replyTo { id databaseId }\n                    reactionGroups {\n                      content\n                      users { totalCount }\n                    }\n                  }\n                }\n              }\n            }\n            comments(first: 100) {\n              nodes {\n                id\n                databaseId\n                body\n                author { login }\n                authorAssociation\n                createdAt\n                reactionGroups {\n                  content\n                  users { totalCount }\n                }\n              }\n            }\n          }\n        }\n      }\n    ]],\n  })\nend\n\n---@async\nfunction M.current_pr()\n  local root = Snacks.git.get_root(vim.uv.cwd() or \".\")\n  if not root then\n    return\n  end\n  ---@type snacks.picker.gh.Item?\n  local pr\n  local branch = Proc.exec({ \"git\", \"branch\", \"--show-current\" })\n\n  local key = root .. \"::\" .. branch\n  if pr_cache[key] then\n    return pr_cache[key]\n  end\n\n  -- try with `pr view` first\n  local api_opts = get_opts(\"pr\", \"list\")\n  pr = M.fetch_sync({\n    args = { \"pr\", \"view\" },\n    fields = api_opts.fields,\n    notify = false,\n  })\n  pr = pr and cache_set(Item.new(pr, api_opts)) or nil\n  if pr then\n    pr_cache[key] = pr\n    return pr\n  end\n\n  -- assume this is the main branch of a fork\n  local author, main = branch:match(\"^(.-)/(.+)$\")\n  if not author or not main then\n    return\n  end\n  local repo = M.cmd_sync({\n    args = { \"repo\", \"view\", \"--json\", \"nameWithOwner\", \"--jq\", \".nameWithOwner\" },\n    notify = false,\n  })\n  if not repo then\n    return\n  end\n  repo = vim.trim(repo)\n\n  M.list(\"pr\", function(items)\n    pr_cache[key] = items and items[1] or nil\n    pr = pr_cache[key]\n  end, {\n    author = author,\n    head = main,\n    base = main,\n    repo = repo,\n    limit = 1,\n    notify = false,\n  }):wait()\n  return pr\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gh/buf.lua",
    "content": "local Actions = require(\"snacks.gh.actions\")\nlocal Api = require(\"snacks.gh.api\")\nlocal Item = require(\"snacks.gh.item\")\nlocal Render = require(\"snacks.gh.render\")\n\n---@class snacks.gh.Buf\n---@field buf number\n---@field opts snacks.gh.Config\n---@field item snacks.gh.api.View\nlocal M = {}\nM.__index = M\n\n---@class vim.var_accessor\n---@field snacks_gh? { repo: string, type: string, number: number }\n\n---@type table<number, snacks.gh.Buf>\nM.attached = {}\nlocal did_setup = false\n\n---@param buf number\n---@param item snacks.gh.api.View\nfunction M.new(buf, item)\n  local self = setmetatable({}, M)\n  self.buf = buf\n  self.item = item\n  self.opts = vim.deepcopy(Snacks.gh.config())\n  self.opts.bo = Snacks.config.merge({}, self.opts.bo, {\n    buftype = \"acwrite\",\n    swapfile = false,\n    filetype = \"markdown.gh\",\n  })\n  vim.b[buf].snacks_gh = {\n    repo = item.repo,\n    type = item.type,\n    number = tonumber(item.number) or item.number,\n  }\n  self:bo()\n  self:wo()\n  self:keys()\n  M.attached[buf] = self\n  vim.schedule(function()\n    self:render()\n  end)\n  return self\nend\n\nfunction M:update()\n  if not self:valid() then\n    return\n  end\n  self:render({ force = true })\nend\n\nfunction M:keys()\n  local actions = Actions.get_actions(self.item, { items = { self.item } })\n\n  ---@param name string\n  local function wrap(name)\n    local action = actions[name]\n    if not action then\n      return\n    end\n    ---@type snacks.gh.Keymap.fn\n    return function(item)\n      action.action(item, { items = { item } })\n    end\n  end\n\n  for name, km in pairs(self.opts.keys or {}) do\n    if km ~= false then\n      local rhs = km[2]\n      local desc = km.desc\n      local action = type(rhs) == \"function\" and rhs or type(rhs) == \"string\" and wrap(rhs) or nil\n      if action then\n        Snacks.keymap.set(km.mode or \"n\", km[1], function()\n          action(self.item, self)\n        end, { buffer = self.buf, desc = desc })\n      elseif type(rhs) == \"string\" and not Actions.actions[rhs] then\n        Snacks.notify.error((\"Invalid gh buffer keymap action `%s:%s`\"):format(name, rhs))\n      end\n    end\n  end\nend\n\nfunction M:valid()\n  return self.buf and M.attached[self.buf] == self and vim.api.nvim_buf_is_valid(self.buf)\nend\n\n---@param opts? {force?:boolean}\nfunction M:render(opts)\n  if not self:valid() then\n    return\n  end\n  opts = opts or {}\n  self.item = Api.get_cached(self.item)\n\n  self:bo()\n  self:wo()\n\n  local spinner ---@type snacks.util.Spinner?\n  local proc = Api.view(function(it, updated)\n    vim.schedule(function()\n      if not self:valid() then\n        return\n      end\n      if spinner then\n        spinner:stop()\n      end\n      self.item = it\n      if updated then\n        Render.render(self.buf, it, self.opts)\n        self:keys()\n      end\n    end)\n  end, self.item, { force = opts.force })\n\n  -- initial render (is partial if proc is running)\n  if Item.is(self.item) then\n    Render.render(self.buf, self.item, Snacks.config.merge({}, vim.deepcopy(self.opts), { partial = proc ~= nil }))\n  end\n\n  if proc then\n    spinner = Snacks.picker.util.spinner(self.buf)\n  end\nend\n\nfunction M:bo()\n  vim.b[self.buf].snacks_statuscolumn_left = false\n  Snacks.util.bo(self.buf, self.opts.bo)\nend\n\nfunction M:wo()\n  for _, win in ipairs(vim.fn.win_findbuf(self.buf)) do\n    Snacks.util.wo(win, self.opts.wo)\n  end\nend\n\n---@param buf number\n---@param item? snacks.gh.api.View\nfunction M.attach(buf, item)\n  M.setup()\n  local ret = M.attached[buf]\n  if ret then\n    ret:update()\n    return ret\n  end\n  if not item then\n    local name = vim.api.nvim_buf_get_name(buf)\n    local repo, type, number = name:match(\"^gh://([^/]+/[^/]+)/([^/]+)/(%d+)$\")\n    if not repo then\n      Snacks.notify.error(\"Invalid gh:// buffer: \" .. name)\n      return\n    end\n    item = {\n      repo = repo,\n      type = type,\n      number = number,\n    }\n  end\n  return M.new(buf, item)\nend\n\n--@param buf number\nfunction M.detach(buf)\n  if not M.attached[buf] then\n    return\n  end\n  M.attached[buf] = nil\nend\n\nfunction M.setup()\n  if did_setup then\n    return\n  end\n  did_setup = true\n  local group = vim.api.nvim_create_augroup(\"snacks.gh.buf\", { clear = true })\n\n  vim.api.nvim_create_autocmd(\"BufReadCmd\", {\n    pattern = \"gh://*\",\n    group = group,\n    callback = function(e)\n      vim.schedule(function()\n        -- schedule since Neovim otherwise runs this in the autocmd window\n        M.attach(e.buf)\n      end)\n    end,\n  })\n\n  -- prevent altering the original image file\n  vim.api.nvim_create_autocmd(\"BufWriteCmd\", {\n    pattern = \"gh://*\",\n    group = group,\n    callback = function(e)\n      vim.bo[e.buf].modified = false\n    end,\n  })\n\n  vim.api.nvim_create_autocmd(\"BufWinEnter\", {\n    pattern = \"gh://*\",\n    group = group,\n    callback = function(e)\n      local buf = M.attached[e.buf]\n      if buf then\n        buf:bo()\n        buf:wo()\n      end\n    end,\n  })\n\n  vim.api.nvim_create_autocmd(\"ColorScheme\", {\n    group = group,\n    callback = function(e)\n      for _, buf in pairs(M.attached) do\n        buf:render()\n      end\n    end,\n  })\n\n  -- detach on buffer delete\n  vim.api.nvim_create_autocmd({ \"BufDelete\", \"BufWipeout\" }, {\n    pattern = \"gh://*\",\n    group = group,\n    callback = function(ev)\n      M.detach(ev.buf)\n    end,\n  })\n\n  -- Keep some empty windows in sessions\n  vim.api.nvim_create_autocmd(\"ExitPre\", {\n    group = group,\n    callback = function()\n      local keep = { \"markdown.gh\" }\n      for _, win in ipairs(vim.api.nvim_list_wins()) do\n        local buf = vim.api.nvim_win_get_buf(win)\n        if vim.tbl_contains(keep, vim.bo[buf].filetype) then\n          vim.bo[buf].buftype = \"\" -- set buftype to empty to keep the window\n        end\n      end\n    end,\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gh/init.lua",
    "content": "---@class snacks.gh\n---@field api snacks.gh.api\n---@field item snacks.picker.gh.Item\nlocal M = setmetatable({}, {\n  ---@param M snacks.gh\n  __index = function(M, k)\n    if vim.tbl_contains({ \"api\" }, k) then\n      M[k] = require(\"snacks.gh.\" .. k)\n    end\n    return rawget(M, k)\n  end,\n})\n\nM.meta = {\n  desc = \"GitHub CLI integration\",\n  needs_setup = false,\n}\n\n---@alias snacks.gh.Keymap.fn fun(item:snacks.picker.gh.Item, buf:snacks.gh.Buf)\n---@class snacks.gh.Keymap: vim.keymap.set.Opts\n---@field [1] string lhs\n---@field [2] string|snacks.gh.Keymap.fn rhs\n---@field mode? string|string[] defaults to `n`\n\n---@class snacks.gh.Config\nlocal defaults = {\n  --- Keymaps for GitHub buffers\n  ---@type table<string, snacks.gh.Keymap|false>?\n  -- stylua: ignore\n  keys = {\n    select  = { \"<cr>\", \"gh_actions\", desc = \"Select Action\" },\n    edit    = { \"i\"   , \"gh_edit\"   , desc = \"Edit\" },\n    comment = { \"a\"   , \"gh_comment\", desc = \"Add Comment\" },\n    close   = { \"c\"   , \"gh_close\"  , desc = \"Close\" },\n    reopen  = { \"o\"   , \"gh_reopen\" , desc = \"Reopen\" },\n  },\n  ---@type vim.wo|{}\n  wo = {\n    breakindent = true,\n    wrap = true,\n    showbreak = \"\",\n    linebreak = true,\n    number = false,\n    relativenumber = false,\n    foldexpr = \"v:lua.vim.treesitter.foldexpr()\",\n    foldmethod = \"expr\",\n    concealcursor = \"n\",\n    conceallevel = 2,\n    list = false,\n    winhighlight = Snacks.util.winhl({\n      Normal = \"SnacksGhNormal\",\n      NormalFloat = \"SnacksGhNormalFloat\",\n      FloatBorder = \"SnacksGhBorder\",\n      FloatTitle = \"SnacksGhTitle\",\n      FloatFooter = \"SnacksGhFooter\",\n    }),\n  },\n  ---@type vim.bo|{}\n  bo = {},\n  diff = {\n    min = 4, -- minimum number of lines changed to show diff\n    wrap = 80, -- wrap diff lines at this length\n  },\n  scratch = {\n    height = 15, -- height of scratch window\n  },\n  -- stylua: ignore\n  icons = {\n    logo = \" \",\n    user= \" \",\n    checkmark = \" \",\n    crossmark = \" \",\n    block = \"■\",\n    file = \" \",\n    checks = {\n      pending = \" \",\n      success = \" \",\n      failure = \"\",\n      skipped = \" \",\n    },\n    issue = {\n      open      = \" \",\n      completed = \" \",\n      other     = \" \"\n    },\n    pr = {\n      open   = \" \",\n      closed = \" \",\n      merged = \" \",\n      draft  = \" \",\n      other  = \" \",\n    },\n    review = {\n      approved           = \" \",\n      changes_requested  = \" \",\n      commented          = \" \",\n      dismissed          = \" \",\n      pending            = \" \",\n    },\n    merge_status = {\n      clean    = \" \",\n      dirty    = \" \",\n      blocked  = \" \",\n      unstable = \" \"\n    },\n    reactions = {\n      thumbs_up   = \"👍\",\n      thumbs_down = \"👎\",\n      eyes        = \"👀\",\n      confused    = \"😕\",\n      heart       = \"❤️\",\n      hooray      = \"🎉\",\n      laugh       = \"😄\",\n      rocket      = \"🚀\",\n    },\n  },\n}\n\nlocal function diff_linenr(hl)\n  local fg = Snacks.util.color({ hl, \"SnacksGhNormalFloat\", \"Normal\" })\n  local bg = Snacks.util.color({ hl, \"SnacksGhNormalFloat\", \"Normal\" }, \"bg\")\n  bg = bg or vim.o.background == \"dark\" and \"#1e1e1e\" or \"#f5f5f5\"\n  return {\n    fg = fg,\n    bg = Snacks.util.blend(fg, bg, 0.1),\n  }\nend\n\nSnacks.util.set_hl({\n  Normal = \"NormalFloat\",\n  NormalFloat = \"NormalFloat\",\n  Border = \"FloatBorder\",\n  Title = \"FloatTitle\",\n  ScratchTitle = \"Number\",\n  ScratchBorder = \"Number\",\n  Footer = \"FloatFooter\",\n  Number = \"Number\",\n  Green = { fg = \"#28a745\" },\n  Purple = { fg = \"#6f42c1\" },\n  Gray = { fg = \"#6a737d\" },\n  Red = { fg = \"#d73a49\" },\n  Branch = \"@markup.link\",\n  IssueOpen = \"SnacksGhGreen\",\n  IssueCompleted = \"SnacksGhPurple\",\n  IssueOther = \"SnacksGhGray\",\n  PrOpen = \"SnacksGhGreen\",\n  PrClosed = \"SnacksGhRed\",\n  PrMerged = \"SnacksGhPurple\",\n  PrDraft = \"SnacksGhGray\",\n  Label = \"@property\",\n  Delim = \"@punctuation.delimiter\",\n  UserBadge = \"DiagnosticInfo\",\n  AuthorBadge = \"DiagnosticWarn\",\n  OwnerBadge = \"DiagnosticError\",\n  BotBadge = { fg = Snacks.util.color({ \"NonText\", \"SignColumn\", \"FoldColumn\" }) },\n  ReactionBadge = \"Special\",\n  AssocBadge = {}, -- will be set to inverse of Normal\n  StatBadge = \"Special\",\n  PrClean = \"DiagnosticInfo\",\n  PrUnstable = \"DiagnosticWarn\",\n  PrDirty = \"DiagnosticError\",\n  PrBlocked = \"DiagnosticError\",\n  Additions = \"SnacksGhGreen\",\n  Deletions = \"SnacksGhRed\",\n  CheckPending = \"DiagnosticWarn\",\n  CheckSuccess = \"SnacksGhGreen\",\n  CheckFailure = \"SnacksGhRed\",\n  CheckSkipped = \"SnacksGhStat\",\n  ReviewApproved = \"SnacksGhGreen\",\n  ReviewChangesRequested = \"DiagnosticError\",\n  ReviewCommented = {},\n  ReviewPending = \"DiagnosticWarn\",\n  CommentAction = \"@property\",\n  DiffHeader = \"DiagnosticVirtualTextInfo\",\n  DiffAdd = \"DiffAdd\",\n  DiffDelete = \"DiffDelete\",\n  DiffContext = \"DiffChange\",\n  DiffAddLineNr = diff_linenr(\"DiffAdd\"),\n  DiffDeleteLineNr = diff_linenr(\"DiffDelete\"),\n  DiffContextLineNr = diff_linenr(\"DiffChange\"),\n  Stat = { fg = Snacks.util.color(\"SignColumn\") },\n}, { default = true, prefix = \"SnacksGh\" })\n\nM._config = nil ---@type snacks.gh.Config?\nlocal did_setup = false\n\n---@param opts? snacks.picker.gh.issue.Config\nfunction M.issue(opts)\n  return Snacks.picker.gh_issue(opts)\nend\n\n---@param opts? snacks.picker.gh.pr.Config\nfunction M.pr(opts)\n  return Snacks.picker.gh_pr(opts)\nend\n\n---@private\nfunction M.config()\n  M._config = M._config or Snacks.config.get(\"gh\", defaults)\n  return M._config\nend\n\n---@private\n---@param ev? vim.api.keyset.create_autocmd.callback_args\nfunction M.setup(ev)\n  if did_setup then\n    return\n  end\n  did_setup = true\n\n  -- vim.treesitter.language.register(\"markdown\", \"gh\")\n\n  require(\"snacks.gh.buf\").setup()\n  if ev then\n    vim.schedule(function()\n      require(\"snacks.gh.buf\").attach(ev.buf)\n    end)\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gh/item.lua",
    "content": "---@class snacks.picker.gh.Item\n---@field opts snacks.gh.api.Config\nlocal M = {}\n\nlocal time_fields = {\n  created = \"createdAt\",\n  updated = \"updatedAt\",\n  closed = \"closedAt\",\n  merged = \"mergedAt\",\n  submitted = \"submittedAt\",\n}\n\n---@param s? string\n---@return number?\nlocal function ts(s)\n  if not s then\n    return nil\n  end\n  local year, month, day, hour, min, sec = s:match(\"^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z$\")\n  if not year then\n    return\n  end\n  local t = os.time({\n    year = assert(tonumber(year), \"invalid year in timestamp: \" .. s),\n    month = assert(tonumber(month), \"invalid month in timestamp: \" .. s),\n    day = assert(tonumber(day), \"invalid day in timestamp: \" .. s),\n    hour = assert(tonumber(hour), \"invalid hour in timestamp: \" .. s),\n    min = assert(tonumber(min), \"invalid minute in timestamp: \" .. s),\n    sec = assert(tonumber(sec), \"invalid second in timestamp: \" .. s),\n    isdst = false,\n  })\n  -- Calculate UTC offset\n  local now = os.time()\n  local utc_date = os.date(\"!*t\", now) --[[@as osdate]]\n  utc_date.isdst = false\n  return t + os.difftime(now, os.time(utc_date))\nend\n\n---@param obj {body?:string}\nlocal function fix(obj)\n  obj.body = obj.body and obj.body:gsub(\"\\r\\n\", \"\\n\") or nil\n  for key, field in pairs(time_fields) do\n    ---@diagnostic disable-next-line: no-unknown, assign-type-mismatch\n    obj[key] = obj[key] or ts(obj[field] or obj[field:gsub(\"At\", \"_at\")])\n  end\nend\n\n---@param item snacks.gh.Item\n---@param opts snacks.gh.api.Config\nfunction M.new(item, opts)\n  if getmetatable(item) == M then\n    return item --[[@as snacks.picker.gh.Item]]\n  end\n  local self = setmetatable({}, M) --[[@as snacks.picker.gh.Item]]\n  for k, v in pairs(item) do\n    if v == vim.NIL then\n      item[k] = nil\n    end\n  end\n  self.item = item\n  self.opts = opts\n  self.type = opts.type\n  self.repo = opts.repo\n  self.fields = {}\n  for _, field in ipairs(opts.fields or {}) do\n    self.fields[field] = true\n  end\n  self:update()\n  return self --[[@as snacks.picker.gh.Item]]\nend\n\n---@param item any\nfunction M.is(item)\n  return getmetatable(item) == M\nend\n\nfunction M:__index(key)\n  if time_fields[key] then\n    return ts(self.item[time_fields[key]])\n  end\n  return rawget(M, key) or rawget(self.item, key)\nend\n\n---@param fields string[]\nfunction M:need(fields)\n  ---@param field string\n  return vim.tbl_filter(function(field)\n    return not self.fields[field]\n  end, fields)\nend\n\n---@param data? table<string, any>\n---@param fields? string[]\nfunction M:update(data, fields)\n  for k, v in pairs(data or {}) do\n    ---@diagnostic disable-next-line: no-unknown\n    self.item[k] = v ~= vim.NIL and v or nil\n  end\n  local item = self.item\n  for _, field in ipairs(fields or {}) do\n    if data and data[field] == nil then\n      self.item[field] = nil\n    end\n    self.fields[field] = true\n  end\n  if not self.repo and item.url then\n    local repo = M.get_repo(item.url)\n    if repo then\n      self.repo = repo\n    end\n  end\n  if self.repo then\n    self.uri = (\"gh://%s/%s/%s\"):format(self.repo, self.type, tostring(item.number or \"\"))\n    self.file = self.uri\n  end\n  self.author = item.author and item.author.login or nil\n  self.hash = item.number and (\"#\" .. tostring(item.number)) or nil\n  self.state = item.state and item.state:lower() or nil\n  self.status = self.state\n  self.state_reason = item.stateReason and item.stateReason:lower() or nil\n  self.draft = item.isDraft\n  self.label = item.labels\n      and table.concat(\n        ---@param label snacks.gh.Label\n        vim.tbl_map(function(label)\n          return label.name\n        end, item.labels),\n        \",\"\n      )\n    or nil\n  self.body = item.body and item.body:gsub(\"\\r\\n\", \"\\n\") or nil\n  vim.tbl_map(fix, item.comments or {})\n  self.pendingReview = nil\n  for _, review in ipairs(item.reviews or {}) do\n    fix(review)\n    if review.state == \"PENDING\" and review.viewerDidAuthor then\n      self.pendingReview = review\n    end\n    vim.tbl_map(fix, review.comments or {})\n  end\n\n  if item.reactionGroups then\n    self.reactions = {}\n    for _, reaction in ipairs(item.reactionGroups) do\n      table.insert(\n        self.reactions,\n        { content = reaction.content:lower(), count = reaction.users and reaction.users.totalCount or 0 }\n      )\n    end\n  end\n  if self.opts.transform then\n    self.opts.transform(self)\n  end\n  self.text = Snacks.picker.util.text(self.item, self.opts.text or self.opts.fields or {})\nend\n\n---@param item snacks.gh.api.View\nfunction M.to_uri(item)\n  if item.uri then\n    return item.uri\n  end\n  return (\"gh://%s/%s/%s\"):format(item.repo or \"\", assert(item.type), tostring(assert(item.number)))\nend\n\n---@param url string\nfunction M.get_repo(url)\n  local path = url:find(\"^http\") and url:gsub(\"^https?://[^/]+/\", \"\") or url:gsub(\"^[^/]+/\", \"\")\n  return path:match(\"([^/]+/[^/]+)\") --[[@as string?]]\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gh/render/init.lua",
    "content": "local Markdown = require(\"snacks.picker.util.markdown\")\n\nlocal M = {}\nlocal H = Snacks.picker.highlight\nlocal U = Snacks.picker.util\n\n-- tracking comment_skip is needed because review comments can appear both:\n-- 1. As top-level review.comments\n-- 2. As replies in the thread tree\n---@class snacks.gh.render.ctx\n---@field item snacks.picker.gh.Item\n---@field opts snacks.gh.Config\n---@field comment_skip table<string, boolean>\n---@field is_review? boolean\n---@field diff? boolean render diffs (defaults to true)\n---@field markdown? boolean render in a markdown buffer (defaults to true)\n---@field annotations? snacks.diff.Annotation[]\n\n---@param field string\nlocal function time_prop(field)\n  return {\n    name = U.title(field),\n    hl = function(item)\n      if not item[field] then\n        return\n      end\n      return { { U.reltime(item[field]), \"SnacksPickerGitDate\" } }\n    end,\n  }\nend\n\n---@type {name: string, hl:fun(item:snacks.picker.gh.Item, opts:snacks.gh.Config):snacks.picker.Highlight[]? }[]\nM.props = {\n  {\n    name = \"Status\",\n    hl = function(item, opts)\n      -- Status Icon\n      local icons = opts.icons[item.type]\n      local status = icons[item.status] and item.status or \"other\"\n      local ret = {} ---@type snacks.picker.Highlight[]\n      if status then\n        local icon = icons[status]\n        local hl = \"SnacksGh\" .. U.title(item.type) .. U.title(status)\n        local text = icon .. U.title(item.status or \"other\")\n        H.extend(ret, H.badge(text, { bg = Snacks.util.color(hl), fg = \"#ffffff\" }))\n      end\n      if item.baseRefName and item.headRefName then\n        ret[#ret + 1] = { \" \" }\n        vim.list_extend(ret, {\n          { item.baseRefName, \"SnacksGhBranch\" },\n          { \" ← \", \"SnacksGhDelim\" },\n          { item.headRefName, \"SnacksGhBranch\" },\n        })\n      end\n      return ret\n    end,\n  },\n  {\n    name = \"Repo\",\n    hl = function(item, opts)\n      return { { opts.icons.logo, \"Special\" }, { item.repo, \"@markup.link\" } }\n    end,\n  },\n  {\n    name = \"Author\",\n    hl = function(item, opts)\n      return H.badge(opts.icons.user .. \" \" .. item.author, \"SnacksGhUserBadge\")\n    end,\n  },\n  time_prop(\"created\"),\n  time_prop(\"updated\"),\n  time_prop(\"closed\"),\n  time_prop(\"merged\"),\n  {\n    name = \"Reactions\",\n    hl = function(item, opts)\n      if item.reactions then\n        local ret = {} ---@type snacks.picker.Highlight[]\n        table.sort(item.reactions, function(a, b)\n          return a.count > b.count\n        end)\n        for _, r in pairs(item.reactions) do\n          local badge = H.badge(opts.icons.reactions[r.content] .. \" \" .. tostring(r.count), \"SnacksGhReactionBadge\")\n          vim.list_extend(ret, badge)\n          ret[#ret + 1] = { \" \" }\n        end\n        return ret\n      end\n    end,\n  },\n  {\n    name = \"Labels\",\n    hl = function(item)\n      local ret = {} ---@type snacks.picker.Highlight[]\n      for _, label in ipairs(item.item.labels or {}) do\n        local color = label.color or \"888888\"\n        local badge = H.badge(label.name, \"#\" .. color)\n        H.extend(ret, badge)\n        ret[#ret + 1] = { \" \" }\n      end\n      return ret\n    end,\n  },\n  {\n    name = \"Assignees\",\n    hl = function(item)\n      local ret = {} ---@type snacks.picker.Highlight[]\n      for _, u in ipairs(item.item.assignees or {}) do\n        local badge = H.badge(u.login, \"Identifier\")\n        vim.list_extend(ret, badge)\n        ret[#ret + 1] = { \" \" }\n      end\n      return ret\n    end,\n  },\n  {\n    name = \"Milestone\",\n    hl = function(item)\n      if item.item.milestone then\n        return H.badge(item.item.milestone.title, \"Title\")\n      end\n    end,\n  },\n  {\n    name = \"Merge Status\",\n    hl = function(item, opts)\n      if not item.mergeStateStatus or item.state ~= \"open\" then\n        return\n      end\n      local status = item.mergeStateStatus:lower()\n      status = opts.icons.merge_status[status] and status or \"dirty\"\n      local icon = opts.icons.merge_status[status]\n      status = U.title(status)\n      local hl = \"SnacksGhPr\" .. status\n      return { { icon .. \" \" .. status, hl } }\n    end,\n  },\n  {\n    name = \"Checks\",\n    hl = function(item, opts)\n      if item.type ~= \"pr\" then\n        return\n      end\n      if #(item.statusCheckRollup or {}) == 0 then\n        return { { \" \" } }\n      end\n      local workflows = {} ---@type table<string, string>\n      for _, check in ipairs(item.statusCheckRollup or {}) do\n        local status, name = nil, nil ---@type string, string\n        if check.__typename == \"CheckRun\" then\n          name = check.workflowName .. \":\" .. check.name\n          status = check.status == \"COMPLETED\" and (check.conclusion or \"pending\") or check.status\n        elseif check.__typename == \"StatusContext\" then\n          name = check.context\n          status = check.state\n        end\n        if name and status then\n          status = U.title(status:lower())\n          workflows[name] = status\n        end\n      end\n      local stats = {} ---@type table<string, number>\n      for _, status in pairs(workflows) do\n        stats[status] = (stats[status] or 0) + 1\n      end\n      local ret = {} ---@type snacks.picker.Highlight[]\n      local order = { \"Success\", \"Failure\", \"Pending\", \"Skipped\" }\n      for _, status in ipairs(order) do\n        local count = stats[status]\n        if count then\n          local icon = opts.icons.checks[status:lower()] or opts.icons.checks[\"pending\"]\n          local badge = H.badge(icon .. \" \" .. tostring(count), \"SnacksGhCheck\" .. status)\n          vim.list_extend(ret, badge)\n          ret[#ret + 1] = { \" \" }\n        end\n      end\n      ret[#ret + 1] = { \" \" }\n      for _, status in ipairs(order) do\n        local count = stats[status]\n        if count then\n          ret[#ret + 1] = { string.rep(opts.icons.block, count), \"SnacksGhCheck\" .. status }\n        end\n      end\n      return ret\n    end,\n  },\n  {\n    name = \"Mergeable\",\n    hl = function(item, opts)\n      if not item.mergeable then\n        return\n      end\n      return {\n        {\n          (item.mergeable and opts.icons.checkmark or opts.icons.crossmark),\n          item.mergeable and \"SnacksGhPrClean\" or \"SnacksGhPrDirty\",\n        },\n      } or nil\n    end,\n  },\n  {\n    name = \"Changes\",\n    hl = function(item, opts)\n      if item.type ~= \"pr\" then\n        return\n      end\n      local ret = {} ---@type snacks.picker.Highlight[]\n\n      if item.changedFiles then\n        ret = H.badge(opts.icons.file .. item.changedFiles, \"SnacksGhStatBadge\")\n        ret[#ret + 1] = { \" \" }\n      end\n\n      if (item.additions or 0) > 0 then\n        ret[#ret + 1] = { \"+\" .. tostring(item.additions), \"SnacksGhAdditions\" }\n        ret[#ret + 1] = { \" \" }\n      end\n      if (item.deletions or 0) > 0 then\n        ret[#ret + 1] = { \"-\" .. tostring(item.deletions), \"SnacksGhDeletions\" }\n        ret[#ret + 1] = { \" \" }\n      end\n      if #ret == 0 then\n        return\n      end\n\n      if item.additions and item.deletions then\n        local unit = math.ceil((item.additions + item.deletions) / 5)\n        local additions = math.floor((0.5 + item.additions) / unit)\n        local deletions = math.floor((0.5 + item.deletions) / unit)\n        local neutral = 5 - additions - deletions\n\n        ret[#ret + 1] = { string.rep(opts.icons.block, additions), \"SnacksGhAdditions\" }\n        ret[#ret + 1] = { string.rep(opts.icons.block, deletions), \"SnacksGhDeletions\" }\n        ret[#ret + 1] = { string.rep(opts.icons.block, neutral), \"SnacksGhStat\" }\n      end\n\n      return ret\n    end,\n  },\n}\n\nlocal ns = vim.api.nvim_create_namespace(\"snacks.gh.render\")\n\n---@param buf number\n---@param item snacks.picker.gh.Item\n---@param opts snacks.gh.Config|{partial?:boolean}\nfunction M.render(buf, item, opts)\n  if not vim.api.nvim_buf_is_valid(buf) then\n    return\n  end\n\n  ---@type snacks.gh.render.ctx\n  local ctx = {\n    item = item,\n    opts = opts,\n    comment_skip = {},\n  }\n\n  local lines = {} ---@type snacks.picker.Highlight[][]\n\n  item.msg = item.title\n  ---@diagnostic disable-next-line: missing-fields\n  lines[#lines + 1] = Snacks.picker.format.commit_message(item, {})\n  vim.list_extend(lines[#lines], { { \" \" }, { item.hash, \"SnacksPickerDimmed\" } }) -- space after title\n  lines[#lines + 1] = {} -- empty line\n\n  for _, prop in ipairs(M.props) do\n    local value = prop.hl(item, opts)\n    if value and #value > 0 then\n      local line = {} ---@type snacks.picker.Highlight[]\n      line[#line + 1] = { prop.name, \"SnacksGhLabel\" }\n      line[#line + 1] = { \":\", \"SnacksGhDelim\" }\n      line[#line + 1] = { \" \" }\n      H.extend(line, value)\n      lines[#lines + 1] = line\n    end\n  end\n\n  lines[#lines + 1] = {} -- empty line\n  lines[#lines + 1] = { { \"---\", \"@punctuation.special.markdown\" } }\n  lines[#lines + 1] = {} -- empty line\n\n  do\n    local text = item.body or \"\"\n    text = text:gsub(\"<%!%-%-.-%-%->%s*\", \"\") -- remove html comments\n    local body = vim.split(text or \"\", \"\\n\")\n    while #body > 0 and body[1]:match(\"^%s*$\") do\n      table.remove(body, 1)\n    end\n    for _, l in ipairs(body) do\n      lines[#lines + 1] = { { l } }\n    end\n  end\n\n  local threads = M.get_threads(item)\n  if #threads > 0 then\n    lines[#lines + 1] = { { \"\" } } -- empty line\n    lines[#lines + 1] = { { \"---\", \"@punctuation.special.markdown\" } }\n    lines[#lines + 1] = {} -- empty line\n\n    for _, thread in ipairs(threads) do\n      local c = #lines\n\n      ctx.is_review = thread.state ~= nil\n      if ctx.is_review then\n        ---@cast thread snacks.gh.Review\n        vim.list_extend(lines, M.review(thread, ctx))\n      else\n        ---@cast thread snacks.gh.Comment\n        vim.list_extend(lines, M.comment(thread, ctx))\n      end\n\n      if #lines > c then -- only add separator if there were comments added\n        lines[#lines + 1] = {} -- empty line\n      end\n    end\n  end\n\n  local changed = H.render(buf, ns, lines)\n\n  if changed then\n    Markdown.render(buf, { bullets = false })\n  end\n\n  vim.schedule(function()\n    for _, win in ipairs(vim.fn.win_findbuf(buf)) do\n      vim.api.nvim_win_call(win, function()\n        if vim.wo.foldmethod == \"expr\" then\n          vim.wo.foldmethod = \"expr\"\n        end\n      end)\n    end\n  end)\nend\n\n---@param item snacks.picker.gh.Item\nfunction M.get_threads(item)\n  local ret = {} ---@type snacks.gh.Thread[]\n  vim.list_extend(ret, item.comments or {})\n  vim.list_extend(ret, item.reviews or {})\n  table.sort(ret, function(a, b)\n    return a.created < b.created\n  end)\n  return ret\nend\n\n---@param comment snacks.gh.Comment|snacks.gh.Review\n---@param opts? {text?:string}\n---@param ctx snacks.gh.render.ctx\nfunction M.comment_header(comment, opts, ctx)\n  opts = opts or {}\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local is_bot = comment.author.login == \"github-actions\" or comment.author.login:find(\"copilot\")\n  H.extend(\n    ret,\n    H.badge(\n      (\"%s %s\"):format(is_bot and ctx.opts.icons.logo or ctx.opts.icons.user, comment.author.login),\n      is_bot and \"SnacksGhBotBadge\" or \"SnacksGhUserBadge\"\n    )\n  )\n\n  if opts.text then\n    ret[#ret + 1] = { opts.text, \"SnacksGhCommentAction\" }\n    ret[#ret + 1] = { \" \" }\n  end\n  ret[#ret + 1] = { U.reltime(comment.created), \"SnacksPickerGitDate\" }\n  local assoc = comment.authorAssociation\n  assoc = assoc and assoc ~= \"NONE\" and U.title(assoc:lower()) or nil\n  assoc = comment.author.login == ctx.item.author and \"Author\" or assoc\n  if assoc then\n    ret[#ret + 1] = { \" \" }\n    H.extend(\n      ret,\n      H.badge(\n        assoc,\n        assoc == \"Author\" and \"SnacksGhAuthorBadge\" or assoc == \"Owner\" and \"SnacksGhOwnerBadge\" or \"SnacksGhAssocBadge\"\n      )\n    )\n  end\n  for _, r in ipairs(comment.reactionGroups or {}) do\n    ret[#ret + 1] = { \" \" }\n    local badge = H.badge(\n      ctx.opts.icons.reactions[r.content:lower()] .. \" \" .. tostring(r.users.totalCount),\n      \"SnacksGhReactionBadge\"\n    )\n    H.extend(ret, badge)\n  end\n  return ret\nend\n\n---@param item snacks.gh.Comment|snacks.gh.Review\n---@param ctx snacks.gh.render.ctx\nfunction M.comment_body(item, ctx)\n  local body = item.body or \"\"\n  if body:match(\"^%s*$\") then\n    return {}\n  end\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  local md = {} ---@type string[]\n  for _, line in ipairs(vim.split(body, \"\\n\", { plain = true })) do\n    if line:find(\"^```suggestion$\") then\n      local ft = item.path and vim.filetype.match({ filename = item.path }) or \"\"\n      line = \"```\" .. ft\n      ret[#ret + 1] = H.badge(\"Suggested change\", \"SnacksGhSuggestionBadge\")\n      md[#md + 1] = \"\"\n    end\n    md[#md + 1] = line\n    ret[#ret + 1] = { { line } }\n  end\n\n  if ctx.markdown == false then\n    -- if the filetype of the buffer is not markdown,\n    -- we need to add proper highlights for the markdown content\n    local extmarks = H.get_highlights({ code = table.concat(md, \"\\n\"), ft = \"markdown\" })\n    for l, line in pairs(extmarks) do\n      vim.list_extend(ret[l] or {}, line)\n    end\n  end\n  return ret\nend\n\n---@param lines snacks.picker.Highlight[][]\n---@param ctx snacks.gh.render.ctx\nfunction M.indent(lines, ctx)\n  -- indent guides for lines after the first\n  local indent = {} ---@type snacks.picker.Highlight[]\n  indent[#indent + 1] = { \"   \", \"Normal\" }\n  indent[#indent + 1] = {\n    col = 0,\n    virt_text = {\n      { \" \", \"Normal\" },\n      { \"┃\", { \"Normal\", \"@punctuation.definition.blockquote.markdown\" } },\n      { \" \", \"Normal\" },\n    },\n    virt_text_pos = \"overlay\",\n    hl_mode = \"combine\",\n    virt_text_repeat_linebreak = true,\n  }\n\n  --- first indent. In a markdown buffer, we need proper structure,\n  --- so we conceal the list marker\n  ---@type snacks.picker.Highlight[]\n  local first = ctx.markdown == false and {}\n    or {\n      {\n        col = 0,\n        end_col = 3,\n        conceal = \"\",\n        priority = 1000,\n      },\n      { \" * \", \"Normal\" },\n    }\n\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  for l, line in ipairs(lines) do\n    local new = vim.deepcopy(l == 1 and first or indent)\n    H.extend(new, line)\n    ret[l] = new\n  end\n  return ret\nend\n\n---@param comment snacks.gh.Comment\n---@param ctx snacks.gh.render.ctx\nfunction M.comment_diff(comment, ctx)\n  if not comment.path or not comment.diffHunk then\n    return {}\n  end\n  local count = 1\n  local originalLine = comment.originalLine or comment.line or 1\n  if comment.originalStartLine then\n    count = originalLine - comment.originalStartLine + 1\n  end\n  count = math.max(ctx.opts.diff.min, math.abs(count))\n\n  local Diff = require(\"snacks.picker.util.diff\")\n  local diff = (\"diff --git a/%s b/%s\\n%s\"):format(comment.path, comment.path, comment.diffHunk)\n  local ret = Diff.format(diff, {\n    max_hunk_lines = count,\n    hunk_header = false,\n  })\n  table.insert(ret, 1, { { \"```\" } })\n  table.insert(ret, { { \"```\" } })\n  return ret\nend\n\n---@param comment snacks.gh.Comment\n---@param ctx snacks.gh.render.ctx\nfunction M.annotate(comment, ctx)\n  if not comment.path or not comment.diffHunk then\n    return\n  end\n  local side = \"right\"\n  for _, thread in ipairs(ctx.item.reviewThreads or {}) do\n    for _, c in ipairs(thread.comments or {}) do\n      if c.id == comment.id then\n        side = (thread.diffSide or \"RIGHT\"):lower()\n        break\n      end\n    end\n  end\n  ---@type snacks.diff.Annotation\n  local ret = {\n    side = side,\n    file = comment.path,\n    line = comment.line or comment.originalLine or 1,\n    text = {},\n  }\n  ctx.annotations = ctx.annotations or {}\n  table.insert(ctx.annotations, ret)\n  return ret\nend\n\n---@param comment snacks.gh.Comment\n---@param ctx snacks.gh.render.ctx\nfunction M.comment(comment, ctx)\n  local ret = {} ---@type snacks.picker.Highlight[][]\n\n  local header = {} ---@type snacks.picker.Highlight[]\n  H.extend(header, M.comment_header(comment, {}, ctx))\n  ret[#ret + 1] = header\n\n  local annotation ---@type snacks.diff.Annotation?\n  if not comment.replyTo then\n    annotation = M.annotate(comment, ctx)\n    if ctx.diff ~= false then\n      -- add diff hunk for top-level comments\n      local diff = M.comment_diff(comment, ctx)\n      if #diff > 0 then\n        vim.list_extend(ret, diff)\n        ret[#ret + 1] = {} -- empty line between diff and body\n      end\n    end\n  end\n\n  vim.list_extend(ret, M.comment_body(comment, ctx))\n  local replies = M.find_reply(comment.id, ctx)\n  for _, reply in ipairs(replies) do\n    ret[#ret + 1] = {} -- empty line between comment and reply\n    vim.list_extend(ret, M.comment(reply, ctx))\n    ctx.comment_skip[reply.id] = true\n  end\n  if ctx.is_review then\n    for _, line in ipairs(ret) do\n      local reply_id = comment.replyTo and comment.replyTo.databaseId or comment.databaseId\n      if reply_id then\n        line[#line + 1] = { \"\", meta = { comment_id = reply_id } }\n      end\n    end\n  end\n  ret = M.indent(ret, ctx)\n  if annotation then\n    annotation.text = vim.deepcopy(ret)\n  end\n  return ret\nend\n\n---@param id string\n---@param ctx snacks.gh.render.ctx\nfunction M.find_reply(id, ctx)\n  local ret = {} ---@type snacks.gh.Comment[]\n  for _, review in ipairs(ctx.item.reviews or {}) do\n    for _, comment in ipairs(review.comments or {}) do\n      if comment.replyTo and comment.replyTo.id == id then\n        ret[#ret + 1] = comment\n      end\n    end\n  end\n  return ret\nend\n\n---@param review snacks.gh.Review\n---@param ctx snacks.gh.render.ctx\nfunction M.review(review, ctx)\n  local ret = {} ---@type snacks.picker.Highlight[][]\n\n  ---@type snacks.gh.Comment[]\n  local comments = vim.tbl_filter(function(c)\n    return not ctx.comment_skip[c.id]\n  end, review.comments or {})\n\n  if #comments == 0 and review.state == \"COMMENTED\" and ((review.body or \"\"):match(\"^%s*$\")) then\n    return ret\n  end\n\n  local header = {} ---@type snacks.picker.Highlight[]\n  local state_icon = ctx.opts.icons.review[review.state:lower()] or ctx.opts.icons.pr.open\n  H.extend(header, H.badge(state_icon, \"SnacksGhReview\" .. U.title(review.state:lower()):gsub(\" \", \"\")))\n  header[#header + 1] = { \" \" }\n  local texts = {\n    [\"CHANGES_REQUESTED\"] = \"requested changes\",\n    [\"COMMENTED\"] = \"reviewed\",\n  }\n\n  local text = texts[review.state] or review.state:lower():gsub(\"_\", \" \")\n  H.extend(header, M.comment_header(review, { text = text }, ctx))\n  ret[#ret + 1] = header\n  vim.list_extend(ret, M.comment_body(review, ctx))\n  for _, comment in ipairs(comments) do\n    ret[#ret + 1] = {} -- empty line between review and comments\n    vim.list_extend(ret, M.comment(comment, ctx))\n  end\n  return M.indent(ret, ctx)\nend\n\n---@param pr snacks.picker.gh.Item\nfunction M.annotations(pr)\n  ---@type snacks.gh.render.ctx\n  local ctx = {\n    item = pr,\n    opts = Snacks.gh.config(),\n    comment_skip = {},\n    is_review = true,\n    diff = false,\n    markdown = false,\n  }\n  for _, review in ipairs(pr.reviews or {}) do\n    M.review(review, ctx)\n  end\n  return ctx.annotations\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gh/types.lua",
    "content": "---@class snacks.gh.api.Config\n---@field type \"issue\" | \"pr\"\n---@field repo? string\n---@field fields string[]\n---@field view string[] -- fields to fetch for gh view\n---@field list string[] -- fields to fetch for gh list\n---@field text string[]\n---@field options string[]\n---@field transform? fun(item: snacks.picker.gh.Item): snacks.picker.gh.Item?\n\n---@class snacks.picker.gh.list.Config: snacks.picker.gh.Config\n---@field type \"issue\" | \"pr\"\n\n---@class snacks.picker.gh.api.Config: snacks.picker.gh.Config\n---@field api snacks.gh.api.Api\n---@field transform? fun(item: snacks.picker.finder.Item): snacks.picker.finder.Item?\n\n---@alias snacks.gh.api.View snacks.picker.gh.Item|{number: number, type: string, repo: string}\n\n---@class snacks.gh.api.Cmd\n---@field args string[]\n---@field repo? string\n---@field input? string\n---@field notify? boolean\n---@field on_error? fun(proc: snacks.spawn.Proc, err: string)\n\n---@class snacks.gh.api.Api\n---@field endpoint string\n---@field cache? string cache the response, e.g. \"3600s\", \"1h\"\n---@field fields? table<string, string|number|boolean> raw fields (--raw-field)\n---@field params? table<string, string|number|boolean> typed fields (--field)\n---@field header? table<string, string|number|boolean>\n---@field jq? string\n---@field input? any\n---@field method? \"GET\" | \"POST\" | \"PATCH\" | \"PUT\" | \"DELETE\"\n---@field paginate? boolean\n---@field silent? boolean\n---@field slurp? boolean\n---@field on_error? fun(proc: snacks.spawn.Proc, err: string)\n\n---@class snacks.gh.api.GraphQL: snacks.gh.api.Api\n---@field endpoint? nil -- should be \"/graphql\"\n---@field query string\n\n---@alias snacks.gh.Field {arg:string, prop:string, name:string}\n\n---@class snacks.gh.cli.Action: snacks.gh.api.Cmd\n---@field args? string[]\n---@field stdin? boolean -- whether to write to stdin\n---@field edit? string field to edit\n---@field api? snacks.gh.api.Api -- api options\n---@field cmd? string -- subcommand to run (e.g., \"issue edit\" or \"pr comment\")\n---@field fields? snacks.gh.Field[] -- field args to parse from the body\n---@field title? string -- title of the scratch buffer\n---@field template? string -- template to use for the scratch buffer\n---@field desc? string -- description to show in the scratch buffer\n---@field icon? string -- icon to show in the scratch buffer\n---@field type? \"issue\" | \"pr\" -- action for items of this type (nil means both)\n---@field enabled? fun(item: snacks.picker.gh.Item, ctx: snacks.gh.action.ctx): boolean -- whether the action is enabled for the item\n---@field success? string -- success message to show after the action\n---@field confirm? string -- confirmation message to show before performing the action\n---@field refresh? boolean -- whether to refresh the item after performing the action (default: true)\n---@field on_submit? fun(body: string, ctx: snacks.gh.cli.Action.ctx): string?\n\n---@class snacks.gh.api.Fetch: snacks.gh.api.Cmd\n---@field fields string[]\n\n---@alias snacks.gh.Reaction { content: string, users: { totalCount: number } }\n\n---@class snacks.gh.Label\n---@field id string\n---@field name string\n---@field color string\n---@field description? string\n\n---@class snacks.gh.User\n---@field id string\n---@field login string\n---@field name string\n---@field is_bot? boolean\n\n---@class snacks.gh.Check\n---@field __typename \"CheckRun\" | \"StatusContext\"\n---@field completedAt? string\n---@field conclusion? \"SUCCESS\" | \"FAILURE\" | \"SKIPPED\"\n---@field detailsUrl? string\n---@field name string\n---@field startedAt? string\n---@field status \"PENDING\" | \"COMPLETED\"\n---@field workflowName string\n---@field context? string\n---@field state? \"SUCCESS\" | \"FAILURE\" | \"PENDING\"\n\n---@class snacks.gh.review.Thread\n---@field id string\n---@field diffSide \"LEFT\" | \"RIGHT\"\n---@field comments {id: string}[]\n\n---@class snacks.gh.Review\n---@field id string\n---@field databaseId number\n---@field author snacks.gh.User\n---@field authorAssociation string\n---@field body string\n---@field createdAt string\n---@field submittedAt string\n---@field submitted number\n---@field created number\n---@field reactionGroups? snacks.gh.Reaction[]\n---@field state \"APPROVED\" | \"CHANGES_REQUESTED\" | \"COMMENTED\" | \"DISMISSED\" | \"PENDING\"\n---@field commit? {oid: string}\n---@field comments? snacks.gh.Comment[]\n---@field viewerDidAuthor? boolean -- whether the viewer authored the review\n\n---@alias snacks.gh.Thread snacks.gh.Comment|snacks.gh.Review\n\n---@class snacks.gh.Item\n---@field number number\n---@field id string\n---@field title string\n---@field labels? snacks.gh.Label[]\n---@field author? snacks.gh.User\n---@field state string\n---@field stateReason? string\n---@field updatedAt string\n---@field url string\n---@field reactionGroups? snacks.gh.Reaction[]\n---@field body? string\n---@field comments? snacks.gh.Comment[]\n---@field changedFiles? number\n---@field additions? number\n---@field deletions? number\n---@field mergeStateStatus? string\n---@field mergeable? boolean\n---@field commits? snacks.gh.Commit[]\n---@field statusCheckRollup? snacks.gh.Check[]\n---@field baseRefName? string\n---@field headRefName? string\n---@field headRefOid? string\n---@field isDraft? boolean\n---@field reviews? snacks.gh.Review[]\n---@field reviewThreads? snacks.gh.review.Thread[]\n---@field pendingReview? snacks.gh.Review\n\n---@class snacks.gh.Commit\n---@field oid string\n---@field messageHeadline string\n---@field messageBody? string\n---@field committedDate string\n---@field authors? snacks.gh.User[]\n---@field authoredDate string\n\n---@class snacks.gh.Comment\n---@field id string\n---@field databaseId number\n---@field url string\n---@field author { login: string }\n---@field authorAssociation? string\n---@field includesCreatedEdit? boolean\n---@field viewerDidAuthor? boolean\n---@field isMinimized? boolean\n---@field minimizedReason? string\n---@field body string\n---@field createdAt string\n---@field reactionGroups? snacks.gh.Reaction[]\n---@field created? number\n---@field replyTo? {id: string, databaseId: number}\n---@field path? string\n---@field diffHunk? string\n---@field line? number\n---@field originalLine? number\n---@field originalStartLine? number\n\n---@class snacks.picker.gh.Item: snacks.picker.Item,snacks.gh.Item,snacks.picker.finder.Item\n---@field type \"issue\" | \"pr\"\n---@field dirty? boolean\n---@field uri string\n---@field repo? string\n---@field hash string\n---@field status string\n---@field author? string\n---@field label? string\n---@field status_reason? string\n---@field item snacks.gh.Item\n---@field body? string\n---@field reactions? {content: string, count: number}[]\n---@field fields table<string, boolean>\n---@field created number\n---@field updated number\n---@field closed? number\n---@field merged? number\n---@field draft? boolean\n\n---@class snacks.gh.api.Branch\n---@field url string URL of the remote branch\n---@field author? string owner of the remote branch\n---@field repo? string owner/name format\n---@field branch string local branch name\n---@field base string branch we want to merge into\n---@field head string branch we want to merge from\n"
  },
  {
    "path": "lua/snacks/git.lua",
    "content": "---@class snacks.git\nlocal M = {}\n\nM.meta = {\n  desc = \"Git utilities\",\n}\n\nSnacks.config.style(\"blame_line\", {\n  width = 0.6,\n  height = 0.6,\n  border = true,\n  title = \" Git Blame \",\n  title_pos = \"center\",\n  ft = \"git\",\n})\n\nlocal git_cache = {} ---@type table<string, boolean>\nlocal function is_git_root(dir)\n  if git_cache[dir] == nil then\n    git_cache[dir] = (vim.uv or vim.loop).fs_stat(dir .. \"/.git\") ~= nil\n  end\n  return git_cache[dir]\nend\n\n--- Gets the git root for a buffer or path.\n--- Defaults to the current buffer.\n---@param path? number|string buffer or path\n---@return string?\nfunction M.get_root(path)\n  path = path or 0\n  path = type(path) == \"number\" and vim.api.nvim_buf_get_name(path) or path --[[@as string]]\n  path = path == \"\" and (vim.uv or vim.loop).cwd() or path\n  path = svim.fs.normalize(path)\n\n  if is_git_root(path) then\n    return path\n  end\n\n  for dir in vim.fs.parents(path) do\n    if is_git_root(dir) then\n      return svim.fs.normalize(dir)\n    end\n  end\n\n  return os.getenv(\"GIT_WORK_TREE\")\nend\n\n--- Show git log for the current line.\n---@param opts? snacks.terminal.Opts | {count?: number}\nfunction M.blame_line(opts)\n  opts = vim.tbl_deep_extend(\"force\", {\n    count = 5,\n    interactive = false,\n    win = { style = \"blame_line\" },\n  }, opts or {})\n  local cursor = vim.api.nvim_win_get_cursor(0)\n  local line = cursor[1]\n  local file = vim.api.nvim_buf_get_name(0)\n  local root = M.get_root()\n  local cmd = { \"git\", \"-C\", root, \"log\", \"-n\", opts.count, \"-u\", \"-L\", line .. \",+1:\" .. file }\n  return Snacks.terminal(cmd, opts)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/gitbrowse.lua",
    "content": "---@class snacks.gitbrowse\n---@overload fun(opts?: snacks.gitbrowse.Config)\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.open(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Open the current file, branch, commit, or repo in a browser (e.g. GitHub, GitLab, Bitbucket)\",\n}\n\nlocal uv = vim.uv or vim.loop\n\n---@class snacks.gitbrowse.Config\n---@field url_patterns? table<string, table<string, string|fun(fields:snacks.gitbrowse.Fields):string>>\nlocal defaults = {\n  notify = true, -- show notification on open\n  -- Handler to open the url in a browser\n  ---@param url string\n  open = function(url)\n    if vim.fn.has(\"nvim-0.10\") == 0 then\n      require(\"lazy.util\").open(url, { system = true })\n      return\n    end\n    vim.ui.open(url)\n  end,\n  ---@type \"repo\" | \"branch\" | \"file\" | \"commit\" | \"permalink\"\n  what = \"commit\", -- what to open. not all remotes support all types\n  commit = nil, ---@type string?\n  branch = nil, ---@type string?\n  line_start = nil, ---@type number?\n  line_end = nil, ---@type number?\n  -- patterns to transform remotes to an actual URL\n  -- stylua: ignore\n  remote_patterns = {\n    { \"^(https?://.*)%.git$\"              , \"%1\" },\n    { \"^git@(.+):(.+)%.git$\"              , \"https://%1/%2\" },\n    { \"^git@(.+):(.+)$\"                   , \"https://%1/%2\" },\n    { \"^git@(.+)/(.+)$\"                   , \"https://%1/%2\" },\n    { \"^org%-%d+@(.+):(.+)%.git$\"         , \"https://%1/%2\" },\n    { \"^ssh://git@(.*)$\"                  , \"https://%1\" },\n    { \"^ssh://([^:/]+)(:%d+)/(.*)$\"       , \"https://%1/%3\" },\n    { \"^ssh://([^/]+)/(.*)$\"              , \"https://%1/%2\" },\n    { \"ssh%.dev%.azure%.com/v3/(.*)/(.*)$\", \"dev.azure.com/%1/_git/%2\" },\n    { \"^https://%w*@(.*)\"                 , \"https://%1\" },\n    { \"^git@(.*)\"                         , \"https://%1\" },\n    { \":%d+\"                              , \"\" },\n    { \"%.git$\"                            , \"\" },\n  },\n  url_patterns = {\n    [\"github%.com\"] = {\n      branch = \"/tree/{branch}\",\n      file = \"/blob/{branch}/{file}#L{line_start}-L{line_end}\",\n      permalink = \"/blob/{commit}/{file}#L{line_start}-L{line_end}\",\n      commit = \"/commit/{commit}\",\n    },\n    [\"gitlab%.com\"] = {\n      branch = \"/-/tree/{branch}\",\n      file = \"/-/blob/{branch}/{file}#L{line_start}-{line_end}\",\n      permalink = \"/-/blob/{commit}/{file}#L{line_start}-{line_end}\",\n      commit = \"/-/commit/{commit}\",\n    },\n    [\"bitbucket%.org\"] = {\n      branch = \"/src/{branch}\",\n      file = \"/src/{branch}/{file}#lines-{line_start}-L{line_end}\",\n      permalink = \"/src/{commit}/{file}#lines-{line_start}-L{line_end}\",\n      commit = \"/commits/{commit}\",\n    },\n    [\"git.sr.ht\"] = {\n      branch = \"/tree/{branch}\",\n      file = \"/tree/{branch}/item/{file}\",\n      permalink = \"/tree/{commit}/item/{file}#L{line_start}\",\n      commit = \"/commit/{commit}\",\n    },\n  },\n}\n\n---@class snacks.gitbrowse.Fields\n---@field branch? string\n---@field file? string\n---@field line_start? number\n---@field line_end? number\n---@field commit? string\n---@field line_count? number\n\n---@private\n---@param remote string\n---@param opts? snacks.gitbrowse.Config\nfunction M.get_repo(remote, opts)\n  opts = Snacks.config.get(\"gitbrowse\", defaults, opts)\n  local ret = remote\n  for _, pattern in ipairs(opts.remote_patterns) do\n    ret = ret:gsub(pattern[1], pattern[2]) --[[@as string]]\n  end\n  return ret:find(\"https://\") == 1 and ret or (\"https://%s\"):format(ret)\nend\n\n---@param repo string\n---@param fields snacks.gitbrowse.Fields\n---@param opts? snacks.gitbrowse.Config\nfunction M.get_url(repo, fields, opts)\n  opts = Snacks.config.get(\"gitbrowse\", defaults, opts)\n  for remote, patterns in pairs(opts.url_patterns) do\n    if repo:find(remote) then\n      local pattern = patterns[opts.what]\n      if type(pattern) == \"string\" then\n        return repo .. pattern:gsub(\"(%b{})\", function(key)\n          return fields[key:sub(2, -2)] or key\n        end)\n      elseif type(pattern) == \"function\" then\n        return repo .. pattern(fields)\n      end\n    end\n  end\n  return repo\nend\n\n---@param cmd string[]\n---@param err string\nlocal function system(cmd, err)\n  local proc = vim.fn.system(cmd)\n  if vim.v.shell_error ~= 0 then\n    Snacks.notify.error({ err, proc }, { title = \"Git Browse\" })\n    error(\"__ignore__\")\n  end\n  return vim.split(vim.trim(proc), \"\\n\")\nend\n\n---@param hash string\n---@param cwd string\n---@return boolean\nlocal function is_valid_commit_hash(hash, cwd)\n  if not (hash:match(\"^[a-fA-F0-9]+$\") and #hash >= 7) then\n    return false\n  end\n  system({ \"git\", \"-C\", cwd, \"rev-parse\", \"--verify\", hash }, \"Invalid commit hash\")\n  return true\nend\n\n---@param opts? snacks.gitbrowse.Config\nfunction M.open(opts)\n  local ok, err = pcall(M._open, opts) -- errors are handled with notifications\n  if not ok and err ~= \"__ignore__\" then\n    error(err)\n  end\nend\n\n---@param opts? snacks.gitbrowse.Config\nfunction M._open(opts)\n  opts = Snacks.config.get(\"gitbrowse\", defaults, opts)\n  local file = vim.api.nvim_buf_get_name(0) ---@type string?\n  file = file and (uv.fs_stat(file) or {}).type == \"file\" and svim.fs.normalize(file) or nil\n  local cwd = file and vim.fn.fnamemodify(file, \":h\") or vim.fn.getcwd()\n\n  ---@type snacks.gitbrowse.Fields\n  local fields = {\n    branch = opts.branch\n      or system({ \"git\", \"-C\", cwd, \"rev-parse\", \"--abbrev-ref\", \"HEAD\" }, \"Failed to get current branch\")[1],\n    file = file and system({ \"git\", \"-C\", cwd, \"ls-files\", \"--full-name\", file }, \"Failed to get git file path\")[1],\n    line_start = opts.line_start,\n    line_end = opts.line_end,\n    commit = opts.commit,\n  }\n\n  if not fields.commit then\n    if opts.what == \"permalink\" then\n      fields.commit = system(\n        { \"git\", \"-C\", cwd, \"log\", \"-n\", \"1\", \"--pretty=format:%H\", \"--\", file },\n        \"Failed to get latest commit of file\"\n      )[1]\n    else\n      local word = vim.fn.expand(\"<cword>\")\n      fields.commit = is_valid_commit_hash(word, cwd) and word or nil\n    end\n  end\n\n  -- Get visual selection range if in visual mode\n  if vim.fn.mode():find(\"[vV]\") then\n    vim.fn.feedkeys(\":\", \"nx\")\n    local line_start = vim.api.nvim_buf_get_mark(0, \"<\")[1]\n    local line_end = vim.api.nvim_buf_get_mark(0, \">\")[1]\n    vim.fn.feedkeys(\"gv\", \"nx\")\n    -- Ensure line_start is always the smaller number\n    if line_start > line_end then\n      line_start, line_end = line_end, line_start\n    end\n    fields.line_start = line_start\n    fields.line_end = line_end\n  else\n    fields.line_start = fields.line_start or vim.fn.line(\".\")\n    fields.line_end = fields.line_end or fields.line_start\n  end\n  fields.line_count = fields.line_end - fields.line_start + 1\n\n  if not fields.commit and (opts.what == \"commit\" or opts.what == \"permalink\") then\n    opts.what = \"file\"\n  end\n  if not fields.commit and not fields.file then\n    opts.what = \"branch\"\n  end\n  if not fields.commit and not fields.branch then\n    opts.what = \"repo\"\n  end\n\n  local remotes = {} ---@type {name:string, url:string}[]\n\n  for _, line in ipairs(system({ \"git\", \"-C\", cwd, \"remote\", \"-v\" }, \"Failed to get git remotes\")) do\n    local name, remote = line:match(\"(%S+)%s+(%S+)%s+%(fetch%)\")\n    if name and remote then\n      local repo = M.get_repo(remote, opts)\n      if repo then\n        table.insert(remotes, {\n          name = name,\n          url = M.get_url(repo, fields, opts),\n        })\n      end\n    end\n  end\n\n  local function open(remote)\n    if remote then\n      if opts.notify ~= false then\n        Snacks.notify((\"Opening [%s](%s)\"):format(remote.name, remote.url), { title = \"Git Browse\" })\n      end\n      opts.open(remote.url)\n    end\n  end\n\n  if #remotes == 0 then\n    return Snacks.notify.error(\"No git remotes found\", { title = \"Git Browse\" })\n  elseif #remotes == 1 then\n    return open(remotes[1])\n  end\n\n  vim.ui.select(remotes, {\n    prompt = \"Select remote to browse\",\n    format_item = function(item)\n      return item.name .. (\" \"):rep(8 - #item.name) .. \" 🔗 \" .. item.url\n    end,\n  }, open)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/health.lua",
    "content": "---@class snacks.health\n---@field ok fun(msg: string)\n---@field warn fun(msg: string)\n---@field error fun(msg: string)\n---@field info fun(msg: string)\n---@field start fun(msg: string)\nlocal M = setmetatable({}, {\n  __index = function(M, k)\n    return function(msg)\n      return require(\"vim.health\")[k](M.prefix .. msg)\n    end\n  end,\n})\n\n---@class snacks.health.Tool\n---@field cmd string|string[]\n---@field version? string|false\n---@field enabled? boolean\n\n---@alias snacks.health.Tool.spec (string|snacks.health.Tool)[]|snacks.health.Tool|string\n\nM.prefix = \"\"\n\nM.meta = {\n  desc = \"Snacks health checks\",\n  readme = false,\n  health = false,\n}\n\nfunction M.check()\n  M.prefix = \"\"\n  M.start(\"Snacks\")\n  if Snacks.did_setup then\n    M.ok(\"setup called\")\n    if Snacks.did_setup_after_vim_enter then\n      M.warn(\"setup called *after* `VimEnter`\")\n    end\n  else\n    M.error(\"setup not called\")\n  end\n  if package.loaded.lazy then\n    local plugin = require(\"lazy.core.config\").spec.plugins[\"snacks.nvim\"]\n    if plugin then\n      if plugin.lazy ~= false then\n        M.warn(\"`snacks.nvim` should not be lazy-loaded. Add `lazy=false` to the plugin spec\")\n      end\n      if (plugin.priority or 0) < 1000 then\n        M.warn(\"`snacks.nvim` should have a priority of 1000 or higher. Add `priority=1000` to the plugin spec\")\n      end\n    else\n      M.error(\"`snacks.nvim` not found in lazy\")\n    end\n  end\n  for _, plugin in ipairs(Snacks.meta.get()) do\n    local opts = Snacks.config[plugin.name] or {} --[[@as {enabled?: boolean}]]\n    if plugin.meta.health ~= false and (plugin.meta.needs_setup or plugin.health) then\n      M.start((\"Snacks.%s\"):format(plugin.name))\n      -- M.prefix = (\"`Snacks.%s` \"):format(name)\n      if plugin.meta.needs_setup then\n        if opts.enabled then\n          M.ok(\"setup {enabled}\")\n        else\n          M.warn(\"setup {disabled}\")\n        end\n      end\n      if plugin.health then\n        plugin.health()\n      end\n    end\n  end\nend\n\n--- Check if any of the tools are available, with an optional version check\n---@param tools snacks.health.Tool.spec\nfunction M.have_tool(tools)\n  tools = type(tools) == \"string\" and { tools } or tools\n  tools = tools[1] and tools or { tools }\n  ---@cast tools (string|snacks.health.Tool)[]\n  tools = vim.tbl_map(function(tool)\n    return type(tool) == \"string\" and { cmd = tool } or tool\n  end, tools)\n  ---@cast tools snacks.health.Tool[]\n\n  local all = {} ---@type string[]\n  local found = false\n  local version_ok = false\n  for _, tool in ipairs(tools) do\n    if tool.enabled ~= false then\n      local tool_version = tool.version and vim.version.parse(tool.version)\n      local cmds = type(tool.cmd) == \"string\" and { tool.cmd } or tool.cmd --[[@as string[] ]]\n      vim.list_extend(all, cmds)\n      for _, cmd in ipairs(cmds) do\n        if vim.fn.executable(cmd) == 1 then\n          local version = tool.version == false and \"\" or vim.fn.system(cmd .. \" --version\") or \"\"\n          version = vim.trim(vim.split(version, \"\\n\")[1])\n          if tool_version and tool_version > vim.version.parse(version) then\n            M.error(\"'\" .. cmd .. \"' `\" .. version .. \"` is too old, expected `\" .. tool.version .. \"`\")\n          elseif tool.version == false then\n            M.ok(\"'\" .. cmd .. \"'\")\n            version_ok = true\n          else\n            M.ok(\"'\" .. cmd .. \"' `\" .. version .. \"`\")\n            version_ok = true\n          end\n          found = true\n        end\n      end\n    end\n  end\n  if found then\n    return true, version_ok\n  end\n  all = vim.tbl_map(function(t)\n    return \"'\" .. tostring(t) .. \"'\"\n  end, all)\n  if #all == 1 then\n    M.error(\"Tool not found: \" .. all[1])\n  else\n    M.error(\"None of the tools found: \" .. table.concat(all, \", \"))\n  end\n  return false\nend\n\n--- Check if the given languages are available in treesitter\n---@param langs string[]|string\nfunction M.has_lang(langs)\n  langs = type(langs) == \"string\" and { langs } or langs --[[@as string[] ]]\n  local ret = {} ---@type table<string, boolean>\n  local available, missing = {}, {} ---@type string[], string[]\n  for _, lang in ipairs(langs) do\n    local has_lang = Snacks.util.get_lang(lang) ~= nil\n    ret[lang] = has_lang\n    lang = (\"`%s`\"):format(lang)\n    if has_lang then\n      available[#available + 1] = lang\n    else\n      missing[#missing + 1] = lang\n    end\n  end\n  table.sort(available)\n  table.sort(missing)\n  if #available > 0 then\n    M.ok(\"Available Treesitter languages:\\n  \" .. table.concat(available, \", \"))\n  end\n  if #missing > 0 then\n    M.warn(\"Missing Treesitter languages:\\n  \" .. table.concat(missing, \", \"))\n  end\n  return ret, #available, #missing\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/buf.lua",
    "content": "---@class snacks.image.buf\nlocal M = {}\n\n---@param buf number\n---@param opts? snacks.image.Opts|{src?: string}\nfunction M._attach(buf, opts)\n  Snacks.image.placement.clean(buf)\n  if not vim.api.nvim_buf_is_valid(buf) then\n    return\n  end\n  opts = opts or {}\n  local file = opts.src or vim.api.nvim_buf_get_name(buf)\n  if not Snacks.image.supports(file) then\n    local lines = {} ---@type string[]\n    lines[#lines + 1] = \"# Image viewer\"\n    lines[#lines + 1] = \"- **file**: `\" .. file .. \"`\"\n    if not Snacks.image.supports_file(file) then\n      lines[#lines + 1] = \"- unsupported image format\"\n    end\n    if not Snacks.image.supports_terminal() then\n      lines[#lines + 1] = \"- terminal does not support the kitty graphics protocol.\"\n      lines[#lines + 1] = \"  See `:checkhealth snacks` for more info.\"\n    end\n    vim.bo[buf].modifiable = true\n    vim.bo[buf].filetype = \"markdown\"\n    vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(table.concat(lines, \"\\n\"), \"\\n\"))\n    vim.bo[buf].modifiable = false\n    vim.bo[buf].modified = false\n  else\n    Snacks.util.bo(buf, {\n      filetype = \"image\",\n      modifiable = false,\n      modified = false,\n      swapfile = false,\n    })\n    opts.conceal = true\n    opts.auto_resize = true\n    return Snacks.image.placement.new(buf, file, opts)\n  end\nend\n\n---@param buf number\n---@param opts? snacks.image.Opts|{src?: string}\nfunction M.attach(buf, opts)\n  if Snacks.image.config.enabled == false then\n    return\n  end\n  local Terminal = require(\"snacks.image.terminal\")\n  Terminal.detect(function()\n    M._attach(buf, opts)\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/convert.lua",
    "content": "local Spawn = require(\"snacks.util.spawn\")\n\n---@class snacks.image.convert\nlocal M = {}\n\nlocal uv = vim.uv or vim.loop\n\n---@class snacks.image.Info\n---@field format string\n---@field size snacks.image.Size\n---@field dpi snacks.image.Size\n\n---@class snacks.image.convert.Opts\n---@field src string\n---@field on_done? fun(convert: snacks.image.Convert)\n\n---@class snacks.image.meta\n---@field src string\n---@field info? snacks.image.Info\n---@field [string] string|number|boolean\n\n---@alias snacks.image.args (number|string)[] | fun(): ((number|string)[])\n\n---@class snacks.image.Proc\n---@field cmd string\n---@field cwd? string\n---@field args snacks.image.args\n\n---@class snacks.image.step\n---@field name string\n---@field file string\n---@field ft string\n---@field cmd snacks.image.cmd\n---@field meta snacks.image.meta\n---@field done? boolean\n---@field err? string\n---@field proc? snacks.spawn.Proc\n\n---@class snacks.image.cmd\n---@field cmd (fun(step: snacks.image.step):(snacks.image.Proc|snacks.image.Proc[]))|snacks.image.Proc|snacks.image.Proc[]\n---@field ft? string\n---@field file? fun(convert: snacks.image.Convert, meta: snacks.image.meta): string\n---@field depends? string[]\n---@field on_done? fun(step: snacks.image.step)\n---@field on_error? fun(step: snacks.image.step):boolean? when return true, continue to next step\n---@field pipe? boolean\n\n---@type table<string, snacks.image.cmd>\nlocal commands = {\n  icns = {\n    ft = \"png\",\n    cmd = {\n      {\n        cmd = \"sips\",\n        args = { \"-s\", \"format\", \"png\", \"{src}\", \"--out\", \"{file}\" },\n      },\n    },\n  },\n  url = {\n    cmd = {\n      {\n        cmd = \"curl\",\n        args = { \"-L\", \"-o\", \"{file}\", \"{src}\" },\n      },\n      {\n        cmd = \"wget\",\n        args = { \"-O\", \"{file}\", \"{src}\" },\n      },\n    },\n    file = function(convert, ctx)\n      local src = M.norm(ctx.src)\n      return M.is_uri(src) and convert:tmpfile(\"data\") or src\n    end,\n    on_error = function(step)\n      if uv.fs_stat(step.file) then\n        vim.fs.rm(step.file)\n      end\n    end,\n  },\n  typ = {\n    ft = \"pdf\",\n    cmd = {\n      {\n        cmd = \"typst\",\n        args = { \"compile\", \"--format\", \"pdf\", \"--pages\", 1, \"{src}\", \"{file}\" },\n      },\n    },\n  },\n  tex = {\n    ft = \"pdf\",\n    file = function(convert, ctx)\n      ctx.pdf = Snacks.image.config.cache .. \"/\" .. vim.fs.basename(ctx.src):gsub(\"%.tex$\", \".pdf\")\n      return convert:tmpfile(\"pdf\")\n    end,\n    cmd = {\n      {\n        cwd = \"{dirname}\",\n        cmd = \"tectonic\",\n        args = { \"-Z\", \"continue-on-errors\", \"--outdir\", \"{cache}\", \"{src}\" },\n      },\n      {\n        cmd = \"pdflatex\",\n        cwd = \"{dirname}\",\n        args = { \"-output-directory={cache}\", \"-interaction=nonstopmode\", \"{src}\" },\n      },\n    },\n    on_done = function(step)\n      local pdf = assert(step.meta.pdf, \"No pdf file\") --[[@as string]]\n      if uv.fs_stat(pdf) then\n        uv.fs_rename(pdf, step.file)\n      end\n    end,\n    on_error = function(step)\n      local pdf = assert(step.meta.pdf, \"No pdf file\") --[[@as string]]\n      if step.meta.pdf and vim.fn.getfsize(pdf) > 0 then\n        return true\n      end\n    end,\n  },\n  mmd = {\n    cmd = {\n      cmd = \"mmdc\",\n      args = Snacks.image.config.convert.mermaid,\n    },\n    file = function(convert, ctx)\n      return convert:tmpfile(vim.o.background .. \".png\")\n    end,\n  },\n  identify = {\n    pipe = false,\n    file = function(convert, ctx)\n      return convert:tmpfile(convert:ft() .. \".info\")\n    end,\n    cmd = {\n      {\n        cmd = \"magick\",\n        args = { \"identify\", \"-format\", \"%m %[fx:w]x%[fx:h] %xx%y\", \"{src}[{page}]\" },\n      },\n      {\n        cmd = \"identify\",\n        args = { \"-format\", \"%m %[fx:w]x%[fx:h] %xx%y\", \"{src}[{page}]\" },\n      },\n    },\n    on_done = function(step)\n      local file = step.file\n      if step.proc then\n        local fd = assert(io.open(file, \"w\"), \"Failed to open file: \" .. file)\n        fd:write(step.proc:out())\n        fd:close()\n      end\n      local fd = assert(io.open(file, \"r\"), \"Failed to open file: \" .. file)\n      local info = vim.trim(fd:read(\"*a\"))\n      fd:close()\n      local format, w, h, x, y = info:match(\"^(%w+)%s+(%d+)x(%d+)%s+(%d+%.?%d*)x(%d+%.?%d*)$\")\n      if not format then\n        return\n      end\n      step.meta.info = {\n        format = format:lower(),\n        size = { width = tonumber(w) or 0, height = tonumber(h) or 0 },\n        dpi = { width = tonumber(x) or 0, height = tonumber(y) or 0 },\n      }\n    end,\n  },\n  convert = {\n    ft = \"png\",\n    cmd = function(step)\n      local formats = vim.deepcopy(Snacks.image.config.convert.magick or {})\n      local args = formats.default or { \"{src}[{page}]\" }\n      local info = step.meta.info\n      local format = info and info.format or vim.fn.fnamemodify(step.meta.src, \":e\")\n\n      local vector = vim.tbl_contains({ \"pdf\", \"svg\", \"eps\", \"ai\", \"mvg\" }, format)\n      if vector then\n        args = formats.vector or args\n      end\n\n      local fts = { vim.fs.basename(step.file):match(\"%.([^%.]+)%.png\") } ---@type string[]\n      fts[#fts + 1] = format\n\n      for _, ft in ipairs(fts) do\n        local fmt = formats[ft]\n        if fmt then\n          args = type(fmt) == \"function\" and fmt() or fmt\n          break\n        end\n      end\n      args = type(args) == \"function\" and args() or args\n      ---@cast args (string|number)[]\n\n      vim.list_extend(args, { \"-write\", \"{file}\", \"-identify\", \"-format\", \"%m %[fx:w]x%[fx:h] %xx%y\", \"{file}.info\" })\n      return {\n        { cmd = \"magick\", args = args },\n        not Snacks.util.is_win and { cmd = \"convert\", args = args } or nil,\n      }\n    end,\n  },\n}\n\nlocal have = {} ---@type table<string, boolean>\nlocal proc_queue = {} ---@type snacks.spawn.Proc[]\nlocal proc_running = 0 ---@type number\nlocal MAX_PROCS = 3\n\n---@param proc? snacks.spawn.Proc\nlocal function schedule(proc)\n  if proc then\n    table.insert(proc_queue, proc)\n  else\n    proc_running = proc_running - 1\n  end\n  -- Snacks.notify(\"proc_running: \" .. proc_running .. \"\\nproc_queue: \" .. #proc_queue, { id = \"proc_running\" })\n  if proc_running < MAX_PROCS and #proc_queue > 0 then\n    proc_running = proc_running + 1\n    proc = table.remove(proc_queue, 1)\n    proc:run()\n  end\nend\n\n---@param step snacks.image.step\nlocal function get_cmd(step)\n  local cmd = step.cmd.cmd\n  cmd = type(cmd) == \"function\" and cmd(step) or cmd\n  local cmds = cmd.cmd and { cmd } or cmd\n  ---@cast cmds snacks.image.Proc[]\n  for _, c in ipairs(cmds) do\n    if have[c.cmd] == nil then\n      have[c.cmd] = vim.fn.executable(c.cmd) == 1\n    end\n    if have[c.cmd] then\n      return c\n    end\n  end\nend\n\n---@class snacks.image.Convert\n---@field opts snacks.image.convert.Opts\n---@field src string\n---@field page number\n---@field file string\n---@field prefix string\n---@field meta snacks.image.meta\n---@field steps snacks.image.step[]\n---@field _done? boolean\n---@field _err? string\n---@field _step number\n---@field tpl_data table<string, string>\nlocal Convert = {}\nConvert.__index = Convert\n\n---@param opts snacks.image.convert.Opts\nfunction Convert.new(opts)\n  vim.fn.mkdir(Snacks.image.config.cache, \"p\")\n  local self = setmetatable({}, Convert)\n  opts.src, self.page = M.get_page(opts.src)\n  opts.src = M.norm(opts.src)\n  self.opts = opts\n  self.src = opts.src\n  self._step = 0\n  local base = vim.fn.fnamemodify(opts.src, \":t:r\")\n  if M.is_uri(self.opts.src) then\n    base = self.opts.src:gsub(\"%?.*\", \"\"):match(\"^%w%w+://(.*)$\") or base\n  end\n  self.prefix = vim.fn.sha256(self.opts.src .. self.page):sub(1, 8) .. \"-\" .. base:gsub(\"[^%w%.]+\", \"-\")\n  self.meta = { src = opts.src }\n  self.steps = {}\n  self.tpl_data = {\n    cache = Snacks.image.config.cache,\n    bg = vim.o.background,\n    scale = tostring(Snacks.image.terminal.size().scale or 1),\n  }\n  self:resolve()\n  return self\nend\n\n---@return snacks.image.step?\nfunction Convert:current()\n  return self.steps[self._step]\nend\n\nfunction Convert:ready()\n  return self:done() and not self:error()\nend\n\nfunction Convert:done()\n  return self._done or false\nend\n\nfunction Convert:error()\n  return self._err\nend\n\n---@param ft string\nfunction Convert:tmpfile(ft)\n  return Snacks.image.config.cache .. \"/\" .. self.prefix .. \".\" .. ft\nend\n\n---@param target string\nfunction Convert:_resolve(target)\n  local cmd = assert(commands[target], \"No command for target: \" .. target)\n  assert(cmd.file or cmd.ft, \"No file or ft for target: \" .. target)\n  for _, dep in ipairs(cmd.depends or {}) do\n    self:_resolve(dep)\n  end\n  local file = cmd.file and cmd.file(self, self.meta) or self:tmpfile(cmd.ft)\n  ---@type snacks.image.step\n  local step = {\n    name = target,\n    file = file,\n    ft = self:ft(file),\n    meta = self.meta,\n    done = uv.fs_stat(file) ~= nil,\n    cmd = cmd,\n  }\n  if cmd.pipe ~= false then\n    self.meta = setmetatable({ src = file }, { __index = self.meta })\n  end\n  table.insert(self.steps, step)\nend\n\n---@param src? string\n---@return string\nfunction Convert:ft(src)\n  return vim.fn.fnamemodify(src or self.meta.src, \":e\"):lower()\nend\n\nfunction Convert:resolve()\n  if M.is_uri(self.src) then\n    self:_resolve(\"url\")\n    self:_resolve(\"identify\")\n  end\n  while self:ft() ~= \"png\" do\n    local ft = self:ft()\n    local target = commands[ft] and ft or \"convert\"\n    if self:_resolve(target) then\n      break\n    end\n  end\n  self:_resolve(\"identify\")\n  self.file = self.meta.src\nend\n\n---@param err? string\nfunction Convert:on_step(err)\n  local step = assert(self:current(), \"No current step\")\n  step.done = true\n  step.err = err\n  if self.aborted then\n    return self:on_done()\n  end\n  if step and err and step.cmd.on_error and step.cmd.on_error(step) then\n    -- keep going\n  elseif err then\n    self._err = err\n    return self:on_done()\n  end\n  if step and step.cmd.on_done then\n    step.cmd.on_done(step)\n  end\n\n  if self._step < #self.steps then\n    self:step()\n  else\n    self:on_done()\n  end\nend\n\n-- Called when all steps are done or when an error occurs\nfunction Convert:on_done()\n  local step = self:current()\n  self._done = true\n  if self._err and Snacks.image.config.convert.notify then\n    local title = step and (\"Conversion failed at step `%s`\"):format(step.name) or \"Conversion failed\"\n    if step and step.proc then\n      step.proc:debug({ title = title })\n    else\n      Snacks.notify.error(\"# \" .. title .. \"\\n\" .. self._err, { title = \"Snacks Image\" })\n    end\n  end\n  if self.opts.on_done then\n    self.opts.on_done(self)\n  end\nend\n\nfunction Convert:abort()\n  if self.aborted then\n    return\n  end\n  if self:done() then\n    return\n  end\n  self.aborted = true\n  self._err = \"Aborted\"\n  for _, step in ipairs(self.steps) do\n    if step.proc then\n      step.proc:kill()\n    end\n  end\nend\n\nfunction Convert:step()\n  self._step = self._step + 1\n  assert(self._step <= #self.steps, \"No more steps\")\n\n  local step = self.steps[self._step]\n  step.done = step.done or (uv.fs_stat(step.file) ~= nil)\n  if step.done then\n    return self:on_step()\n  end\n\n  local cmd = get_cmd(step)\n  if not cmd then\n    return self:on_step(\"No command available\")\n  end\n\n  local args = type(cmd.args) == \"function\" and cmd.args() or cmd.args\n  ---@cast args (number|string)[]\n  args = vim.deepcopy(args)\n\n  local data = vim.tbl_extend(\"keep\", {\n    file = step.file,\n    basename = vim.fs.basename(step.file),\n    name = vim.fn.fnamemodify(step.file, \":t:r\"),\n    dirname = vim.fs.dirname(step.meta.src),\n    src = step.meta.src,\n    page = self.page,\n  }, self.tpl_data)\n\n  for a, arg in ipairs(args) do\n    if type(arg) == \"string\" then\n      args[a] = Snacks.picker.util.tpl(arg, data)\n    end\n  end\n\n  step.proc = Spawn.new({\n    run = false,\n    debug = Snacks.image.config.debug.convert,\n    cwd = cmd.cwd and Snacks.picker.util.tpl(cmd.cwd, data) or nil,\n    cmd = cmd.cmd,\n    args = args,\n    on_exit = function(proc, err)\n      schedule()\n      local out = vim.trim(proc:out() .. \"\\n\" .. proc:err())\n      vim.schedule(function()\n        self:on_step(err and out or nil)\n      end)\n    end,\n  })\n  schedule(step.proc)\nend\n\nfunction Convert:run()\n  if #self.steps == 0 then\n    return self:on_done()\n  end\n\n  if not M.is_uri(self.src) and vim.fn.filereadable(self.src) == 0 then\n    local f = M.is_uri(self.src) and self.src or vim.fn.fnamemodify(self.src, \":p:~\")\n    self._err = (\"File not found\\n- `%s`\"):format(f)\n    return self:on_done()\n  end\n\n  self:step()\nend\n\n---@param src string\nfunction M.is_url(src)\n  return src:find(\"^https?://\") == 1\nend\n\n---@param src string\nfunction M.is_uri(src)\n  return src:find(\"^%w%w+://\") == 1\nend\n\n---@param src string\nfunction M.norm(src)\n  if src:find(\"^file://\") then\n    src = vim.uri_to_fname(src)\n  end\n  if not M.is_uri(src) then\n    src = svim.fs.normalize(vim.fn.fnamemodify(src, \":p\"))\n  end\n  return src\nend\n\n---@param src string\n---@return string, number\nfunction M.get_page(src)\n  local parts = vim.split(src, \"#page=\", { plain = true })\n  local page_number = tonumber(parts[2]) or 1\n  return parts[1], page_number - 1\nend\n\n---@param opts snacks.image.convert.Opts\nfunction M.convert(opts)\n  return Convert.new(opts)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/doc.lua",
    "content": "---@class snacks.image.doc\nlocal M = {}\n\n---@alias TSMatch {node:TSNode, meta:vim.treesitter.query.TSMetadata}\n---@alias snacks.image.transform fun(match: snacks.image.match, ctx: snacks.image.ctx)\n---@alias snacks.image.find fun(matches: snacks.image.match[])\n\n---@class snacks.image.Hover\n---@field img snacks.image.Placement\n---@field win snacks.win\n---@field buf number\n\n---@class snacks.image.ctx\n---@field buf number\n---@field lang string\n---@field meta vim.treesitter.query.TSMetadata\n---@field pos? TSMatch\n---@field src? TSMatch\n---@field content? TSMatch\n\n---@class snacks.image.match\n---@field id string\n---@field pos snacks.image.Pos\n---@field src? string\n---@field content? string\n---@field content_id? string\n---@field ext? string\n---@field range? Range4\n---@field lang string\n---@field type snacks.image.Type\n\nlocal META_EXT = \"image.ext\"\nlocal META_SRC = \"image.src\"\nlocal META_TYPE = \"image.type\"\nlocal META_IGNORE = \"image.ignore\"\nlocal META_LANG = \"image.lang\"\n\n---@type table<string, snacks.image.transform>\nM.transforms = {\n  norg = function(img, ctx)\n    local row, col = ctx.src.node:start()\n    local line = vim.api.nvim_buf_get_lines(ctx.buf, row, row + 1, false)[1]\n    img.src = line:sub(col + 1)\n  end,\n  typst = function(img, ctx)\n    if not img.content then\n      return\n    end\n    img.content = Snacks.picker.util.tpl(Snacks.image.config.math.typst.tpl, {\n      color = Snacks.util.color(\"SnacksImageMath\") or \"#000000\",\n      header = M.get_header(ctx.buf),\n      content = img.content,\n    }, { indent = true, prefix = \"$\" })\n  end,\n  data_img = function(img, ctx)\n    if not vim.base64 then\n      return\n    end\n    if not img.src then\n      return\n    end\n    local ft, data = img.src:match(\"^data:(.-);base64,(.+)$\")\n    if not (ft and data) then\n      return\n    end\n    img.content = vim.base64.decode(data)\n    img.content_id = data:sub(1, 20)\n    img.src = nil\n    img.ext = ft:match(\"^image/(%w+)$\") or \"png\"\n  end,\n  latex = function(img, ctx)\n    if not (img.content and img.ext == \"math.tex\") then\n      return\n    end\n    local fg = Snacks.util.color(\"SnacksImageMath\") or \"#000000\"\n    local content = vim.trim(img.content or \"\")\n    content = content:gsub(\"^%$+`?\", \"\"):gsub(\"`?%$+$\", \"\")\n    content = content:gsub(\"^\\\\[%[%(]\", \"\"):gsub(\"\\\\[%]%)]$\", \"\")\n    if not content:find(\"^\\\\begin\") then\n      content = (\"\\\\[%s\\\\]\"):format(content)\n    end\n    local packages = { \"xcolor\" }\n    vim.list_extend(packages, Snacks.image.config.math.latex.packages)\n    vim.list_extend(packages, M.get_packages(ctx.buf))\n    table.sort(packages)\n    local seen = {} ---@type table<string, boolean>\n    packages = vim.tbl_filter(function(p)\n      if seen[p] then\n        return false\n      end\n      seen[p] = true\n      return true\n    end, packages)\n    img.content = Snacks.picker.util.tpl(Snacks.image.config.math.latex.tpl, {\n      font_size = Snacks.image.config.math.latex.font_size or \"large\",\n      packages = table.concat(packages, \", \"),\n      header = M.get_header(ctx.buf),\n      color = fg:upper():sub(2),\n      content = content,\n    }, { indent = true, prefix = \"$\" })\n  end,\n}\n\nlocal hover ---@type snacks.image.Hover?\nlocal uv = vim.uv or vim.loop\nlocal dir_cache = {} ---@type table<string, boolean>\nlocal buf_cache = {} ---@type table<number,{tick: number, [string]:any}>\n\n---@param buf number\n---@param key string\n---@param fn fun():any\nfunction M._cache(buf, key, fn)\n  if buf_cache[buf] and buf_cache[buf].tick ~= vim.api.nvim_buf_get_changedtick(buf) then\n    buf_cache[buf] = nil\n  end\n  buf_cache[buf] = buf_cache[buf] or { tick = vim.api.nvim_buf_get_changedtick(buf) }\n  if buf_cache[buf][key] == nil then\n    buf_cache[buf][key] = fn()\n  end\n  return buf_cache[buf][key]\nend\n\n---@param buf number\nfunction M.get_packages(buf)\n  if vim.bo[buf].filetype ~= \"tex\" then\n    return {}\n  end\n  return M._cache(buf, \"packages\", function()\n    local ret = {} ---@type string[]\n    for _, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do\n      line = line:match(\"(.-)%%\") or line\n      if line:find(\"\\\\usepackage\", 1, true) then\n        for _, p in ipairs(vim.split(line:match(\"\\\\usepackage.-{(.-)}\") or \"\", \",%s*\")) do\n          if not vim.tbl_contains(ret, p) then\n            ret[#ret + 1] = p\n          end\n        end\n      elseif line:find(\"\\\\begin{document}\", 1, true) then\n        break\n      end\n    end\n    return ret\n  end)\nend\n\n---@param buf number\nfunction M.get_header(buf)\n  return M._cache(buf, \"header\", function()\n    local header = {} ---@type string[]\n    local in_header = false\n    for _, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do\n      if line:find(\"snacks:%s*header%s*start\") then\n        in_header = true\n      elseif line:find(\"snacks:%s*header%s*end\") then\n        in_header = false\n      elseif in_header then\n        header[#header + 1] = line\n      end\n    end\n    return table.concat(header, \"\\n\")\n  end)\nend\n\n---@param str string\nfunction M.url_decode(str)\n  return str:gsub(\"+\", \" \"):gsub(\"%%(%x%x)\", function(hex)\n    return string.char(tonumber(hex, 16))\n  end)\nend\n\n---@param dir string\nfunction M.is_dir(dir)\n  if dir_cache[dir] == nil then\n    dir_cache[dir] = vim.fn.isdirectory(dir) == 1\n  end\n  return dir_cache[dir]\nend\n\n---@param buf number\n---@param src string\nfunction M.resolve(buf, src)\n  src = M.url_decode(src)\n  local file = svim.fs.normalize(vim.api.nvim_buf_get_name(buf))\n  local s = Snacks.image.config.resolve and Snacks.image.config.resolve(file, src) or nil\n  if s then\n    return s\n  end\n  if not src:find(\"^%w%w+://\") then\n    local cwd = uv.cwd() or \".\"\n    local checks = { [src] = true }\n    for _, root in ipairs({ cwd, vim.fs.dirname(file) }) do\n      checks[root .. \"/\" .. src] = true\n      for _, dir in ipairs(Snacks.image.config.img_dirs) do\n        dir = root .. \"/\" .. dir\n        if M.is_dir(dir) then\n          checks[dir .. \"/\" .. src] = true\n        end\n      end\n    end\n    for f in pairs(checks) do\n      if vim.fn.filereadable(f) == 1 then\n        src = uv.fs_realpath(f) or f\n        break\n      end\n    end\n    src = svim.fs.normalize(src)\n  end\n  return src\nend\n\n---@param buf number\n---@param cb snacks.image.find\nfunction M.find_visible(buf, cb)\n  local ret = {} ---@type table<string,snacks.image.match>\n  local wins = vim.fn.win_findbuf(buf)\n  local count = #wins\n  for _, win in ipairs(wins) do\n    local info = vim.fn.getwininfo(win)[1]\n    M.find(buf, function(mathes)\n      for _, i in ipairs(mathes) do\n        ret[i.id] = i\n      end\n      count = count - 1\n      if count == 0 and cb then\n        cb(vim.tbl_values(ret))\n      end\n    end, { from = math.max(info.topline - 1, 1), to = info.botline })\n  end\nend\n\n---@param buf number\n---@param cb snacks.image.find\n---@param opts? {from?: number, to?: number}\nfunction M.find(buf, cb, opts)\n  local ok, parser = pcall(vim.treesitter.get_parser, buf)\n  if not ok or not parser then\n    return cb({})\n  end\n  opts = opts or {}\n  local from, to = opts.from, opts.to\n  Snacks.util.parse(parser, from and to and { from, to } or true, function()\n    local ret = {} ---@type snacks.image.match[]\n    parser:for_each_tree(function(tstree, tree)\n      if not tstree then\n        return\n      end\n      local query = vim.treesitter.query.get(tree:lang(), \"images\")\n      if not query then\n        return\n      end\n      for _, match, meta in query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to) do\n        if not meta[META_IGNORE] then\n          ---@type snacks.image.ctx\n          local ctx = {\n            buf = buf,\n            lang = tostring(meta[META_LANG] or meta[\"injection.language\"] or tree:lang()),\n            meta = meta,\n          }\n          for id, nodes in pairs(match) do\n            nodes = type(nodes) == \"userdata\" and { nodes } or nodes\n            local name = query.captures[id]\n            local field = name == \"image\" and \"pos\" or name:match(\"^image%.(.*)$\")\n            if field then\n              ---@diagnostic disable-next-line: assign-type-mismatch\n              ctx[field] = { node = nodes[1], meta = meta[id] or {} }\n            end\n          end\n          ret[#ret + 1] = M._img(ctx)\n        end\n      end\n    end)\n    cb(ret)\n  end)\nend\n\n---@param ctx snacks.image.ctx\nfunction M._img(ctx)\n  ctx.pos = ctx.pos or ctx.src or ctx.content\n  assert(ctx.pos, \"no image node\")\n\n  local range6 = vim.treesitter.get_range(ctx.pos.node, ctx.buf, ctx.pos.meta)\n  local range = { range6[1], range6[2], range6[4], range6[5] } ---@type Range4\n  if range[3] > 0 and range[4] == 0 then\n    range[3] = range[3] - 1\n    local line = vim.api.nvim_buf_get_lines(ctx.buf, range[3], range[3] + 1, false)[1]\n    range[4] = #line\n  end\n  ---@type snacks.image.match\n  local img = {\n    ext = ctx.meta[META_EXT],\n    src = ctx.meta[META_SRC],\n    lang = ctx.lang,\n    id = ctx.pos.node:id(),\n    range = { range[1] + 1, range[2], range[3] + 1, range[4] },\n    pos = { range[1] + 1, range[2] },\n    type = \"image\",\n  }\n  if ctx.meta[META_TYPE] then\n    img.type = ctx.meta[META_TYPE]\n  elseif img.ext then\n    img.type = img.ext:match(\"^(%w+)%.\") or img.type\n  end\n  if not Snacks.image.config.math.enabled and img.type == \"math\" then\n    return\n  end\n  if ctx.src then\n    img.src = vim.treesitter.get_node_text(ctx.src.node, ctx.buf, { metadata = ctx.src.meta })\n  end\n  if ctx.content then\n    img.content = vim.treesitter.get_node_text(ctx.content.node, ctx.buf, { metadata = ctx.content.meta })\n  end\n  assert(img.src or img.content, \"no image src or content\")\n\n  local transform = M.transforms[ctx.lang]\n  if img.src and img.src:find(\"^data:%w+/%w+;base64,\") then\n    transform = M.transforms[\"data_img\"]\n  end\n  if transform then\n    transform(img, ctx)\n  end\n  if img.src then\n    img.src = M.resolve(ctx.buf, img.src)\n  end\n  if img.content and not img.src then\n    local root = Snacks.image.config.cache\n    vim.fn.mkdir(root, \"p\")\n    img.src = root\n      .. \"/\"\n      .. (img.content_id or vim.fn.sha256(img.content):sub(1, 8))\n      .. \"-content.\"\n      .. (img.ext or \"png\")\n    if vim.fn.filereadable(img.src) == 0 then\n      local fd = assert(io.open(img.src, \"w\"), \"failed to open \" .. img.src)\n      fd:write(img.content)\n      fd:close()\n    end\n  end\n  return img\nend\n\nfunction M.hover_close()\n  if hover then\n    hover.win:close()\n    hover.img:close()\n    hover = nil\n  end\nend\n\n--- Get the image at the cursor (if any)\n---@param cb fun(image_src?:string, image_pos?: snacks.image.Pos)\nfunction M.at_cursor(cb)\n  local cursor = vim.api.nvim_win_get_cursor(0)\n  M.find(vim.api.nvim_get_current_buf(), function(imgs)\n    for _, img in ipairs(imgs) do\n      local range = img.range\n      if range then\n        if\n          (range[1] == range[3] and cursor[2] >= range[2] and cursor[2] <= range[4])\n          or (range[1] ~= range[3] and cursor[1] >= range[1] and cursor[1] <= range[3])\n        then\n          return cb(img.src, img.pos)\n        end\n      end\n    end\n    cb()\n  end, { from = cursor[1], to = cursor[1] + 1 })\nend\n\nfunction M.hover()\n  local current_win = vim.api.nvim_get_current_win()\n  local current_buf = vim.api.nvim_get_current_buf()\n\n  if hover and hover.win.win == current_win and hover.win:valid() then\n    return\n  end\n\n  if hover and (hover.buf ~= current_buf or vim.fn.mode() ~= \"n\") then\n    return M.hover_close()\n  end\n\n  if hover and not hover.win:valid() then\n    M.hover_close()\n  end\n\n  M.at_cursor(function(src)\n    if not src then\n      return M.hover_close()\n    end\n\n    if hover and hover.img.img.src ~= src then\n      M.hover_close()\n    elseif hover then\n      hover.img:update()\n      return\n    end\n\n    local win = Snacks.win(Snacks.win.resolve(Snacks.image.config.doc, \"snacks_image\", {\n      show = false,\n      enter = false,\n      wo = { winblend = Snacks.image.terminal.env().placeholders and 0 or nil },\n    }))\n    win:open_buf()\n    local updated = false\n    local o = Snacks.config.merge({}, Snacks.image.config.doc, {\n      on_update_pre = function()\n        if hover and not updated then\n          updated = true\n          local loc = hover.img:state().loc\n          win.opts.width = loc.width\n          win.opts.height = loc.height\n          win:show()\n        end\n      end,\n      inline = false,\n    })\n    hover = {\n      win = win,\n      buf = current_buf,\n      img = Snacks.image.placement.new(win.buf, src, o),\n    }\n    vim.api.nvim_create_autocmd({ \"BufWritePost\", \"CursorMoved\", \"ModeChanged\", \"BufLeave\" }, {\n      group = vim.api.nvim_create_augroup(\"snacks.image.hover\", { clear = true }),\n      callback = function()\n        if not hover then\n          return true\n        end\n        M.hover()\n        if not hover then\n          return true\n        end\n      end,\n    })\n  end)\nend\n\n---@param buf number\nfunction M._attach(buf)\n  if not vim.api.nvim_buf_is_valid(buf) then\n    return\n  end\n  if vim.b[buf].snacks_image_attached then\n    return\n  end\n  vim.b[buf].snacks_image_attached = true\n  local inline = Snacks.image.config.doc.inline and Snacks.image.terminal.env().placeholders\n  local float = Snacks.image.config.doc.float and not inline\n\n  if not inline and not float then\n    return\n  end\n\n  if inline then\n    Snacks.image.inline.new(buf)\n  else\n    local group = vim.api.nvim_create_augroup(\"snacks.image.doc.\" .. buf, { clear = true })\n    vim.api.nvim_create_autocmd({ \"CursorMoved\" }, {\n      group = group,\n      buffer = buf,\n      callback = vim.schedule_wrap(M.hover),\n    })\n    vim.schedule(M.hover)\n  end\nend\n\n---@param buf number\nfunction M.attach(buf)\n  if Snacks.image.config.enabled == false then\n    return\n  end\n  local Terminal = require(\"snacks.image.terminal\")\n  Terminal.detect(function()\n    M._attach(buf)\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/image.lua",
    "content": "---@class snacks.Image\n---@field src string\n---@field file string\n---@field id number image id. unique per nvim instance and file\n---@field sent? boolean image data is sent\n---@field placements table<number, snacks.image.Placement> image placements\n---@field info? snacks.image.Info\n---@field _convert? snacks.image.Convert\n---@field fsize? number\nlocal M = {}\nM.__index = M\n\nlocal NVIM_ID_BITS = 10\nlocal CHUNK_SIZE = 4096\nlocal MAX_FSIZE = 200 * 1024 * 1024 -- 200MB\nlocal _id = 30\nlocal _pid = 10\nlocal nvim_id = 0\nlocal uv = vim.uv or vim.loop\nlocal images = {} ---@type table<string, snacks.Image>\nlocal terminal = Snacks.image.terminal\nlocal lru = {} ---@type {img:snacks.Image, used:number}[]\nlocal lru_fsize = 0\n\n---@param img snacks.Image\nlocal function use(img)\n  if img.fsize == 0 then\n    return\n  end\n  local now = os.time()\n  for _, v in ipairs(lru) do\n    if v.img == img then\n      v.used = now\n      return\n    end\n  end\n  table.sort(lru, function(a, b)\n    return a.used > b.used\n  end)\n  while lru_fsize >= MAX_FSIZE and #lru > 0 do\n    local i = table.remove(lru).img\n    i.sent = false\n    lru_fsize = lru_fsize - (i.fsize or 0)\n  end\n  lru_fsize = lru_fsize + (img.fsize or 0)\n  table.insert(lru, { img = img, used = now })\nend\n\n---@param src string\nfunction M.new(src)\n  local self = setmetatable({}, M)\n  self.src = src\n  self.file = self:convert()\n  if images[self.file] then\n    return images[self.file]\n  end\n  images[self.file] = self\n  _id = _id + 1\n  local bit = require(\"bit\")\n  -- generate a unique id for this nvim instance (10 bits)\n  if nvim_id == 0 then\n    local pid = vim.fn.getpid()\n    nvim_id = bit.band(bit.bxor(pid, bit.rshift(pid, 5), bit.rshift(pid, NVIM_ID_BITS)), 0x3FF)\n  end\n  -- interleave the nvim id and the image id\n  self.id = bit.bor(bit.lshift(nvim_id, 24 - NVIM_ID_BITS), _id)\n  self.placements = {}\n\n  self:run()\n  if self:ready() then\n    self:on_ready()\n  end\n\n  return self\nend\n\nfunction M:on_ready()\n  if not self.sent then\n    self.fsize = vim.fn.getfsize(self.file)\n    self.info = self._convert and self._convert.meta.info or nil\n    if self.info and self.info.size then\n      -- ghostty uses the decoded rgba size to calculate the fsize\n      self.fsize = (self.info.size.width * 4 + 1) * self.info.size.height\n    end\n    self:send()\n  end\nend\n\nfunction M:on_send()\n  use(self)\n  for _, placement in pairs(self.placements) do\n    placement:update()\n  end\nend\n\nfunction M:failed()\n  if self._convert and not self._convert:done() then\n    return false\n  end\n  if self._convert and self._convert:error() then\n    return true\n  end\n  return self.file and vim.fn.filereadable(self.file) == 0\nend\n\nfunction M:ready()\n  if self._convert and not self._convert:done() then\n    return false\n  end\n  return self.file and vim.fn.filereadable(self.file) == 1\nend\n\nfunction M:run()\n  if not self._convert then\n    return\n  end\n  self._convert:run()\nend\n\nfunction M:convert()\n  self._convert = Snacks.image.convert.convert({\n    src = self.src,\n    on_done = function(convert)\n      if convert:error() then\n        vim.schedule(function()\n          for _, p in pairs(self.placements) do\n            p:error()\n          end\n        end)\n      else\n        vim.schedule(function()\n          self:on_ready()\n        end)\n      end\n    end,\n  })\n  return self._convert.file\nend\n\n-- create the image\nfunction M:send()\n  assert(not self.sent, \"Image already sent\")\n  self.sent = true\n  -- local image\n  if not terminal.env().remote then\n    terminal.request({\n      t = \"f\",\n      i = self.id,\n      f = 100,\n      data = Snacks.util.base64(self.file),\n    })\n  else\n    -- remote image\n    local fd = assert(io.open(self.file, \"rb\"), \"Failed to open file: \" .. self.file)\n    local data = fd:read(\"*a\")\n    fd:close()\n    data = Snacks.util.base64(data) -- encode the data\n    local offset = 1\n    while offset <= #data do\n      local chunk = data:sub(offset, offset + CHUNK_SIZE - 1)\n      local first = offset == 1\n      offset = offset + CHUNK_SIZE\n      local last = offset > #data\n      if first then\n        terminal.request({\n          t = \"d\",\n          i = self.id,\n          f = 100,\n          m = last and 0 or 1,\n          data = chunk,\n        })\n      else\n        terminal.request({\n          m = last and 0 or 1,\n          data = chunk,\n        })\n      end\n      uv.sleep(1)\n    end\n  end\n  self:on_send()\nend\n\n---@param placement snacks.image.Placement\nfunction M:place(placement)\n  if not placement.id then\n    _pid = _pid + 1\n    placement.id = _pid\n  end\n  self.placements[placement.id] = placement\n  if self.sent then\n    use(self)\n  elseif self:ready() then\n    self:send()\n  end\nend\n\n---@param pid? number\nfunction M:del(pid)\n  for id, p in ipairs(pid and { pid } or vim.tbl_keys(self.placements)) do\n    if self.placements[p] then\n      terminal.request({ a = \"d\", d = \"i\", i = self.id, p = id })\n      self.placements[p] = nil\n    end\n  end\n\n  if not next(self.placements) then\n    terminal.request({ a = \"d\", d = \"i\", i = self.id })\n  end\nend\n\nfunction M.clear()\n  images = {}\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/init.lua",
    "content": "---@class snacks.image\n---@field terminal snacks.image.terminal\n---@field image snacks.Image\n---@field placement snacks.image.Placement\n---@field util snacks.image.util\n---@field buf snacks.image.buf\n---@field doc snacks.image.doc\n---@field convert snacks.image.convert\n---@field inline snacks.image.inline\nlocal M = setmetatable({}, {\n  ---@param M snacks.image\n  __index = function(M, k)\n    if vim.tbl_contains({ \"terminal\", \"image\", \"placement\", \"util\", \"doc\", \"buf\", \"convert\", \"inline\" }, k) then\n      M[k] = require(\"snacks.image.\" .. k)\n    end\n    return rawget(M, k)\n  end,\n})\n\nM.meta = {\n  desc = \"Image viewer using Kitty Graphics Protocol, supported by `kitty`, `wezterm` and `ghostty`\",\n  needs_setup = true,\n}\n\n---@alias snacks.image.Size {width: number, height: number}\n---@alias snacks.image.Pos {[1]: number, [2]: number}\n---@alias snacks.image.Loc snacks.image.Pos|snacks.image.Size|{zindex?: number}\n---@alias snacks.image.Type \"image\"|\"math\"|\"chart\"\n\n---@class snacks.image.Env\n---@field name string\n---@field env? table<string, string|true>\n---@field terminal? string\n---@field supported? boolean default: false\n---@field placeholders? boolean default: false\n---@field setup? fun(): boolean?\n---@field transform? fun(data: string): string\n---@field detected? boolean\n---@field remote? boolean this is a remote client, so full transfer of the image data is required\n\n---@class snacks.image.Config\n---@field enabled? boolean enable image viewer\n---@field wo? vim.wo|{} options for windows showing the image\n---@field bo? vim.bo|{} options for the image buffer\n---@field formats? string[]\n--- Resolves a reference to an image with src in a file (currently markdown only).\n--- Return the absolute path or url to the image.\n--- When `nil`, the path is resolved relative to the file.\n---@field resolve? fun(file: string, src: string): string?\n---@field convert? snacks.image.convert.Config\nlocal defaults = {\n  formats = {\n    \"png\",\n    \"jpg\",\n    \"jpeg\",\n    \"gif\",\n    \"bmp\",\n    \"webp\",\n    \"tiff\",\n    \"heic\",\n    \"avif\",\n    \"mp4\",\n    \"mov\",\n    \"avi\",\n    \"mkv\",\n    \"webm\",\n    \"pdf\",\n    \"icns\",\n  },\n  force = false, -- try displaying the image, even if the terminal does not support it\n  doc = {\n    -- enable image viewer for documents\n    -- a treesitter parser must be available for the enabled languages.\n    enabled = true,\n    -- render the image inline in the buffer\n    -- if your env doesn't support unicode placeholders, this will be disabled\n    -- takes precedence over `opts.float` on supported terminals\n    inline = true,\n    -- render the image in a floating window\n    -- only used if `opts.inline` is disabled\n    float = true,\n    max_width = 80,\n    max_height = 40,\n    -- Set to `true`, to conceal the image text when rendering inline.\n    -- (experimental)\n    ---@param lang string tree-sitter language\n    ---@param type snacks.image.Type image type\n    conceal = function(lang, type)\n      -- only conceal math expressions\n      return type == \"math\"\n    end,\n  },\n  img_dirs = { \"img\", \"images\", \"assets\", \"static\", \"public\", \"media\", \"attachments\" },\n  -- window options applied to windows displaying image buffers\n  -- an image buffer is a buffer with `filetype=image`\n  wo = {\n    wrap = false,\n    number = false,\n    relativenumber = false,\n    cursorcolumn = false,\n    signcolumn = \"no\",\n    foldcolumn = \"0\",\n    list = false,\n    spell = false,\n    statuscolumn = \"\",\n  },\n  cache = vim.fn.stdpath(\"cache\") .. \"/snacks/image\",\n  debug = {\n    request = false,\n    convert = false,\n    placement = false,\n  },\n  env = {},\n  -- icons used to show where an inline image is located that is\n  -- rendered below the text.\n  icons = {\n    math = \"󰪚 \",\n    chart = \"󰄧 \",\n    image = \" \",\n  },\n  ---@class snacks.image.convert.Config\n  convert = {\n    notify = false, -- show a notification on error\n    ---@type snacks.image.args\n    mermaid = function()\n      local theme = vim.o.background == \"light\" and \"neutral\" or \"dark\"\n      return { \"-i\", \"{src}\", \"-o\", \"{file}\", \"-b\", \"transparent\", \"-t\", theme, \"-s\", \"{scale}\" }\n    end,\n    ---@type table<string,snacks.image.args>\n    magick = {\n      default = { \"{src}[0]\", \"-scale\", \"1920x1080>\" }, -- default for raster images\n      vector = { \"-density\", 192, \"{src}[{page}]\" }, -- used by vector images like svg\n      math = { \"-density\", 192, \"{src}[{page}]\", \"-trim\" },\n      pdf = { \"-density\", 192, \"{src}[{page}]\", \"-background\", \"white\", \"-alpha\", \"remove\", \"-trim\" },\n    },\n  },\n  math = {\n    enabled = true, -- enable math expression rendering\n    -- in the templates below, `${header}` comes from any section in your document,\n    -- between a start/end header comment. Comment syntax is language-specific.\n    -- * start comment: `// snacks: header start`\n    -- * end comment:   `// snacks: header end`\n    typst = {\n      tpl = [[\n        #set page(width: auto, height: auto, margin: (x: 2pt, y: 2pt))\n        #show math.equation.where(block: false): set text(top-edge: \"bounds\", bottom-edge: \"bounds\")\n        #set text(size: 12pt, fill: rgb(\"${color}\"))\n        ${header}\n        ${content}]],\n    },\n    latex = {\n      font_size = \"Large\", -- see https://www.sascha-frank.com/latex-font-size.html\n      -- for latex documents, the doc packages are included automatically,\n      -- but you can add more packages here. Useful for markdown documents.\n      packages = { \"amsmath\", \"amssymb\", \"amsfonts\", \"amscd\", \"mathtools\" },\n      tpl = [[\n        \\documentclass[preview,border=0pt,varwidth,12pt]{standalone}\n        \\usepackage{${packages}}\n        \\begin{document}\n        ${header}\n        { \\${font_size} \\selectfont\n          \\color[HTML]{${color}}\n        ${content}}\n        \\end{document}]],\n    },\n  },\n}\nM.config = Snacks.config.get(\"image\", defaults)\n\nSnacks.config.style(\"snacks_image\", {\n  relative = \"cursor\",\n  border = true,\n  focusable = false,\n  backdrop = false,\n  row = 1,\n  col = 1,\n  -- width/height are automatically set by the image size unless specified below\n})\n\nSnacks.util.set_hl({\n  Spinner = \"Special\",\n  Anchor = \"Special\",\n  Loading = \"NonText\",\n  Math = { fg = Snacks.util.color({ \"@markup.math.latex\", \"Special\", \"Normal\" }) },\n}, { prefix = \"SnacksImage\", default = true })\n\n---@class snacks.image.Opts\n---@field pos? snacks.image.Pos (row, col) (1,0)-indexed. defaults to the top-left corner\n---@field range? Range4\n---@field conceal? boolean\n---@field inline? boolean render the image inline in the buffer\n---@field width? number\n---@field min_width? number\n---@field max_width? number\n---@field height? number\n---@field min_height? number\n---@field max_height? number\n---@field on_update? fun(placement: snacks.image.Placement)\n---@field on_update_pre? fun(placement: snacks.image.Placement)\n---@field type? snacks.image.Type\n---@field auto_resize? boolean\n\nlocal did_setup = false\n\n--- Check if the file format is supported\n---@param file string\nfunction M.supports_file(file)\n  return vim.tbl_contains(M.config.formats or {}, vim.fn.fnamemodify(file, \":e\"):lower())\nend\n\n--- Check if the file format is supported and the terminal supports the kitty graphics protocol\n---@param file string\nfunction M.supports(file)\n  return M.supports_file(file) and M.supports_terminal()\nend\n\n-- Check if the terminal supports the kitty graphics protocol\nfunction M.supports_terminal()\n  return M.terminal.env().supported or M.config.force or false\nend\n\n--- Show the image at the cursor in a floating window\nfunction M.hover()\n  M.doc.hover()\nend\n\n---@return string[]\nfunction M.langs()\n  local queries = vim.api.nvim_get_runtime_file(\"queries/*/images.scm\", true)\n  return vim.tbl_map(function(q)\n    return q:match(\"queries/(.-)/images%.scm\")\n  end, queries)\nend\n\n---@private\n---@param ev? vim.api.keyset.create_autocmd.callback_args\nfunction M.setup(ev)\n  if did_setup then\n    return\n  end\n  did_setup = true\n\n  local group = vim.api.nvim_create_augroup(\"snacks.image\", { clear = true })\n\n  vim.api.nvim_create_autocmd({ \"BufWipeout\", \"BufDelete\" }, {\n    group = group,\n    callback = function(e)\n      vim.schedule(function()\n        Snacks.image.placement.clean(e.buf)\n      end)\n    end,\n  })\n  vim.api.nvim_create_autocmd({ \"ExitPre\" }, {\n    group = group,\n    once = true,\n    callback = function()\n      Snacks.image.placement.clean()\n    end,\n  })\n\n  if M.config.formats and #M.config.formats > 0 then\n    vim.api.nvim_create_autocmd(\"BufReadCmd\", {\n      pattern = \"*.\" .. table.concat(M.config.formats, \",*.\"),\n      group = group,\n      callback = function(e)\n        M.buf.attach(e.buf)\n      end,\n    })\n    -- prevent altering the original image file\n    vim.api.nvim_create_autocmd(\"BufWriteCmd\", {\n      pattern = \"*.\" .. table.concat(M.config.formats, \",*.\"),\n      group = group,\n      callback = function(e)\n        -- vim.api.nvim_exec_autocmds(\"BufWritePre\", { buffer = e.buf })\n        vim.bo[e.buf].modified = false\n        -- vim.api.nvim_exec_autocmds(\"BufWritePost\", { buffer = e.buf })\n      end,\n    })\n  end\n  if M.config.enabled and M.config.doc.enabled then\n    local langs = M.langs()\n    vim.api.nvim_create_autocmd(\"FileType\", {\n      group = group,\n      callback = function(e)\n        local ft = vim.bo[e.buf].filetype\n        local lang = vim.treesitter.language.get_lang(ft)\n        if vim.tbl_contains(langs, lang) then\n          vim.schedule(function()\n            if vim.api.nvim_buf_is_valid(e.buf) then\n              M.doc.attach(e.buf)\n            end\n          end)\n        end\n      end,\n    })\n  end\n  if ev and ev.event == \"BufReadCmd\" then\n    M.buf.attach(ev.buf)\n  end\nend\n\n---@private\nfunction M.health()\n  local detected = false\n  require(\"snacks.image.terminal\").detect(function()\n    detected = true\n  end)\n  vim.wait(1500, function()\n    return detected\n  end, 10)\n  Snacks.health.have_tool({ \"kitty\", \"wezterm\", \"ghostty\" })\n  local is_win = jit.os:find(\"Windows\")\n  if not Snacks.health.have_tool({ \"magick\", not is_win and \"convert\" or nil }) then\n    Snacks.health.error(\"`magick` is required to convert images. Only PNG files will be displayed.\")\n  end\n  local env = M.terminal.env()\n  for _, e in ipairs(M.terminal.envs()) do\n    if e.detected then\n      if e.supported == false then\n        Snacks.health.error(\"`\" .. e.name .. \"` is not supported\")\n      else\n        Snacks.health.ok(\"`\" .. e.name .. \"` detected and supported\")\n        if e.placeholders == false then\n          Snacks.health.warn(\"`\" .. e.name .. \"` does not support placeholders. Fallback rendering will be used\")\n          Snacks.health.warn(\"Inline images are disabled\")\n        elseif e.placeholders == true then\n          Snacks.health.ok(\"`\" .. e.name .. \"` supports unicode placeholders\")\n          Snacks.health.ok(\"Inline images are available\")\n        end\n      end\n    end\n  end\n  local size = M.terminal.size()\n  Snacks.health.ok(\n    (\"Terminal Dimensions:\\n- {size}: `%d` x `%d` pixels\\n- {scale}: `%.2f`\\n- {cell}: `%d` x `%d` pixels\"):format(\n      size.width,\n      size.height,\n      size.scale,\n      size.cell_width,\n      size.cell_height\n    )\n  )\n\n  local langs, _, missing = Snacks.health.has_lang(M.langs())\n  if missing > 0 then\n    Snacks.health.warn(\"Image rendering in docs with missing treesitter parsers won't work\")\n  end\n\n  if Snacks.health.have_tool(\"gs\") then\n    Snacks.health.ok(\"PDF files are supported\")\n  else\n    Snacks.health.warn(\"`gs` is required to render PDF files\")\n  end\n\n  if Snacks.health.have_tool({ \"tectonic\", \"pdflatex\" }) then\n    if langs.latex then\n      Snacks.health.ok(\"LaTeX math equations are supported\")\n    else\n      Snacks.health.warn(\"The `latex` treesitter parser is required to render LaTeX math expressions\")\n    end\n  else\n    Snacks.health.warn(\"`tectonic` or `pdflatex` is required to render LaTeX math expressions\")\n  end\n\n  if Snacks.health.have_tool(\"mmdc\") then\n    Snacks.health.ok(\"Mermaid diagrams are supported\")\n  else\n    Snacks.health.warn(\"`mmdc` is required to render Mermaid diagrams\")\n  end\n\n  if env.supported then\n    Snacks.health.ok(\"your terminal supports the kitty graphics protocol\")\n  elseif M.config.force then\n    Snacks.health.warn(\"image viewer is enabled with `opts.force = true`. Use at your own risk\")\n  else\n    Snacks.health.error(\"your terminal does not support the kitty graphics protocol\")\n    Snacks.health.info(\"supported terminals: `kitty`, `wezterm`, `ghostty`\")\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/inline.lua",
    "content": "---@class snacks.image.inline\n---@field buf number\n---@field imgs table<number, snacks.image.Placement>\n---@field idx table<number, snacks.image.Placement>\nlocal M = {}\nM.__index = M\n\nfunction M.new(buf)\n  local self = setmetatable({}, M)\n  self.buf = buf\n  self.imgs = {}\n  self.idx = {}\n  local group = vim.api.nvim_create_augroup(\"snacks.image.inline.\" .. buf, { clear = true })\n\n  local update = Snacks.util.debounce(function()\n    self:update()\n  end, { ms = 100 })\n\n  vim.api.nvim_create_autocmd({ \"BufWritePost\", \"WinScrolled\", \"BufWinEnter\" }, {\n    group = group,\n    buffer = buf,\n    callback = vim.schedule_wrap(update),\n  })\n  vim.api.nvim_create_autocmd({ \"ModeChanged\", \"CursorMoved\" }, {\n    group = group,\n    buffer = buf,\n    callback = function(ev)\n      if ev.buf == self.buf and ev.buf == vim.api.nvim_get_current_buf() then\n        self:conceal()\n      end\n    end,\n  })\n  vim.api.nvim_buf_attach(buf, false, {\n    on_lines = update,\n  })\n  vim.schedule(update)\n  return self\nend\n\nfunction M:conceal()\n  local mode = vim.fn.mode():sub(1, 1):lower() ---@type string\n  for _, img in pairs(self.imgs) do\n    img:show()\n  end\n  if vim.wo.concealcursor:find(mode) then\n    return\n  end\n  local from, to = vim.fn.line(\"v\"), vim.fn.line(\".\")\n  from, to = math.min(from, to), math.max(from, to)\n  local hide = self:get(from, to)\n  for _, img in pairs(hide) do\n    if img.opts.conceal then\n      img:hide()\n    end\n  end\nend\n\nfunction M:visible()\n  local ret = {} ---@type table<number, snacks.image.Placement>\n  for _, win in ipairs(vim.fn.win_findbuf(self.buf)) do\n    local info = vim.fn.getwininfo(win)[1]\n    for k, v in pairs(self:get(math.max(info.topline - 1, 1), info.botline)) do\n      ret[k] = v\n    end\n  end\n  return ret\nend\n\n---@param from number 1-indexed inclusive\n---@param to number 1-indexed inclusive\nfunction M:get(from, to)\n  local ret = {} ---@type table<number, snacks.image.Placement>\n  local marks = vim.api.nvim_buf_get_extmarks(self.buf, Snacks.image.placement.ns, { from - 1, 0 }, { to, -1 }, {\n    overlap = true,\n    hl_name = false,\n  })\n  for _, m in ipairs(marks) do\n    local p = self.idx[m[1]] ---@type snacks.image.Placement?\n    if p and not self.imgs[p.id] then\n      self.idx[m[1]] = nil\n      p = nil\n    end\n    if p then\n      ret[p.id] = p\n    end\n  end\n  return ret\nend\n\nfunction M:update()\n  local conceal = Snacks.image.config.doc.conceal\n  conceal = type(conceal) ~= \"function\" and function()\n    return conceal\n  end or conceal\n  Snacks.image.doc.find_visible(self.buf, function(imgs)\n    local visible = self:visible()\n    local stats = { new = 0, del = 0, update = 0 }\n    for _, i in ipairs(imgs) do\n      local img ---@type snacks.image.Placement?\n      for v, o in pairs(visible) do\n        if o.img.src == i.src then\n          img = o\n          visible[v] = nil\n          break\n        end\n      end\n      if not img then\n        stats.new = stats.new + 1\n        img = Snacks.image.placement.new(\n          self.buf,\n          i.src,\n          Snacks.config.merge({}, Snacks.image.config.doc, {\n            pos = i.pos,\n            range = i.range,\n            inline = true,\n            conceal = vim.b[self.buf].snacks_image_conceal or conceal(i.lang, i.type),\n            type = i.type,\n            ---@param p snacks.image.Placement\n            on_update = function(p)\n              for _, eid in ipairs(p.eids) do\n                self.idx[eid] = p\n              end\n            end,\n          })\n        )\n        for _, eid in ipairs(img.eids) do\n          self.idx[eid] = img\n        end\n        self.imgs[img.id] = img\n      else\n        stats.update = stats.update + 1\n        img.opts.pos = i.pos\n        img.opts.range = i.range\n        img:update()\n      end\n    end\n    for _, img in pairs(visible) do\n      stats.del = stats.del + 1\n      img:close()\n      self.imgs[img.id] = nil\n    end\n    for k, v in pairs(stats) do\n      stats[k] = v > 0 and v or nil\n    end\n    -- Snacks.notify(\n    --   vim.inspect({ all = vim.tbl_count(self.imgs), stats = stats }),\n    --   { ft = \"lua\", id = \"snacks.image.inline\" }\n    -- )\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/placement.lua",
    "content": "---@class snacks.image.Placement\n---@field img snacks.Image\n---@field id number image placement id\n---@field ns number\n---@field buf number\n---@field opts snacks.image.Opts\n---@field augroup number\n---@field hidden? boolean\n---@field closed? boolean\n---@field type? snacks.image.Type\n---@field _loc? snacks.image.Loc\n---@field _state? snacks.image.State\n---@field eids number[]\n---@field _extmarks? snacks.image.Extmark[]\nlocal M = {}\nM.__index = M\n\n---@alias snacks.image.Extmark vim.api.keyset.set_extmark|{row:number, col:number}\n\nlocal terminal = Snacks.image.terminal\nlocal uv = vim.uv or vim.loop\nlocal ns = vim.api.nvim_create_namespace(\"snacks.image\")\nM.ns = ns\nlocal PLACEHOLDER = vim.fn.nr2char(0x10EEEE)\nlocal placements = {} ---@type table<number, table<number, snacks.image.Placement>>\n-- stylua: ignore\nlocal diacritics = vim.split( \"0305,030D,030E,0310,0312,033D,033E,033F,0346,034A,034B,034C,0350,0351,0352,0357,035B,0363,0364,0365,0366,0367,0368,0369,036A,036B,036C,036D,036E,036F,0483,0484,0485,0486,0487,0592,0593,0594,0595,0597,0598,0599,059C,059D,059E,059F,05A0,05A1,05A8,05A9,05AB,05AC,05AF,05C4,0610,0611,0612,0613,0614,0615,0616,0617,0657,0658,0659,065A,065B,065D,065E,06D6,06D7,06D8,06D9,06DA,06DB,06DC,06DF,06E0,06E1,06E2,06E4,06E7,06E8,06EB,06EC,0730,0732,0733,0735,0736,073A,073D,073F,0740,0741,0743,0745,0747,0749,074A,07EB,07EC,07ED,07EE,07EF,07F0,07F1,07F3,0816,0817,0818,0819,081B,081C,081D,081E,081F,0820,0821,0822,0823,0825,0826,0827,0829,082A,082B,082C,082D,0951,0953,0954,0F82,0F83,0F86,0F87,135D,135E,135F,17DD,193A,1A17,1A75,1A76,1A77,1A78,1A79,1A7A,1A7B,1A7C,1B6B,1B6D,1B6E,1B6F,1B70,1B71,1B72,1B73,1CD0,1CD1,1CD2,1CDA,1CDB,1CE0,1DC0,1DC1,1DC3,1DC4,1DC5,1DC6,1DC7,1DC8,1DC9,1DCB,1DCC,1DD1,1DD2,1DD3,1DD4,1DD5,1DD6,1DD7,1DD8,1DD9,1DDA,1DDB,1DDC,1DDD,1DDE,1DDF,1DE0,1DE1,1DE2,1DE3,1DE4,1DE5,1DE6,1DFE,20D0,20D1,20D4,20D5,20D6,20D7,20DB,20DC,20E1,20E7,20E9,20F0,2CEF,2CF0,2CF1,2DE0,2DE1,2DE2,2DE3,2DE4,2DE5,2DE6,2DE7,2DE8,2DE9,2DEA,2DEB,2DEC,2DED,2DEE,2DEF,2DF0,2DF1,2DF2,2DF3,2DF4,2DF5,2DF6,2DF7,2DF8,2DF9,2DFA,2DFB,2DFC,2DFD,2DFE,2DFF,A66F,A67C,A67D,A6F0,A6F1,A8E0,A8E1,A8E2,A8E3,A8E4,A8E5,A8E6,A8E7,A8E8,A8E9,A8EA,A8EB,A8EC,A8ED,A8EE,A8EF,A8F0,A8F1,AAB0,AAB2,AAB3,AAB7,AAB8,AABE,AABF,AAC1,FE20,FE21,FE22,FE23,FE24,FE25,FE26,10A0F,10A38,1D185,1D186,1D187,1D188,1D189,1D1AA,1D1AB,1D1AC,1D1AD,1D242,1D243,1D244\", \",\")\n---@type table<number, string>\nlocal positions = {}\nsetmetatable(positions, {\n  __index = function(_, k)\n    positions[k] = vim.fn.nr2char(tonumber(diacritics[k], 16))\n    return positions[k]\n  end,\n})\n\n---@param buf? number\n---@param id? number\nfunction M.clean(buf, id)\n  for _, b in ipairs(buf and { buf } or vim.tbl_keys(placements)) do\n    for _, p in ipairs(id and { placements[b][id] } or vim.tbl_values(placements[b] or {})) do\n      if p then\n        p:close()\n      end\n    end\n  end\nend\n\n---@param buf number\n---@param opts? snacks.image.Opts\nfunction M.new(buf, src, opts)\n  assert(type(buf) == \"number\", \"`Image.new`: buf should be a number\")\n  assert(type(src) == \"string\", \"`Image.new`: src should be a string\")\n  Snacks.image.setup() -- always setup so that images/videos can be opened\n  local self = setmetatable({}, M)\n\n  self.img = Snacks.image.image.new(src)\n  self.img:place(self)\n  self.opts = opts or {}\n  self.opts.pos = self.opts.pos or { 1, 0 }\n  self.buf = buf\n  self.augroup = vim.api.nvim_create_augroup(\"snacks.image.\" .. self.id, { clear = true })\n  self.eids = {}\n\n  if self.opts.auto_resize then\n    vim.api.nvim_create_autocmd({ \"BufWinEnter\", \"WinEnter\", \"BufWinLeave\", \"BufEnter\" }, {\n      group = self.augroup,\n      buffer = self.buf,\n      callback = function()\n        vim.schedule(function()\n          self:update()\n        end)\n      end,\n    })\n    vim.api.nvim_create_autocmd({ \"WinClosed\", \"WinNew\", \"WinEnter\", \"WinResized\" }, {\n      group = self.augroup,\n      callback = function()\n        vim.schedule(function()\n          self:update()\n        end)\n      end,\n    })\n  end\n  placements[self.buf] = placements[self.buf] or {}\n  placements[self.buf][self.id] = self\n\n  if self:ready() then\n    vim.schedule(function()\n      self:update()\n    end)\n  elseif self.img:failed() then\n    self:error()\n  elseif self.opts.inline then\n    -- temporary extmark so that we can keep track of unloaded images in the buffer\n    self:_render({\n      {\n        row = self.opts.pos[1] - 1,\n        col = self.opts.pos[2],\n      },\n    })\n  else\n    self:progress()\n  end\n\n  local update = self.update\n  self.update = Snacks.util.debounce(function()\n    update(self)\n  end, { ms = 10 })\n  return self\nend\n\nfunction M:error()\n  if self.opts.inline then\n    return\n  end\n  local msg = \"# Image Conversion Failed:\\n\\n\"\n  local convert = self.img._convert\n  if convert then\n    for _, step in ipairs(convert.steps) do\n      if step.err then\n        msg = msg .. \"## \" .. step.name .. \"\\n\\n\" .. step.err .. \"\\n\\n\"\n        if step.proc then\n          msg = msg\n            .. Snacks.debug.cmd({\n              cmd = step.proc.opts.cmd,\n              args = step.proc.opts.args,\n              cwd = step.proc.opts.cwd,\n              notify = false,\n            })\n          msg = msg .. \"\\n\\n# Output\\n\" .. vim.trim(step.proc:out() .. \"\\n\" .. step.proc:err()) .. \"\\n\"\n        end\n      end\n    end\n  end\n  local lines = vim.split(msg, \"\\n\")\n  vim.bo[self.buf].modifiable = true\n  vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, lines)\n  vim.bo[self.buf].modifiable = false\n  if not vim.treesitter.start(self.buf, \"markdown\") then\n    vim.bo[self.buf].syntax = \"markdown\"\n  end\nend\n\nfunction M:progress()\n  if self.opts.inline or self:ready() then\n    return\n  end\n  vim.bo[self.buf].modifiable = true\n  vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, {})\n  vim.bo[self.buf].modifiable = false\n  local timer = assert(uv.new_timer())\n  timer:start(\n    0,\n    80,\n    vim.schedule_wrap(function()\n      if self:ready() or self.img:failed() or not vim.api.nvim_buf_is_valid(self.buf) then\n        timer:stop()\n        if not timer:is_closing() then\n          timer:close()\n        end\n        return\n      end\n      vim.api.nvim_buf_clear_namespace(self.buf, ns, 0, -1)\n      vim.api.nvim_buf_set_extmark(self.buf, ns, 0, 0, {\n        virt_text = {\n          { Snacks.util.spinner(), \"SnacksImageSpinner\" },\n          { \" \" },\n          { self.img._convert:current().name .. \" loading …\", \"SnacksImageLoading\" },\n        },\n      })\n    end)\n  )\nend\n\n---@return number[]\nfunction M:wins()\n  ---@param win number\n  return vim.tbl_filter(function(win)\n    return vim.api.nvim_win_get_buf(win) == self.buf\n  end, vim.api.nvim_tabpage_list_wins(0))\nend\n\nfunction M:close()\n  if self.closed then\n    return\n  end\n  placements[self.buf][self.id] = nil\n  self.closed = true\n  self:del()\n  self:debug(\"close\")\n  pcall(vim.api.nvim_del_augroup_by_id, self.augroup)\nend\n\nfunction M:del()\n  self.img:del(self.id)\n  if vim.api.nvim_buf_is_valid(self.buf) then\n    for _, eid in ipairs(self.eids) do\n      vim.api.nvim_buf_del_extmark(self.buf, ns, eid)\n    end\n  end\nend\n\n---@param row number\n---@param col number\nfunction M:is_concealed(row, col)\n  local captures = vim.treesitter.get_captures_at_pos(self.buf, row, col)\n  for _, cap in ipairs(captures) do\n    if vim.tbl_get(cap, \"metadata\", \"conceal_lines\") ~= nil then\n      return true\n    end\n  end\n  return false\nend\n\n---@param row number\nfunction M:find_line(row)\n  local line_count = vim.api.nvim_buf_line_count(self.buf)\n  while row < line_count and self:is_concealed(row, 0) do\n    row = row + 1\n  end\n  return row\nend\n\n--- Renders the unicode placeholder grid in the buffer\n---@param loc snacks.image.Loc\nfunction M:render_grid(loc)\n  local hl = \"SnacksImage\" .. self.id -- image id is encoded in the foreground color\n  Snacks.util.set_hl({\n    [hl] = {\n      fg = self.img.id,\n      sp = self.id,\n      bg = Snacks.image.config.debug.placement and \"#FF007C\" or \"none\",\n      nocombine = true,\n    },\n  })\n  local img = {} ---@type string[]\n  local height = math.min(#diacritics, loc.height)\n  local width = math.min(#diacritics, loc.width)\n  for r = 1, height do\n    local line = {} ---@type string[]\n    for c = 1, width do\n      -- cell positions are encoded as diacritics for the placeholder unicode character\n      line[#line + 1] = PLACEHOLDER\n      line[#line + 1] = positions[r]\n      line[#line + 1] = positions[c]\n    end\n    img[#img + 1] = table.concat(line)\n  end\n\n  local range = self.opts.range or { loc[1], loc[2], loc[1], loc[2] }\n  local lines = vim.api.nvim_buf_get_lines(self.buf, range[1] - 1, range[3], false)\n  local text_width = 0\n  for _, line in ipairs(lines) do\n    text_width = math.max(text_width, vim.api.nvim_strwidth(line))\n  end\n  local offset = range[2]\n  local has_after = lines[#lines]:sub(range[4] + 1):find(\"%S\") ~= nil\n  local has_before = lines[1]:sub(1, range[2]):find(\"%S\") ~= nil\n  local conceal = self.opts.conceal and \"\" or nil\n  local extmarks = {} ---@type snacks.image.Extmark[]\n\n  -- we can overlay the image if the text is multiline,\n  -- or the text has nothing after the image\n  -- and the text is not wrapped or the text fits the window width\n  local can_overlay = (#lines > 1 or not has_after)\n  for _, win in ipairs(can_overlay and self:wins() or {}) do\n    if vim.wo[win].wrap then\n      local info = vim.fn.getwininfo(win)[1]\n      if info.width - info.textoff < text_width then\n        can_overlay = false\n        break\n      end\n    end\n  end\n\n  if height == 1 and #lines == 1 then\n    -- render inline\n    self:_render({\n      {\n        row = range[1] - 1,\n        col = range[2],\n        end_row = range[3] - 1,\n        end_col = range[4],\n        conceal = conceal,\n        invalidate = vim.fn.has(\"nvim-0.10\") == 1 and true or nil,\n        virt_text_pos = \"inline\",\n        virt_text = { { img[1], hl } },\n        virt_text_hide = true,\n      },\n    })\n  elseif can_overlay then\n    if conceal then\n      -- conceal and overlay on the first line\n      if not self:is_concealed(range[1] - 1, range[2]) then\n        extmarks[#extmarks + 1] = {\n          row = range[1] - 1,\n          col = range[2],\n          end_row = range[3] - 1,\n          end_col = range[4],\n          conceal = conceal,\n          virt_text_pos = \"overlay\",\n          virt_text = { { table.remove(img, 1), hl } },\n          virt_text_hide = false,\n          virt_text_win_col = offset,\n        }\n      end\n      -- overlay over the other lines\n      for i = 1, math.min(#img, #lines - 1) do\n        if self:is_concealed(range[1] - 1 + i, 0) then\n          break\n        end\n        extmarks[#extmarks + 1] = {\n          row = range[1] - 1 + i,\n          col = 0,\n          virt_text_pos = \"overlay\",\n          virt_text = { { table.remove(img, 1), hl } },\n          virt_text_hide = false,\n          virt_text_win_col = offset,\n        }\n      end\n      -- conceal remaining lines if any\n      local last = extmarks[#extmarks]\n      if last and #img == 0 and (last.row < range[3] - 1) and vim.fn.has(\"nvim-0.11.4\") == 1 then\n        extmarks[#extmarks + 1] = {\n          row = last.row + 1,\n          end_row = range[3] - 1,\n          col = 0,\n          conceal_lines = \"\",\n          virt_text_hide = false,\n        }\n      end\n    end\n    if #img > 0 then\n      -- add additional virtual lines if there are more lines to render\n      local row = self:find_line(range[3] - 1)\n      local padding = string.rep(\" \", offset)\n      extmarks[#extmarks + 1] = {\n        row = row,\n        col = 0,\n        virt_lines_above = row ~= range[3] - 1,\n        ---@param l string\n        virt_lines = vim.tbl_map(function(l)\n          return { { padding }, { l, hl } }\n        end, img),\n        virt_text_hide = false,\n      }\n    end\n    self:_render(extmarks)\n  else\n    local is_inline = has_before or has_after\n    local icon = Snacks.image.config.icons[self.opts.type or \"image\"] or Snacks.image.config.icons.image\n    -- render below in virtual lines\n    extmarks[#extmarks + 1] = {\n      row = range[1] - 1,\n      col = range[2],\n      end_row = range[3] - 1,\n      end_col = range[4],\n      conceal = conceal,\n      virt_text = is_inline and { { icon, \"SnacksImageAnchor\" } } or nil,\n      virt_text_pos = \"inline\",\n      virt_text_hide = false,\n      ---@param l string\n      virt_lines = vim.tbl_map(function(l)\n        return { { l, hl } }\n      end, img),\n    }\n    self:_render(extmarks)\n  end\nend\n\n---@param extmarks snacks.image.Extmark[]\nfunction M:_render(extmarks)\n  for _, e in ipairs(extmarks) do\n    e.undo_restore = false\n    e.strict = false\n    if self.hidden then\n      e.virt_text = nil\n      e.conceal = nil\n      if e.virt_lines then\n        e.virt_lines = vim.tbl_map(function(l)\n          return { { \"\" } }\n        end, e.virt_lines)\n      end\n    end\n  end\n  local eids = {} ---@type number[]\n  for _, extmark in ipairs(extmarks) do\n    local row, col = extmark.row, extmark.col\n    extmark.row, extmark.col, extmark.id = nil, nil, table.remove(self.eids, 1)\n    table.insert(eids, vim.api.nvim_buf_set_extmark(self.buf, ns, row, col, extmark))\n  end\n  for _, eid in ipairs(self.eids) do\n    vim.api.nvim_buf_del_extmark(self.buf, ns, eid)\n  end\n  self.eids = eids\nend\n\nfunction M:hide()\n  if self.hidden or not self:ready() then\n    return\n  end\n  self.hidden = true\n  self:update()\nend\n\nfunction M:show()\n  if not self.hidden or not self:ready() then\n    return\n  end\n  self.hidden = false\n  self:update()\nend\n\n---@param state snacks.image.State\nfunction M:render_fallback(state)\n  if not self.opts.inline then\n    vim.api.nvim_buf_clear_namespace(self.buf, ns, 0, -1)\n  end\n  for _, win in ipairs(state.wins) do\n    self:debug(\"render_fallback\", win)\n    local border = setmetatable({ opts = vim.api.nvim_win_get_config(win) }, { __index = Snacks.win }):border_size()\n    local pos = vim.api.nvim_win_get_position(win)\n    if\n      (Snacks.config.styles.snacks_image.relative ~= \"editor\")\n      and ((vim.o.showtabline == 2) or (vim.o.showtabline == 1 and vim.fn.tabpagenr(\"$\") > 1))\n    then\n      terminal.set_cursor({ pos[1] + border.top, pos[2] + border.left })\n    else\n      terminal.set_cursor({ pos[1] + 1 + border.top, pos[2] + border.left })\n    end\n    terminal.request({\n      a = \"p\",\n      i = self.img.id,\n      p = self.id,\n      C = 1,\n      c = state.loc.width,\n      r = state.loc.height,\n    })\n  end\nend\n\nfunction M:debug(...)\n  if true or not Snacks.image.config.debug then\n    return\n  end\n  Snacks.debug.inspect({ ... }, self.img.src, self.img.id, self.id)\nend\n\nfunction M:state()\n  local width, height = vim.o.columns, vim.o.lines\n  local wins = {} ---@type number[]\n  local is_fallback = not terminal.env().placeholders\n  local zindex = vim.api.nvim_win_get_config(0).zindex or 0\n\n  for _, win in ipairs(self:wins()) do\n    width = math.min(width, vim.api.nvim_win_get_width(win))\n    height = math.min(height, vim.api.nvim_win_get_height(win))\n    if is_fallback then\n      local z = vim.api.nvim_win_get_config(win).zindex or 0\n      if z >= zindex or (zindex > 0 and z > 0) then\n        wins[#wins + 1] = win -- use if higher z-index or both are floating\n      end\n    else\n      wins[#wins + 1] = win\n    end\n  end\n\n  local function minmax(value, min, max)\n    return math.max(min or 1, math.min(value, max or value))\n  end\n\n  width = minmax(self.opts.width or width, self.opts.min_width, self.opts.max_width)\n  height = minmax(self.opts.height or height, self.opts.min_height, self.opts.max_height)\n  local size = Snacks.image.util.fit(self.img.file, { width = width, height = height }, { info = self.img.info })\n\n  local pos = self.opts.pos or { 1, 0 }\n\n  local function is_inline()\n    local range = self.opts.range or { pos[1], pos[2], pos[1], pos[2] }\n    if range[1] == range[3] then\n      local line = vim.api.nvim_buf_get_lines(self.buf, range[1] - 1, range[1], false)[1] or \"\"\n      local has_before = line:sub(1, range[2]):find(\"%S\") ~= nil\n      local has_after = line:sub(range[4] + 1):find(\"%S\") ~= nil\n      return has_before or has_after\n    end\n  end\n\n  -- scale down to fit inline\n  if size.height <= 2 and is_inline() then\n    size.width = math.ceil(size.width / size.height) + 2\n    size.height = 1\n  end\n\n  ---@class snacks.image.State\n  ---@field hidden boolean\n  ---@field loc snacks.image.Loc\n  ---@field wins number[]\n  return {\n    hidden = self.hidden or false,\n    loc = {\n      pos[1],\n      pos[2],\n      width = size.width,\n      height = size.height,\n    },\n    wins = wins,\n  }\nend\n\nfunction M:valid()\n  return self.buf\n    and vim.api.nvim_buf_is_valid(self.buf)\n    and self:ready()\n    and self.opts.pos[1] <= vim.api.nvim_buf_line_count(self.buf)\nend\n\nfunction M:update()\n  if not self:ready() then\n    return\n  end\n\n  if not self:valid() then\n    self:del()\n    return\n  end\n\n  if self.opts.on_update_pre then\n    self.opts.on_update_pre(self)\n  end\n\n  local state = self:state()\n  if vim.deep_equal(state, self._state) then\n    return\n  end\n  self._state = state\n\n  if #state.wins == 0 then\n    self:hide()\n    return\n  end\n  self.img:place(self)\n\n  self:debug(\"update\")\n\n  if not self.opts.inline then\n    for _, win in ipairs(state.wins) do\n      Snacks.util.wo(win, Snacks.image.config.wo or {})\n    end\n  end\n\n  if terminal.env().placeholders then\n    terminal.request({\n      a = \"p\",\n      U = 1,\n      i = self.img.id,\n      p = self.id,\n      C = 1,\n      c = state.loc.width,\n      r = state.loc.height,\n    })\n    self:render_grid(state.loc)\n  else\n    self:render_fallback(state)\n  end\n\n  if not self.opts.inline then\n    for _, win in ipairs(state.wins) do\n      vim.api.nvim_win_call(win, function()\n        vim.fn.winrestview({ topline = 1, lnum = 1, col = 0, leftcol = 0 })\n      end)\n    end\n  end\n  if self.opts.on_update then\n    self.opts.on_update(self)\n  end\nend\n\nfunction M:ready()\n  return not self.closed and self.buf and vim.api.nvim_buf_is_valid(self.buf) and self.img:ready()\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/terminal.lua",
    "content": "---@class snacks.image.terminal\n---@field transform? fun(data: string): string\nlocal M = {}\n\nlocal size ---@type snacks.image.terminal.Dim?\n---@type snacks.image.Env[]\nlocal environments = {\n  {\n    name = \"kitty\",\n    terminal = \"kitty\",\n    supported = true,\n    placeholders = true,\n  },\n  {\n    name = \"ghostty\",\n    terminal = \"ghostty\",\n    supported = true,\n    placeholders = true,\n  },\n  {\n    name = \"wezterm\",\n    terminal = \"wezterm\",\n    supported = true,\n    placeholders = false,\n  },\n  {\n    name = \"tmux\",\n    env = { TERM = \"tmux\", TMUX = true },\n    setup = function()\n      pcall(vim.fn.system, { \"tmux\", \"set\", \"-p\", \"allow-passthrough\", \"all\" })\n    end,\n    transform = function(data)\n      return (\"\\027Ptmux;\" .. data:gsub(\"\\027\", \"\\027\\027\")) .. \"\\027\\\\\"\n    end,\n  },\n  { name = \"zellij\", env = { TERM = \"zellij\", ZELLIJ = true }, supported = false, placeholders = false },\n  { name = \"ssh\", env = { SSH_CLIENT = true, SSH_CONNECTION = true }, remote = true },\n}\n\nM._env = nil ---@type snacks.image.Env?\n\nM._terminal = nil ---@type snacks.image.Terminal?\n\nvim.api.nvim_create_autocmd(\"VimResized\", {\n  group = vim.api.nvim_create_augroup(\"snacks.image.terminal\", { clear = true }),\n  callback = function()\n    size = nil\n  end,\n})\n\nfunction M.size()\n  if size then\n    return size\n  end\n  local ffi = require(\"ffi\")\n  ffi.cdef([[\n    typedef struct {\n      unsigned short row;\n      unsigned short col;\n      unsigned short xpixel;\n      unsigned short ypixel;\n    } winsize;\n    int ioctl(int, int, ...);\n  ]])\n\n  local TIOCGWINSZ = nil\n  if vim.fn.has(\"linux\") == 1 then\n    TIOCGWINSZ = 0x5413\n  elseif vim.fn.has(\"mac\") == 1 or vim.fn.has(\"bsd\") == 1 then\n    TIOCGWINSZ = 0x40087468\n  end\n\n  local dw, dh = 9, 18\n  ---@class snacks.image.terminal.Dim\n  size = {\n    width = vim.o.columns * dw,\n    height = vim.o.lines * dh,\n    columns = vim.o.columns,\n    rows = vim.o.lines,\n    cell_width = dw,\n    cell_height = dh,\n    scale = dw / 8,\n  }\n\n  pcall(function()\n    ---@type { row: number, col: number, xpixel: number, ypixel: number }\n    local sz = ffi.new(\"winsize\")\n    if ffi.C.ioctl(1, TIOCGWINSZ, sz) ~= 0 or sz.col == 0 or sz.row == 0 then\n      return\n    end\n    size = {\n      width = sz.xpixel,\n      height = sz.ypixel,\n      columns = sz.col,\n      rows = sz.row,\n      cell_width = sz.xpixel / sz.col,\n      cell_height = sz.ypixel / sz.row,\n      -- try to guess dpi scale\n      scale = math.max(1, sz.xpixel / sz.col / 8),\n    }\n  end)\n\n  return size\nend\n\nfunction M.envs()\n  return environments\nend\n\nfunction M.env()\n  if M._env then\n    return M._env\n  end\n  if not M._terminal then\n    M.detect()\n  end\n  M._env = {\n    name = \"\",\n    env = {},\n  }\n  for _, e in ipairs(environments) do\n    local override = os.getenv(\"SNACKS_\" .. e.name:upper())\n    if override then\n      e.detected = override ~= \"0\" and override ~= \"false\"\n    else\n      if e.terminal and M._terminal and M._terminal.terminal then\n        e.detected = M._terminal.terminal:lower():find(e.terminal:lower()) ~= nil\n      end\n      if not e.detected then\n        for k, v in pairs(e.env or {}) do\n          local val = os.getenv(k)\n          if val and (v == true or val:find(v)) then\n            e.detected = true\n            break\n          end\n        end\n      end\n    end\n    if e.detected then\n      M._env.name = M._env.name .. \"/\" .. e.name\n      if e.supported ~= nil then\n        M._env.supported = e.supported\n      end\n      if e.placeholders ~= nil then\n        M._env.placeholders = e.placeholders\n      end\n      M._env.transform = e.transform or M._env.transform\n      M._env.remote = e.remote or M._env.remote\n      if e.setup then\n        e.setup()\n      end\n    end\n  end\n  M._env.name = M._env.name:gsub(\"^/\", \"\")\n  return M._env\nend\n\n---@param opts table<string, string|number>|{data?: string}\nfunction M.request(opts)\n  opts.q = opts.q ~= false and (opts.q or 2) or nil -- silence all\n  local msg = {} ---@type string[]\n  for k, v in pairs(opts) do\n    if k ~= \"data\" then\n      table.insert(msg, string.format(\"%s=%s\", k, v))\n    end\n  end\n  msg = { table.concat(msg, \",\") }\n  if opts.data then\n    msg[#msg + 1] = \";\"\n    msg[#msg + 1] = tostring(opts.data)\n  end\n  local data = \"\\27_G\" .. table.concat(msg) .. \"\\27\\\\\"\n  if Snacks.image.config.debug.request and opts.m ~= 1 then\n    Snacks.debug.inspect(opts)\n  end\n  M.write(data)\nend\n\n---@param pos {[1]: number, [2]: number}\nfunction M.set_cursor(pos)\n  M.write(\"\\27[\" .. pos[1] .. \";\" .. (pos[2] + 1) .. \"H\")\nend\n\nfunction M.write(data)\n  data = M.transform and M.transform(data) or data\n  if vim.api.nvim_ui_send then\n    vim.api.nvim_ui_send(data)\n  else\n    io.stdout:write(data)\n  end\nend\n\n--- Detect terminal capabilities\n--- Will call the callback when detection is complete,\n--- or block until detection is complete if no callback is provided.\n---@param cb? fun(term: snacks.image.Terminal)\nfunction M.detect(cb)\n  if cb then -- async\n    return M._detect(cb)\n  end\n  -- sync\n  local detected = false\n  M.detect(function()\n    detected = true\n  end)\n  vim.wait(1500, function()\n    return detected\n  end, 10)\nend\n\n---@param cb fun(term: snacks.image.Terminal)\nfunction M._detect(cb)\n  if M._terminal then\n    if M._terminal.pending then\n      table.insert(M._terminal.pending, cb)\n      return\n    end\n    return cb(M._terminal)\n  end\n\n  ---@class snacks.image.Terminal\n  ---@field terminal? string\n  ---@field version? string\n  ---@field supported? boolean\n  ---@field placeholders? boolean\n  local ret = {\n    terminal = \"unknown\",\n    version = \"unknown\",\n    pending = { cb }, ---@type fun(term: snacks.image.Terminal)[]\n  }\n  M._terminal = ret\n\n  local timer = assert(vim.uv.new_timer())\n\n  local function on_done()\n    if timer and not timer:is_closing() then\n      timer:stop()\n      timer:close()\n    end\n    vim.schedule(function()\n      local todo = ret.pending or {}\n      ret.pending = nil\n      for _, c in ipairs(todo) do\n        c(ret)\n      end\n    end)\n  end\n\n  if vim.env.TMUX then\n    pcall(vim.fn.system, { \"tmux\", \"set\", \"-p\", \"allow-passthrough\", \"all\" })\n    M.transform = function(data)\n      return (\"\\027Ptmux;\" .. data:gsub(\"\\027\", \"\\027\\027\")) .. \"\\027\\\\\"\n    end\n    -- NOTE: When tmux has extended-keys enabled, Neovim's TermResponse autocmd doesn't fire.\n    -- Terminal response sequences leak as literal text instead of being captured.\n    -- Workaround: Query tmux directly for the terminal name instead of sending escape sequences.\n    -- See: https://github.com/folke/snacks.nvim/issues/2332\n    local ok, out = pcall(vim.fn.system, { \"tmux\", \"show\", \"-g\", \"extended-keys\" })\n    if ok and vim.trim(out):find(\" on$\") then\n      ok, out = pcall(vim.fn.system, { \"tmux\", \"display-message\", \"-p\", \"#{client_termname}\" })\n      if ok then\n        ret.terminal = vim.trim(out):gsub(\"^xterm%-\", \"\")\n        return vim.schedule(on_done)\n      end\n    end\n  end\n\n  local id = vim.api.nvim_create_autocmd(\"TermResponse\", {\n    group = vim.api.nvim_create_augroup(\"image.terminal.detect\", { clear = true }),\n    callback = function(ev)\n      local data = ev.data.sequence ---@type string\n      local term, version = data:match(\"P>|(%S+)%s*(.*)\")\n      if not (term and version) then\n        return\n      end\n      ret.terminal = term\n      ret.version = version\n      vim.schedule(on_done)\n      return true -- delete autocmd\n    end,\n  })\n\n  timer:start(1000, 0, function()\n    vim.schedule(function()\n      pcall(vim.api.nvim_del_autocmd, id)\n    end)\n    on_done()\n  end)\n\n  M.write(\"\\27[>q\")\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/image/util.lua",
    "content": "---@class snacks.image.util\nlocal M = {}\n\nlocal dims = {} ---@type table<string, snacks.image.Size>\n\n--- Get the dimensions of a PNG file\n---@param file string\n---@return snacks.image.Size\nfunction M.dim(file)\n  file = svim.fs.normalize(file)\n  if dims[file] then\n    return dims[file]\n  end\n  -- extract header with IHDR chunk\n  local fd = assert(io.open(file, \"rb\"), \"Failed to open file: \" .. file)\n  local header = fd:read(24) ---@type string\n  fd:close()\n\n  -- Check PNG signature\n  assert(header:sub(1, 8) == \"\\137PNG\\r\\n\\26\\n\", \"Not a valid PNG file: \" .. file)\n\n  -- Extract width and height from the IHDR chunk\n  local width = header:byte(17) * 16777216 + header:byte(18) * 65536 + header:byte(19) * 256 + header:byte(20)\n  local height = header:byte(21) * 16777216 + header:byte(22) * 65536 + header:byte(23) * 256 + header:byte(24)\n  dims[file] = { width = width, height = height }\n  return dims[file]\nend\n\n---@param size snacks.image.Size\nfunction M.pixels_to_cells(size)\n  local terminal = Snacks.image.terminal.size()\n  return M.norm({\n    width = size.width / terminal.cell_width,\n    height = size.height / terminal.cell_height,\n  })\nend\n\n---@param size snacks.image.Size\n---@return snacks.image.Size\nfunction M.norm(size)\n  return {\n    width = math.max(1, math.ceil(size.width)),\n    height = math.max(1, math.ceil(size.height)),\n  }\nend\n\n---@param file string\n---@param cells snacks.image.Size size in rows x columns\n---@param opts? { full?: boolean, info?: snacks.image.Info }\nfunction M.fit(file, cells, opts)\n  opts = opts or {}\n  local img_pixels ---@type snacks.image.Size\n  if opts.info then\n    local terminal = Snacks.image.terminal.size()\n    img_pixels = {}\n    img_pixels.height = opts.info.size.height / opts.info.dpi.height * 96 * terminal.scale\n    img_pixels.width = opts.info.size.width / opts.info.dpi.width * 96 * terminal.scale\n  else\n    img_pixels = M.dim(file)\n  end\n  local img_cells = M.pixels_to_cells(img_pixels)\n\n  local ret = vim.deepcopy(cells)\n  -- if not opts.full then\n  if img_cells.width <= cells.width and img_cells.height <= cells.height then\n    return img_cells\n  end\n  ret.width = math.min(cells.width, img_cells.width)\n  ret.height = math.min(cells.height, img_cells.height)\n  -- end\n\n  local scale = ret.width / ret.height\n  local img_scale = img_cells.width / img_cells.height\n  local fit_height = math.floor(ret.width / img_scale + 0.5)\n  local fit_width = math.floor(ret.height * img_scale + 0.5)\n\n  if ret.height == fit_height or ret.width == fit_width then\n    -- Image fits exactly\n  elseif img_scale > scale then\n    -- Image is wider relative to height - fit to width\n    ret.height = fit_height\n  else\n    -- Image is taller relative to width - fit to height\n    ret.width = fit_width\n  end\n  return M.norm(ret)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/indent.lua",
    "content": "---@class snacks.indent\nlocal M = {}\n\nM.meta = {\n  desc = \"Indent guides and scopes\",\n}\n\nM.enabled = false\n\n---@class snacks.indent.Config\n---@field enabled? boolean\nlocal defaults = {\n  indent = {\n    priority = 1,\n    enabled = true, -- enable indent guides\n    char = \"│\",\n    only_scope = false, -- only show indent guides of the scope\n    only_current = false, -- only show indent guides in the current window\n    hl = \"SnacksIndent\", ---@type string|string[] hl groups for indent guides\n    -- can be a list of hl groups to cycle through\n    -- hl = {\n    --     \"SnacksIndent1\",\n    --     \"SnacksIndent2\",\n    --     \"SnacksIndent3\",\n    --     \"SnacksIndent4\",\n    --     \"SnacksIndent5\",\n    --     \"SnacksIndent6\",\n    --     \"SnacksIndent7\",\n    --     \"SnacksIndent8\",\n    -- },\n  },\n  -- animate scopes. Enabled by default for Neovim >= 0.10\n  -- Works on older versions but has to trigger redraws during animation.\n  ---@class snacks.indent.animate: snacks.animate.Config\n  ---@field enabled? boolean\n  --- * out: animate outwards from the cursor\n  --- * up: animate upwards from the cursor\n  --- * down: animate downwards from the cursor\n  --- * up_down: animate up or down based on the cursor position\n  ---@field style? \"out\"|\"up_down\"|\"down\"|\"up\"\n  animate = {\n    enabled = vim.fn.has(\"nvim-0.10\") == 1,\n    style = \"out\",\n    easing = \"linear\",\n    duration = {\n      step = 20, -- ms per step\n      total = 500, -- maximum duration\n    },\n  },\n  ---@class snacks.indent.Scope.Config: snacks.scope.Config\n  scope = {\n    enabled = true, -- enable highlighting the current scope\n    priority = 200,\n    char = \"│\",\n    underline = false, -- underline the start of the scope\n    only_current = false, -- only show scope in the current window\n    hl = \"SnacksIndentScope\", ---@type string|string[] hl group for scopes\n  },\n  chunk = {\n    -- when enabled, scopes will be rendered as chunks, except for the\n    -- top-level scope which will be rendered as a scope.\n    enabled = false,\n    -- only show chunk scopes in the current window\n    only_current = false,\n    priority = 200,\n    hl = \"SnacksIndentChunk\", ---@type string|string[] hl group for chunk scopes\n    char = {\n      corner_top = \"┌\",\n      corner_bottom = \"└\",\n      -- corner_top = \"╭\",\n      -- corner_bottom = \"╰\",\n      horizontal = \"─\",\n      vertical = \"│\",\n      arrow = \">\",\n    },\n  },\n  -- filter for buffers to enable indent guides\n  ---@param buf number\n  ---@param win number\n  filter = function(buf, win)\n    return vim.g.snacks_indent ~= false and vim.b[buf].snacks_indent ~= false and vim.bo[buf].buftype == \"\"\n  end,\n  debug = false,\n}\n\n---@class snacks.indent.Scope: snacks.scope.Scope\n---@field win number\n---@field step? number\n---@field animate? {from: number, to: number}\n\nlocal config = Snacks.config.get(\"scope\", defaults)\nlocal ns = vim.api.nvim_create_namespace(\"snacks_indent\")\nlocal cache_extmarks = {} ---@type table<string, vim.api.keyset.set_extmark[]>\nlocal debug_timer = assert((vim.uv or vim.loop).new_timer())\nlocal cache_underline = {} ---@type table<string, boolean>\nlocal has_repeat_lb = vim.fn.has(\"nvim-0.10.0\") == 1\nlocal states = {} ---@type table<number, snacks.indent.State>\nlocal scopes ---@type snacks.scope.Listener?\nlocal stats = {\n  indents = 0,\n  extmarks = 0,\n  scope = 0,\n}\n\nSnacks.util.set_hl({\n  [\"\"] = \"NonText\",\n  Blank = \"SnacksIndent\",\n  Scope = \"Special\",\n  Chunk = \"SnacksIndentScope\",\n  [\"1\"] = \"DiagnosticInfo\",\n  [\"2\"] = \"DiagnosticHint\",\n  [\"3\"] = \"DiagnosticWarn\",\n  [\"4\"] = \"DiagnosticError\",\n  [\"5\"] = \"DiagnosticInfo\",\n  [\"6\"] = \"DiagnosticHint\",\n  [\"7\"] = \"DiagnosticWarn\",\n  [\"8\"] = \"DiagnosticError\",\n}, { prefix = \"SnacksIndent\", default = true })\n\n---@param level number\n---@param hl string|string[]\nlocal function get_hl(level, hl)\n  return type(hl) == \"string\" and hl or hl[(level - 1) % #hl + 1]\nend\n\n---@param hl string\nlocal function get_underline_hl(hl)\n  local ret = \"SnacksIndentUnderline_\" .. hl\n  if not cache_underline[hl] then\n    local fg = Snacks.util.color(hl, \"fg\")\n    vim.api.nvim_set_hl(0, ret, { sp = fg, underline = true })\n    cache_underline[hl] = true\n  end\n  return ret\nend\n\n--- Get the virtual text for the indent guide with\n--- the given indent level, left column and shiftwidth\n---@param indent number\n---@param state snacks.indent.State\nlocal function get_extmarks(indent, state)\n  local key = indent\n    .. \":\"\n    .. state.leftcol\n    .. \":\"\n    .. state.shiftwidth\n    .. \":\"\n    .. state.indent_offset\n    .. \":\"\n    .. (state.breakindent and \"bi\" or \"\")\n  if cache_extmarks[key] then\n    return cache_extmarks[key]\n  end\n  stats.extmarks = stats.extmarks + 1\n\n  local sw = state.shiftwidth\n  indent = math.floor(indent / sw) -- full visible indents\n  local offset = math.max(math.floor(state.indent_offset / sw), 0) -- offset for the scope\n  cache_extmarks[key] = {}\n\n  for i = 1 + offset, indent do\n    local col = (i - 1) * sw - state.leftcol\n    if col >= 0 then\n      table.insert(cache_extmarks[key], {\n        virt_text = { { config.indent.char, get_hl(i, config.indent.hl) } },\n        virt_text_pos = \"overlay\",\n        virt_text_win_col = col,\n        hl_mode = \"combine\",\n        priority = config.indent.priority,\n        ephemeral = true,\n        virt_text_repeat_linebreak = has_repeat_lb and state.breakindent or nil,\n      })\n    end\n  end\n  return cache_extmarks[key]\nend\n\n---@param win number\n---@param buf number\n---@param top number\n---@param bottom number\nlocal function get_state(win, buf, top, bottom)\n  local prev, changedtick = states[win], vim.b[buf].changedtick ---@type snacks.indent.State?, number\n  if not (prev and prev.buf == buf and prev.changedtick == changedtick) then\n    prev = nil\n  end\n  ---@class snacks.indent.State\n  ---@field indents table<number, number>\n  ---@field blanks table<number, boolean>\n  local state = {\n    win = win,\n    buf = buf,\n    changedtick = changedtick,\n    is_current = win == vim.api.nvim_get_current_win(),\n    top = top,\n    bottom = bottom,\n    leftcol = vim.api.nvim_buf_call(buf, vim.fn.winsaveview).leftcol --[[@as number]],\n    shiftwidth = vim.bo[buf].shiftwidth,\n    indents = prev and prev.indents or { [0] = 0 },\n    blanks = prev and prev.blanks or {},\n    indent_offset = 0, -- the start column of the indent guides\n    breakindent = vim.wo[win].breakindent and vim.wo[win].wrap,\n  }\n  state.shiftwidth = state.shiftwidth == 0 and vim.bo[buf].tabstop or state.shiftwidth\n  states[win] = state\n  return state\nend\n\nfunction M.debug_win()\n  Snacks.debug.inspect(states[vim.api.nvim_get_current_win()])\nend\n\n--- Called during every redraw cycle, so it should be fast.\n--- Everything that can be cached should be cached.\n---@param win number\n---@param buf number\n---@param top number -- 1-indexed\n---@param bottom number -- 1-indexed\n---@private\nfunction M.on_win(win, buf, top, bottom)\n  local state = get_state(win, buf, top, bottom)\n\n  local scope = scopes and scopes:get(win) --[[@as snacks.indent.Scope?]]\n  vim.api.nvim_buf_call(buf, function()\n    if scope and vim.fn.foldclosed(scope.from) ~= -1 then\n      scope = nil\n    end\n  end)\n\n  -- adjust top and bottom if only_scope is enabled\n  if config.indent.only_scope then\n    if not scope then\n      return\n    end\n    state.indent_offset = scope.indent or 0\n    state.top = math.max(state.top, scope.from)\n    state.bottom = math.min(state.bottom, scope.to)\n  end\n\n  local show_indent = config.indent.enabled and (not config.indent.only_current or state.is_current)\n  local show_scope = config.scope.enabled and (not config.scope.only_current or state.is_current)\n  local show_chunk = config.chunk.enabled and (not config.chunk.only_current or state.is_current)\n\n  -- Calculate and render indents\n  local indents = state.indents\n  vim.api.nvim_buf_call(buf, function()\n    local parent_indent, current_indent ---@type number, number\n    for l = state.top, state.bottom do\n      local indent = indents[l]\n      if not indent then\n        stats.indents = stats.indents + 1\n        local next = vim.fn.nextnonblank(l)\n        -- Indent for a blank line is the minimum of the previous and next non-blank line.\n        -- If the previous and next non-blank lines have different indents, add shiftwidth.\n        if next ~= l then\n          state.blanks[l] = true\n          local prev = vim.fn.prevnonblank(l)\n          indents[prev] = indents[prev] or vim.fn.indent(prev)\n          indents[next] = indents[next] or vim.fn.indent(next)\n          indent = math.min(indents[prev], indents[next])\n          if indents[prev] ~= indents[next] and indent > 0 then\n            indent = indent + state.shiftwidth\n          end\n        else\n          indent = vim.fn.indent(l)\n        end\n        indents[l] = indent\n      end\n      if indent ~= current_indent then\n        parent_indent = current_indent or indent\n        current_indent = indent\n      end\n      indent = math.min(indent, parent_indent + state.shiftwidth)\n      local extmarks = show_indent and indent > 0 and get_extmarks(indent, state)\n      for _, opts in ipairs(extmarks or {}) do\n        vim.api.nvim_buf_set_extmark(buf, ns, l - 1, 0, opts)\n      end\n    end\n  end)\n\n  -- Render scope\n  if scope and (scope:size() > 1 or vim.g.snacks_indent_overlap) then\n    show_chunk = show_chunk and (scope.indent or 0) >= state.shiftwidth\n    if show_chunk then\n      M.render_chunk(scope, state)\n    elseif show_scope then\n      M.render_scope(scope, state)\n    end\n  end\nend\n\n---@param scope snacks.indent.Scope\n---@param state snacks.indent.State\n---@return number from, number to\nlocal function bounds(scope, state)\n  local from, to = scope.from, scope.to\n  if scope.animate then\n    from = math.max(scope.animate.from, scope.from)\n    to = math.min(scope.animate.to, scope.to)\n  end\n  from = math.max(from, state.top)\n  to = math.min(to, state.bottom)\n  return from, to\nend\n\n--- Render the scope overlapping the given range\n---@param scope snacks.indent.Scope\n---@param state snacks.indent.State\n---@private\nfunction M.render_scope(scope, state)\n  local indent = (scope.indent or 2)\n  local hl = get_hl(math.floor(scope.indent / state.shiftwidth) + 1, config.scope.hl)\n  local from, to = bounds(scope, state)\n  local col = indent - state.leftcol\n\n  if config.scope.underline and scope.from == from then\n    local scope_first_line = vim.api.nvim_buf_get_lines(scope.buf, scope.from - 1, scope.from, false)[1]\n    if scope_first_line ~= nil then\n      vim.api.nvim_buf_set_extmark(scope.buf, ns, scope.from - 1, math.max(col, 0), {\n        end_col = #scope_first_line,\n        hl_group = get_underline_hl(hl),\n        hl_mode = \"combine\",\n        priority = config.scope.priority + 1,\n        strict = false,\n        ephemeral = true,\n      })\n    end\n  end\n\n  if col < 0 then -- scope is hidden\n    return\n  end\n\n  for l = from, to do\n    local i = state.indents[l]\n    if (i and i > indent) or vim.g.snacks_indent_overlap or state.blanks[l] then\n      vim.api.nvim_buf_set_extmark(scope.buf, ns, l - 1, 0, {\n        virt_text = { { config.scope.char, hl } },\n        virt_text_pos = \"overlay\",\n        virt_text_win_col = col,\n        hl_mode = \"combine\",\n        priority = config.scope.priority,\n        strict = false,\n        ephemeral = true,\n        virt_text_repeat_linebreak = has_repeat_lb and state.breakindent or nil,\n      })\n    end\n  end\nend\n\n--- Render the scope overlappping the given range\n---@param scope snacks.indent.Scope\n---@param state snacks.indent.State\n---@private\nfunction M.render_chunk(scope, state)\n  local indent = (scope.indent or 2)\n  local col = indent - state.leftcol - state.shiftwidth\n  if col < 0 then -- scope is hidden\n    return\n  end\n  local from, to = bounds(scope, state)\n  local hl = get_hl(math.floor(scope.indent / state.shiftwidth) + 1, config.chunk.hl)\n  local char = config.chunk.char\n\n  ---@param l number\n  ---@param line string\n  ---@param repeat_indent? boolean\n  local function add(l, line, repeat_indent)\n    vim.api.nvim_buf_set_extmark(scope.buf, ns, l - 1, 0, {\n      virt_text = { { line, hl } },\n      virt_text_pos = \"overlay\",\n      virt_text_win_col = col,\n      hl_mode = \"combine\",\n      priority = config.chunk.priority,\n      strict = false,\n      virt_text_repeat_linebreak = has_repeat_lb and repeat_indent or nil,\n      ephemeral = true,\n    })\n  end\n\n  for l = from, to do\n    local i = state.indents[l] - state.leftcol\n    if l == scope.from then -- top line\n      if state.breakindent then\n        add(l, char.vertical, true)\n      end\n      add(l, char.corner_top .. (char.horizontal):rep(i - col - 1))\n    elseif l == scope.to then -- bottom line\n      add(l, char.corner_bottom .. (char.horizontal):rep(i - col - 2) .. char.arrow)\n    elseif i and i > col then -- middle line\n      add(l, char.vertical, state.breakindent)\n    end\n  end\nend\n\n---@param scope snacks.indent.Scope\n---@param value number\n---@param prev? number\nlocal function step(scope, value, prev)\n  if not vim.api.nvim_win_is_valid(scope.win) then\n    return\n  end\n  prev = prev or 0\n  local cursor = vim.api.nvim_win_get_cursor(scope.win)\n  local dt = math.abs(scope.from - cursor[1])\n  local db = math.abs(scope.to - cursor[1])\n  local style = config.animate.style == \"up_down\" and (dt < db and \"down\" or \"up\") or config.animate.style\n  if style == \"down\" then\n    scope.animate = { from = scope.from, to = scope.from + value }\n  elseif style == \"up\" then\n    scope.animate = { from = scope.to - value, to = scope.to }\n  elseif style == \"out\" then\n    local line = math.min(math.max(scope.from, cursor[1]), scope.to)\n    scope.animate = {\n      from = math.max(scope.from, line - value),\n      to = math.min(scope.to, line + value),\n    }\n  else\n    Snacks.notify.error(\"Invalid animate style: \" .. style, { title = \"Snacks Indent\", once = true })\n  end\n  Snacks.util.redraw_range(scope.win, scope.animate.from, scope.animate.to)\nend\n\n-- Called when the scope changes\n---@param win number\n---@param buf number\n---@param scope snacks.indent.Scope?\n---@param prev snacks.indent.Scope?\n---@private\nfunction M.on_scope(win, buf, scope, prev)\n  stats.scope = stats.scope + 1\n  if scope then\n    scope.win = win\n    local animate = Snacks.animate.enabled({ buf = buf, name = \"indent\" })\n\n    vim.api.nvim_buf_call(buf, function()\n      -- skip animation if new lines have been added before or inside the scope\n      if prev and (vim.fn.nextnonblank(prev.from) == scope.from) then\n        animate = false\n      end\n    end)\n\n    if animate then\n      step(scope, 0)\n      Snacks.animate(\n        0,\n        scope.to - scope.from,\n        function(value, ctx)\n          if scopes and scopes:get(win) ~= scope then\n            return\n          end\n          step(scope, value, ctx.prev)\n        end,\n        vim.tbl_extend(\"keep\", {\n          int = true,\n          id = \"indent_scope_\" .. win,\n          buf = buf,\n        }, config.animate)\n      )\n    else\n      Snacks.util.redraw_range(win, scope.from, scope.to)\n    end\n  end\n  if prev then -- clear previous scope\n    Snacks.util.redraw_range(win, prev.from, prev.to)\n  end\nend\n\n---@private\nfunction M.debug()\n  if debug_timer:is_active() then\n    debug_timer:stop()\n    return\n  end\n  local last = {}\n  debug_timer:start(50, 50, function()\n    if not vim.deep_equal(stats, last) then\n      last = vim.deepcopy(stats)\n      Snacks.notify(vim.inspect(stats), { ft = \"lua\", id = \"snacks_indent_debug\", title = \"Snacks Indent Debug\" })\n    end\n  end)\nend\n\n--- Enable indent guides\nfunction M.enable()\n  if M.enabled then\n    return\n  end\n  config = Snacks.config.get(\"indent\", defaults)\n\n  if config.debug then\n    M.debug()\n  end\n\n  vim.g.snacks_animate_indent = config.animate.enabled\n\n  M.enabled = true\n\n  -- setup decoration provider\n  vim.api.nvim_set_decoration_provider(ns, {\n    on_win = function(_, win, buf, top, bottom)\n      if M.enabled and config.filter(buf, win) then\n        M.on_win(win, buf, top + 1, bottom + 1)\n      end\n    end,\n  })\n\n  -- Listen for scope changes\n  scopes = scopes or Snacks.scope.attach(M.on_scope, config.scope)\n  if not scopes.enabled then\n    scopes:enable()\n  end\n\n  local group = vim.api.nvim_create_augroup(\"snacks_indent\", { clear = true })\n\n  vim.api.nvim_create_autocmd(\"ColorScheme\", {\n    group = group,\n    callback = function()\n      cache_underline = {}\n    end,\n  })\n\n  -- cleanup cache\n  vim.api.nvim_create_autocmd({ \"WinClosed\", \"BufDelete\", \"BufWipeout\" }, {\n    group = group,\n    callback = function()\n      for win in pairs(states) do\n        if not vim.api.nvim_win_is_valid(win) then\n          states[win] = nil\n        end\n      end\n    end,\n  })\n\n  -- redraw when shiftwidth changes\n  -- vim.api.nvim_create_autocmd(\"OptionSet\", {\n  --   group = group,\n  --   pattern = { \"shiftwidth\", \"listchars\", \"list\" },\n  --   callback = vim.schedule_wrap(function()\n  --     vim.cmd([[redraw!]])\n  --   end),\n  -- })\nend\n\n-- Disable indent guides\nfunction M.disable()\n  if not M.enabled then\n    return\n  end\n  M.enabled = false\n  if scopes then\n    scopes:disable()\n  end\n  vim.api.nvim_del_augroup_by_name(\"snacks_indent\")\n  debug_timer:stop()\n  states = {}\n  stats = { indents = 0, extmarks = 0, scope = 0 }\n  vim.cmd([[redraw!]])\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/init.lua",
    "content": "---@class Snacks: snacks.plugins\nlocal M = {}\n\nsetmetatable(M, {\n  __index = function(t, k)\n    ---@diagnostic disable-next-line: no-unknown\n    t[k] = require(\"snacks.\" .. k)\n    return rawget(t, k)\n  end,\n})\n\n_G.Snacks = M\n_G.svim = vim.fn.has(\"nvim-0.11\") == 1 and vim or require(\"snacks.compat\")\n\nM.version = \"2.31.0\" -- x-release-please-version\n\n---@class snacks.Config.base\n---@field example? string\n---@field config? fun(opts: table, defaults: table)\n\n---@class snacks.Config: snacks.plugins.Config\n---@field styles? table<string, snacks.win.Config>\n---@field image? snacks.image.Config|{}\nlocal config = {\n  image = {\n    -- define these here, so that we don't need to load the image module\n    formats = {\n      \"png\",\n      \"jpg\",\n      \"jpeg\",\n      \"gif\",\n      \"bmp\",\n      \"webp\",\n      \"tiff\",\n      \"heic\",\n      \"avif\",\n      \"mp4\",\n      \"mov\",\n      \"avi\",\n      \"mkv\",\n      \"webm\",\n      \"pdf\",\n      \"icns\",\n    },\n  },\n}\nconfig.styles = {}\n\n---@class snacks.config: snacks.Config\nM.config = setmetatable({}, {\n  __index = function(_, k)\n    config[k] = config[k] or {}\n    return config[k]\n  end,\n  __newindex = function(_, k, v)\n    config[k] = v\n  end,\n})\n\nlocal is_dict_like = function(v) -- has string and number keys\n  return type(v) == \"table\" and (vim.tbl_isempty(v) or not svim.islist(v))\nend\nlocal is_dict = function(v) -- has only string keys\n  return type(v) == \"table\" and (vim.tbl_isempty(v) or not v[1])\nend\n\n--- Merges the values similar to vim.tbl_deep_extend with the **force** behavior,\n--- but the values can be any type\n---@generic T\n---@param ... T\n---@return T\nfunction M.config.merge(...)\n  local ret = select(1, ...)\n  for i = 2, select(\"#\", ...) do\n    local value = select(i, ...)\n    if is_dict_like(ret) and is_dict(value) then\n      for k, v in pairs(value) do\n        ret[k] = M.config.merge(ret[k], v)\n      end\n    elseif value ~= nil then\n      ret = value\n    end\n  end\n  return ret\nend\n\n--- Get an example config from the docs/examples directory.\n---@param snack string\n---@param name string\n---@param opts? table\nfunction M.config.example(snack, name, opts)\n  local path = vim.fn.fnamemodify(debug.getinfo(1, \"S\").source:sub(2), \":h:h:h\") .. \"/docs/examples/\" .. snack .. \".lua\"\n  local ok, ret = pcall(function()\n    return loadfile(path)().examples[name] or error((\"`%s` not found\"):format(name))\n  end)\n  if not ok then\n    M.notify.error((\"Failed to load `%s.%s`:\\n%s\"):format(snack, name, ret))\n  end\n  return ok and vim.tbl_deep_extend(\"force\", {}, vim.deepcopy(ret), opts or {}) or {}\nend\n\n---@generic T: table\n---@param snack string\n---@param defaults T\n---@param ... T[]\n---@return T\nfunction M.config.get(snack, defaults, ...)\n  local merge, todo = {}, { defaults, config[snack] or {}, ... }\n  for i = 1, select(\"#\", ...) + 2 do\n    local v = todo[i] --[[@as snacks.Config.base]]\n    if type(v) == \"table\" then\n      if v.example then\n        table.insert(merge, vim.deepcopy(M.config.example(snack, v.example)))\n        v.example = nil\n      end\n      table.insert(merge, vim.deepcopy(v))\n    end\n  end\n  local ret = M.config.merge(unpack(merge))\n  if type(ret.config) == \"function\" then\n    ret.config(ret, defaults)\n  end\n  return ret\nend\n\n--- Register a new window style config.\n---@param name string\n---@param defaults snacks.win.Config|{}\n---@return string\nfunction M.config.style(name, defaults)\n  config.styles[name] = vim.tbl_deep_extend(\"force\", vim.deepcopy(defaults), config.styles[name] or {})\n  return name\nend\n\nM.did_setup = false\nM.did_setup_after_vim_enter = false\n\n---@param opts snacks.Config?\nfunction M.setup(opts)\n  if M.did_setup then\n    return vim.notify(\"snacks.nvim is already setup\", vim.log.levels.ERROR, { title = \"snacks.nvim\" })\n  end\n  M.did_setup = true\n\n  if vim.fn.has(\"nvim-0.9.4\") ~= 1 then\n    return vim.notify(\"snacks.nvim requires Neovim >= 0.9.4\", vim.log.levels.ERROR, { title = \"snacks.nvim\" })\n  end\n\n  -- enable all by default when config is passed\n  opts = opts or {}\n  for k in pairs(opts) do\n    opts[k].enabled = opts[k].enabled == nil or opts[k].enabled\n  end\n  config = vim.tbl_deep_extend(\"force\", config, opts or {})\n\n  local events = {\n    BufReadPre = { \"bigfile\", \"image\" },\n    BufReadPost = { \"quickfile\", \"indent\" },\n    BufEnter = { \"explorer\" },\n    LspAttach = { \"words\" },\n    UIEnter = { \"dashboard\", \"scroll\", \"input\", \"scope\", \"picker\" },\n  }\n\n  ---@param event string\n  ---@param ev? vim.api.keyset.create_autocmd.callback_args\n  local function load(event, ev)\n    local todo = events[event] or {}\n    events[event] = nil\n    for _, snack in ipairs(todo) do\n      if M.config[snack] and M.config[snack].enabled then\n        if M[snack].setup then\n          M[snack].setup(ev)\n        elseif M[snack].enable then\n          M[snack].enable()\n        end\n      end\n    end\n  end\n\n  if vim.v.vim_did_enter == 1 then\n    M.did_setup_after_vim_enter = true\n    load(\"UIEnter\")\n  end\n\n  local group = vim.api.nvim_create_augroup(\"snacks\", { clear = true })\n  vim.api.nvim_create_autocmd(vim.tbl_keys(events), {\n    group = group,\n    once = true,\n    nested = true,\n    callback = function(ev)\n      load(ev.event, ev)\n    end,\n  })\n\n  if M.config.image.enabled and #M.config.image.formats > 0 then\n    vim.api.nvim_create_autocmd(\"BufReadCmd\", {\n      once = true,\n      pattern = \"*.\" .. table.concat(M.config.image.formats, \",*.\"),\n      group = group,\n      callback = function(e)\n        require(\"snacks.image\").setup(e)\n      end,\n    })\n  end\n\n  vim.api.nvim_create_autocmd(\"BufReadCmd\", {\n    once = true,\n    pattern = \"gh://*\",\n    group = group,\n    callback = function(e)\n      require(\"snacks.gh\").setup(e)\n    end,\n  })\n\n  if M.config.statuscolumn.enabled then\n    vim.o.statuscolumn = [[%!v:lua.require'snacks.statuscolumn'.get()]]\n  end\n\n  if M.config.notifier.enabled then\n    vim.notify = function(msg, level, o)\n      vim.notify = Snacks.notifier.notify\n      return Snacks.notifier.notify(msg, level, o)\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/input.lua",
    "content": "---@class snacks.input\n---@overload fun(opts: snacks.input.Opts, on_confirm: fun(value?: string)): snacks.win\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.input(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Better `vim.ui.input`\",\n  needs_setup = true,\n}\n\n---@alias snacks.input.Pos \"left\"|\"title\"|false\n\n---@class snacks.input.Config\n---@field enabled? boolean\n---@field win? snacks.win.Config|{}\n---@field icon? string\n---@field icon_pos? snacks.input.Pos\n---@field prompt_pos? snacks.input.Pos\nlocal defaults = {\n  icon = \" \",\n  icon_hl = \"SnacksInputIcon\",\n  icon_pos = \"left\",\n  prompt_pos = \"title\",\n  win = { style = \"input\" },\n  expand = true,\n}\n\nSnacks.util.set_hl({\n  Icon = \"DiagnosticHint\",\n  Normal = \"Normal\",\n  Border = \"DiagnosticInfo\",\n  Title = \"DiagnosticInfo\",\n  Prompt = \"SnacksInputTitle\",\n}, { prefix = \"SnacksInput\", default = true })\n\nSnacks.config.style(\"input\", {\n  backdrop = false,\n  position = \"float\",\n  border = true,\n  title_pos = \"center\",\n  height = 1,\n  width = 60,\n  relative = \"editor\",\n  noautocmd = true,\n  row = 2,\n  -- relative = \"cursor\",\n  -- row = -3,\n  -- col = 0,\n  wo = {\n    winhighlight = \"NormalFloat:SnacksInputNormal,FloatBorder:SnacksInputBorder,FloatTitle:SnacksInputTitle\",\n    cursorline = false,\n  },\n  bo = {\n    filetype = \"snacks_input\",\n    buftype = \"prompt\",\n  },\n  --- buffer local variables\n  b = {\n    completion = false, -- disable blink completions in input\n  },\n  keys = {\n    n_esc = { \"<esc>\", { \"cmp_close\", \"cancel\" }, mode = \"n\", expr = true },\n    i_esc = { \"<esc>\", { \"cmp_close\", \"stopinsert\" }, mode = \"i\", expr = true },\n    i_cr = { \"<cr>\", { \"cmp_accept\", \"confirm\" }, mode = { \"i\", \"n\" }, expr = true },\n    i_tab = { \"<tab>\", { \"cmp_select_next\", \"cmp\" }, mode = \"i\", expr = true },\n    i_ctrl_w = { \"<c-w>\", \"<c-s-w>\", mode = \"i\", expr = true },\n    i_up = { \"<up>\", { \"hist_up\" }, mode = { \"i\", \"n\" } },\n    i_down = { \"<down>\", { \"hist_down\" }, mode = { \"i\", \"n\" } },\n    q = \"cancel\",\n  },\n})\n\nlocal ui_input = vim.ui.input\n\n---@alias snacks.input.Highlight {[1]:number, [2]:number, [3]:string}\n\n---@class snacks.input.Opts: snacks.input.Config,{}\n---@field prompt? string\n---@field default? string\n---@field completion? string\n---@field highlight? fun(text: string): snacks.input.Highlight[]\n\n---@class snacks.input.ctx\n---@field opts? snacks.input.Opts\n---@field win? snacks.win\nlocal ctx = {}\n\nlocal ns = vim.api.nvim_create_namespace(\"snacks.input\")\n\n---@param opts? snacks.input.Opts\n---@param on_confirm fun(value?: string)\nfunction M.input(opts, on_confirm)\n  assert(type(on_confirm) == \"function\", \"`on_confirm` must be a function\")\n\n  local history = require(\"snacks.picker.util.history\").new(\"input\", {\n    filter = function(value)\n      return value ~= \"\"\n    end,\n  })\n\n  local parent_win = vim.api.nvim_get_current_win()\n  local mode = vim.fn.mode()\n\n  ---@param force? boolean\n  local function record(force)\n    if not ctx.win then\n      return\n    end\n    if not force and not history:is_current() then\n      return\n    end\n    local text = vim.trim(ctx.win:text())\n    if text == \"\" then\n      return\n    end\n    history:record(text)\n  end\n\n  local function confirm(value)\n    record()\n    ctx.win = nil\n    ctx.opts = nil\n    vim.cmd.stopinsert()\n    vim.schedule(function()\n      if vim.api.nvim_win_is_valid(parent_win) then\n        vim.api.nvim_set_current_win(parent_win)\n        if mode == \"i\" then\n          vim.cmd(\"startinsert\")\n        end\n      end\n      on_confirm(value)\n    end)\n  end\n\n  opts = Snacks.config.get(\"input\", defaults, opts) --[[@as snacks.input.Opts]]\n  opts.prompt = opts.prompt or \"Input\"\n  opts.prompt = vim.trim(opts.prompt)\n  opts.prompt = opts.prompt_pos == \"title\" and opts.prompt:gsub(\":$\", \"\") or opts.prompt\n\n  local title, statuscolumn = {}, {} ---@type string[], string[]\n  local function add(text, hl, pos)\n    if pos == \"title\" then\n      table.insert(title, { \" \" .. text, hl })\n    else\n      table.insert(statuscolumn, \"%#\" .. hl .. \"#\" .. text)\n    end\n  end\n\n  if opts.icon_pos and (opts.icon or \"\") ~= \"\" then\n    add(opts.icon, \"SnacksInputIcon\", opts.icon_pos)\n  end\n  add(opts.prompt, \"SnacksInputTitle\", opts.prompt_pos)\n\n  if next(title) then\n    table.insert(title, { \" \", \"SnacksInputTitle\" })\n  end\n\n  ---@param text? string\n  local function set(text)\n    text = text or \"\"\n    vim.api.nvim_buf_set_lines(ctx.win.buf, 0, -1, false, { text })\n    vim.api.nvim_win_set_cursor(ctx.win.win, { 1, #text })\n  end\n\n  opts.win = Snacks.win.resolve(\"input\", opts.win, {\n    enter = true,\n    title = next(title) and title or nil,\n    bo = {\n      modifiable = true,\n      completefunc = \"v:lua.Snacks.input.complete\",\n      omnifunc = \"v:lua.Snacks.input.complete\",\n    },\n    wo = {\n      statuscolumn = next(statuscolumn) and \" \" .. table.concat(statuscolumn, \" \") .. \" \" or \" \",\n    },\n    actions = {\n      cancel = function(self)\n        confirm()\n        self:close()\n      end,\n      stopinsert = function()\n        vim.schedule(function()\n          vim.cmd(\"stopinsert\")\n        end)\n      end,\n      confirm = function(self)\n        confirm(self:text())\n        self:close()\n      end,\n      hist_up = function(self)\n        record()\n        set(history:prev())\n      end,\n      hist_down = function(self)\n        record()\n        set(history:next())\n      end,\n      cmp = function()\n        return vim.fn.pumvisible() == 0 and \"<c-x><c-u>\"\n      end,\n      cmp_close = function()\n        return vim.fn.pumvisible() == 1 and \"<c-e>\"\n      end,\n      cmp_accept = function()\n        return vim.fn.pumvisible() == 1 and \"<c-y>\"\n      end,\n      cmp_select_next = function()\n        return vim.fn.pumvisible() == 1 and \"<c-n>\"\n      end,\n      cmp_select_prev = function()\n        return vim.fn.pumvisible() == 1 and \"<c-p>\"\n      end,\n    },\n  })\n\n  local parent_zindex = vim.api.nvim_win_get_config(parent_win).zindex\n  opts.win.zindex = math.max((parent_zindex or 50) + 1, opts.win.zindex or 50)\n\n  local min_width = opts.win.width or 60\n  if opts.expand then\n    ---@param self snacks.win\n    opts.win.width = function(self)\n      local w = type(min_width) == \"function\" and min_width(self) or min_width --[[@as number]]\n      return math.max(w, vim.api.nvim_strwidth(self:text()) + 5)\n    end\n  end\n\n  local win = Snacks.win(opts.win)\n  ctx = { opts = opts, win = win }\n  vim.fn.prompt_setprompt(win.buf, \"\")\n  if opts.default then\n    vim.api.nvim_buf_set_lines(win.buf, 0, -1, false, { opts.default })\n  end\n\n  local function highlight()\n    if type(opts.highlight) ~= \"function\" then\n      return\n    end\n    local text = win:text()\n    vim.api.nvim_buf_clear_namespace(win.buf, ns, 0, -1)\n    for _, hl in ipairs(opts.highlight(text)) do\n      vim.api.nvim_buf_set_extmark(win.buf, ns, 0, hl[1], {\n        end_col = hl[2],\n        hl_group = hl[3],\n        strict = false,\n      })\n    end\n  end\n\n  highlight()\n\n  vim.api.nvim_win_call(win.win, function()\n    vim.cmd(\"startinsert!\")\n  end)\n\n  vim.fn.prompt_setcallback(win.buf, function(text)\n    confirm(text)\n    win:close()\n  end)\n  vim.fn.prompt_setinterrupt(win.buf, function()\n    confirm()\n    win:close()\n  end)\n\n  win:on({ \"TextChangedI\", \"TextChanged\" }, function()\n    if not win:valid() then\n      return\n    end\n    highlight()\n    vim.bo[win.buf].modified = false\n    if opts.expand then\n      if vim.api.nvim_win_is_valid(parent_win) then\n        vim.api.nvim_win_call(parent_win, function()\n          win:update()\n        end)\n      end\n      vim.api.nvim_win_call(win.win, function()\n        vim.fn.winrestview({ leftcol = 0 })\n      end)\n    end\n  end, { buf = true })\n  return win\nend\n\n---@param findstart number\n---@param base string\n---@private\nfunction M.complete(findstart, base)\n  local completion = ctx.opts.completion\n  if findstart == 1 then\n    return #ctx.win:text():gsub(\"%S+$\", \"\")\n  end\n  if not completion then\n    return {}\n  end\n  local ok, results = pcall(vim.fn.getcompletion, base, completion)\n  return ok and results or {}\nend\n\nfunction M.enable()\n  vim.ui.input = M.input\nend\n\nfunction M.disable()\n  vim.ui.input = ui_input\nend\n\n---@private\nfunction M.health()\n  if Snacks.config.get(\"input\", defaults).enabled then\n    if vim.ui.input == M.input then\n      Snacks.health.ok(\"`vim.ui.input` is set to `Snacks.input`\")\n    else\n      Snacks.health.error(\"`vim.ui.input` is not set to `Snacks.input`\")\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/keymap.lua",
    "content": "---@class snacks.keymap\nlocal M = {}\n\nM.meta = {\n  desc = \"Better `vim.keymap` with support for filetypes and LSP clients\",\n  needs_setup = false,\n}\n\n---@class snacks.keymap.set.Opts: vim.keymap.set.Opts\n---@field ft? string|string[] Filetype(s) to set the keymap for.\n---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.\n---@field enabled? boolean|fun(buf?:number): boolean condition to enable the keymap.\n\n---@class snacks.keymap.del.Opts: vim.keymap.del.Opts\n---@field buffer? boolean|number If true or 0, use the current buffer.\n---@field ft? string|string[] Filetype(s) to set the keymap for.\n---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.\n\n---@class snacks.Keymap\n---@field id number           Unique ID for the keymap.\n---@field key string          Unique key for the keymap, in the format \"mode:lhs\".\n---@field mode string         Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n---@field lhs string          Left-hand side |{lhs}| of the mapping.\n---@field rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function.\n---@field lsp? vim.lsp.get_clients.Filter\n---@field opts? snacks.keymap.set.Opts\n---@field enabled fun(buf:number): boolean\n\nlocal by_ft = {} ---@type table<string, table<string,snacks.Keymap>>\nlocal by_lsp = {} ---@type table<string, snacks.Keymap> -- all LSP keymaps, indexed by lsp filter string + keymap key\nlocal lsp_on = {} ---@type table<string, boolean> -- tracks which LSP filters we're listening to\nlocal lsp_dirty = {} ---@type table<number, true> -- tracks which buffers need their LSP keymaps re-evaluated\nlocal kid = 0\nlocal valid = {\n  buffer = true,\n  desc = true,\n  callback = true,\n  remap = true,\n  silent = true,\n  expr = true,\n  nowait = true,\n  unique = true,\n  script = true,\n  replace_keycodes = true,\n  noremap = true,\n}\nlocal did_setup = false\n\n---@param filter vim.lsp.get_clients.Filter\nlocal function lsp_key(filter)\n  local ret = {}\n  for k, v in pairs(filter) do\n    table.insert(ret, (\"%s=%s\"):format(k, v))\n  end\n  table.sort(ret)\n  return table.concat(ret, \",\")\nend\n\n---@param buf number\nlocal function on_ft(buf)\n  local ft = vim.bo[buf].filetype\n  for _, map in pairs(by_ft[ft] or {}) do\n    if map.enabled(buf) then\n      vim.keymap.set(map.mode, map.lhs, map.rhs, Snacks.config.merge(map.opts or {}, { buffer = buf }))\n    end\n  end\nend\n\n---@param buf number\nlocal function on_lsp_buf(buf)\n  if not vim.api.nvim_buf_is_valid(buf) then\n    return -- buffer was closed before we could update it, ignore\n  end\n  local keys = vim.tbl_values(by_lsp) ---@type snacks.Keymap[]\n  table.sort(keys, function(a, b)\n    return a.id > b.id -- newer keymaps first, so they take precedence\n  end)\n  local done = {} ---@type table<string, boolean>\n  local matches = {} ---@type table<string, true>\n  for _, map in ipairs(keys) do\n    if not done[map.key] and map.enabled(buf) then\n      local filter = Snacks.config.merge(vim.deepcopy(map.lsp or {}), { bufnr = buf })\n      local lkey = lsp_key(filter)\n      if matches[lkey] == nil then\n        matches[lkey] = #(vim.lsp.get_clients(filter)) > 0\n      end\n      if matches[lkey] then\n        done[map.key] = true\n        vim.keymap.set(map.mode, map.lhs, map.rhs, Snacks.config.merge(map.opts or {}, { buffer = buf }))\n      end\n    end\n  end\nend\n\nlocal function on_lsp()\n  for buf in pairs(lsp_dirty) do\n    lsp_dirty[buf] = nil\n    on_lsp_buf(buf)\n  end\nend\n\nlocal function setup()\n  if did_setup then\n    return\n  end\n  did_setup = true\n  on_lsp = Snacks.util.debounce(on_lsp, { ms = 100 })\n  vim.api.nvim_create_autocmd(\"FileType\", {\n    group = vim.api.nvim_create_augroup(\"snacks.keymap.ft\", { clear = true }),\n    callback = function(ev)\n      on_ft(ev.buf)\n    end,\n  })\nend\n\n---@generic T: snacks.keymap.set.Opts|snacks.keymap.del.Opts\n---@param ... T\n---@return T opts, string[]? fts, vim.lsp.get_clients.Filter? lsp, fun(buf?:number) enabled\nlocal function get_opts(...)\n  ---@type snacks.keymap.set.Opts|snacks.keymap.del.Opts\n  local opts = Snacks.config.merge({}, ...)\n  opts.silent = opts.silent ~= false\n  opts.buffer = (opts.buffer == 0 or opts.buffer == true) and vim.api.nvim_get_current_buf() or opts.buffer\n  local fts = opts.ft and (type(opts.ft) == \"table\" and opts.ft or { opts.ft }) or nil --[[@as string[] ]]\n  local lsp = opts.lsp\n  local ret = vim.deepcopy(opts) ---@type table<string, any>\n  for k in pairs(ret) do\n    if not valid[k] then\n      ret[k] = nil\n    end\n  end\n  local enabled = function(buf)\n    if type(opts.enabled) == \"function\" then\n      return opts.enabled(buf)\n    end\n    return opts.enabled ~= false\n  end\n  return ret, fts, lsp, enabled\nend\n\n---@param mode string|string[] Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n---@param lhs string           Left-hand side |{lhs}| of the mapping.\n---@param rhs string|function  Right-hand side |{rhs}| of the mapping, can be a Lua function.\n---@param opts? snacks.keymap.set.Opts\nfunction M.set(mode, lhs, rhs, opts)\n  setup()\n  if type(mode) == \"table\" then\n    for _, m in ipairs(mode) do\n      M.set(m, lhs, rhs, opts)\n    end\n    return\n  end\n\n  local _opts, fts, lsp, enabled = get_opts(opts)\n  kid = kid + 1\n\n  local key = (\"%s:%s\"):format(mode, lhs)\n  ---@type snacks.Keymap\n  local km = { id = kid, key = key, mode = mode, lhs = lhs, rhs = rhs, lsp = lsp, opts = _opts, enabled = enabled }\n\n  if lsp then\n    local lkey = lsp_key(lsp)\n    by_lsp[lkey .. \":\" .. key] = km\n    if not lsp_on[lkey] then\n      lsp_on[lkey] = true\n      Snacks.util.lsp.on(lsp, function(buf)\n        -- always re-evaluate all LSP keymaps for the buffer,\n        -- to respect the order of keymaps with the same mode:lhs\n        lsp_dirty[buf] = true\n        on_lsp()\n      end)\n    end\n  elseif fts then\n    for _, ft in ipairs(fts) do\n      by_ft[ft] = by_ft[ft] or {}\n      by_ft[ft][key] = km\n    end\n    for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n      if vim.api.nvim_buf_is_loaded(buf) and vim.tbl_contains(fts, vim.bo[buf].filetype) then\n        on_ft(buf)\n      end\n    end\n  else\n    if\n      enabled(_opts and _opts.buffer or nil --[[@as integer?]])\n    then\n      vim.keymap.set(mode, lhs, rhs, _opts)\n    end\n  end\nend\n\n---@param mode string|string[] Mode \"short-name\" (see |nvim_set_keymap()|), or a list thereof.\n---@param lhs string           Left-hand side |{lhs}| of the mapping.\n---@param opts? snacks.keymap.del.Opts\nfunction M.del(mode, lhs, opts)\n  if type(mode) == \"table\" then\n    for _, m in ipairs(mode) do\n      M.del(m, lhs, opts)\n    end\n    return\n  end\n\n  local _opts, fts, lsp = get_opts(opts)\n  local key = (\"%s:%s\"):format(mode, lhs)\n\n  if lsp then\n    local lkey = lsp_key(lsp)\n    by_lsp[lkey .. \":\" .. key] = nil\n    -- re-evaluate all LSP keymaps for all buffers with clients matching this filter,\n    -- since lower-priority keymaps may now take precedence\n    for _, client in ipairs(vim.lsp.get_clients(lsp)) do\n      for buf in pairs(client.attached_buffers) do\n        lsp_dirty[buf] = true\n      end\n    end\n    on_lsp()\n  elseif fts then\n    for _, ft in ipairs(fts) do\n      if by_ft[ft] then\n        by_ft[ft][key] = nil\n      end\n    end\n  else\n    vim.keymap.del(mode, lhs, _opts)\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/layout.lua",
    "content": "---@class snacks.layout\n---@field opts snacks.layout.Config\n---@field root snacks.win\n---@field wins table<string, snacks.win|{enabled?:boolean, layout?:boolean}>\n---@field box_wins snacks.win[]\n---@field win_opts table<string, snacks.win.Config>\n---@field closed? boolean\n---@field split? boolean\n---@field screenpos number[]?\nlocal M = {}\nM.__index = M\n\nM.meta = {\n  desc = \"Window layouts\",\n}\n\n---@class snacks.layout.Win: snacks.win.Config,{}\n---@field depth? number\n---@field win string layout window name\n\n---@class snacks.layout.Box: snacks.layout.Win,{}\n---@field box \"horizontal\" | \"vertical\"\n---@field id? number\n---@field [number] snacks.layout.Win | snacks.layout.Box children\n\n---@alias snacks.layout.Widget snacks.layout.Win | snacks.layout.Box\n\n---@class snacks.layout.Config\n---@field show? boolean show the layout on creation (default: true)\n---@field wins table<string, snacks.win> windows to include in the layout\n---@field layout snacks.layout.Box layout definition\n---@field fullscreen? boolean open in fullscreen\n---@field hidden? string[] list of windows that will be excluded from the layout (but can be toggled)\n---@field on_update? fun(layout: snacks.layout)\n---@field on_update_pre? fun(layout: snacks.layout)\n---@field on_close? fun(layout: snacks.layout)\nlocal defaults = {\n  layout = {\n    width = 0.6,\n    height = 0.6,\n    zindex = 50,\n  },\n}\n\n---@param opts snacks.layout.Config\nfunction M.new(opts)\n  local self = setmetatable({}, M)\n  self.opts = vim.tbl_extend(\"force\", defaults, opts)\n  self.win_opts = {}\n  self.wins = self.opts.wins or {}\n  self.box_wins = {}\n  self.opts.layout.zindex = Snacks.win.zindex(self.opts.layout.zindex) + 2\n\n  -- wrap the split layout in a vertical box\n  -- this is needed since a simple split window can't have borders/titles\n  if self.opts.layout.position and self.opts.layout.position ~= \"float\" then\n    self.split = true\n    local inner = self.opts.layout\n    self.opts.layout = {\n      zindex = 30,\n      box = \"vertical\",\n      position = inner.position,\n      width = inner.width,\n      height = inner.height,\n      backdrop = inner.backdrop,\n      inner,\n    }\n    inner.width, inner.height, inner.col, inner.row, inner.position = 0, 0, 0, 0, nil\n  end\n\n  -- assign ids to boxes and create box wins if needed\n  local id = 1\n  self:each(function(box, parent)\n    box.depth = (parent and parent.depth + 1) or 0\n    if box.box then\n      ---@cast box snacks.layout.Box\n      box.id, id = id, id + 1\n      local has_border = box.border and box.border ~= \"\" and box.border ~= \"none\"\n      local is_root = box.id == 1\n      if is_root or has_border then\n        local backdrop = false\n        if is_root then\n          backdrop = nil\n        end\n        self.box_wins[box.id] = Snacks.win(Snacks.win.resolve(box, {\n          relative = is_root and (box.relative or \"editor\") or \"win\",\n          focusable = false,\n          enter = false,\n          show = false,\n          resize = false,\n          noautocmd = true,\n          backdrop = backdrop,\n          zindex = (self.opts.layout.zindex or 50) + box.depth,\n          bo = { filetype = \"snacks_layout_box\", buftype = \"nofile\" },\n          w = { snacks_layout = true },\n          border = box.border,\n        }))\n      end\n    end\n  end)\n  self.root = self.box_wins[1]\n  assert(self.root, \"no root box found\")\n\n  for w, win in pairs(self.wins) do\n    self.win_opts[w] = vim.deepcopy(win.opts)\n    if win.opts.relative == \"win\" then\n      win.layout = false\n    end\n  end\n\n  -- close layout when any win is closed\n  self.root:on(\"WinClosed\", function(_, ev)\n    if self.closed then\n      return true\n    end\n    local wid = tonumber(ev.match)\n    for _, win in pairs(self:get_wins()) do\n      if win.win == wid then\n        self:close()\n        return true\n      end\n    end\n  end)\n\n  self.root:on(\"WinResized\", function(_, ev)\n    if self.closed then\n      return true\n    end\n    if not self.root:on_current_tab() then\n      return\n    end\n    local sp = vim.fn.screenpos(self.root.win, 1, 1)\n    if not vim.deep_equal(sp, self.screenpos) then\n      self.screenpos = sp\n      return self:update()\n    else\n      if vim.tbl_contains(vim.v.event.windows, self.root.win) then\n        return self:update()\n      end\n      for _, win in pairs(self.wins) do\n        if win:win_valid() and vim.tbl_contains(vim.v.event.windows, win.win) then\n          local width_diff = vim.api.nvim_win_get_width(win.win) - win.opts.width\n          local height_diff = vim.api.nvim_win_get_height(win.win) - win.opts.height\n          if width_diff ~= 0 then\n            vim.api.nvim_win_set_width(self.root.win, vim.api.nvim_win_get_width(self.root.win) + width_diff)\n          end\n          if height_diff ~= 0 then\n            vim.api.nvim_win_set_height(self.root.win, vim.api.nvim_win_get_height(self.root.win) + height_diff)\n          end\n          if width_diff ~= 0 or height_diff ~= 0 then\n            return self:update()\n          end\n        end\n      end\n    end\n  end)\n\n  -- update layout on VimResized\n  self.root:on(\"VimResized\", function()\n    if not self.root:on_current_tab() then\n      return\n    end\n    self:update()\n  end)\n  if self.opts.show ~= false then\n    vim.schedule(function()\n      self:show()\n    end)\n  end\n  return self\nend\n\n---@param cb fun(widget: snacks.layout.Widget, parent?: snacks.layout.Box)\n---@param opts? {wins?:boolean, boxes?:boolean, box?:snacks.layout.Box}\nfunction M:each(cb, opts)\n  opts = opts or {}\n  ---@param widget snacks.layout.Widget\n  ---@param parent? snacks.layout.Box\n  local function _each(widget, parent)\n    if widget.box then\n      if opts.boxes ~= false then\n        cb(widget, parent)\n      end\n      ---@cast widget snacks.layout.Box\n      for _, child in ipairs(widget) do\n        _each(child, widget)\n      end\n    elseif opts.wins ~= false then\n      cb(widget, parent)\n    end\n  end\n  _each(opts.box or self.opts.layout)\nend\n\n---@param win string\nfunction M:needs_layout(win)\n  local w = self.wins[win]\n  return w and w.layout ~= false and not self:is_hidden(win)\nend\n\n--- Check if a window is hidden\n---@param win string\nfunction M:is_hidden(win)\n  return self.opts.hidden and vim.tbl_contains(self.opts.hidden, win)\nend\n\n--- Toggle a window\n---@param win string\n---@param enable? boolean\n---@param on_update? fun(enabled: boolean) called when the layout will be updated\nfunction M:toggle(win, enable, on_update)\n  self.opts.hidden = self.opts.hidden or {}\n  local enabled = not self:is_hidden(win)\n  if enable == nil then\n    enable = not enabled\n  end\n  if enable == enabled then\n    return\n  end\n  if enable then\n    self.opts.hidden = vim.tbl_filter(function(w)\n      return w ~= win\n    end, self.opts.hidden)\n  else\n    table.insert(self.opts.hidden, win)\n  end\n  if on_update then\n    on_update(enable)\n  end\n  self:update()\nend\n\n---@package\nfunction M:update()\n  if self.closed then\n    return\n  end\n  vim.o.lazyredraw = true\n  for _, win in pairs(self.wins) do\n    win.enabled = false\n  end\n  local layout = vim.deepcopy(self.opts.layout)\n  if self.opts.fullscreen then\n    layout.width = 0\n    layout.height = 0\n    layout.col = 0\n    layout.row = 0\n  end\n  if not self.root:valid() then\n    self.root:show()\n    self.screenpos = vim.fn.screenpos(self.root.win, 1, 1)\n  end\n\n  -- Calculate offsets for vertical splits\n  local top, bottom = 0, 0\n  local pos = self.opts.layout.position\n  if pos and (pos == \"left\" or pos == \"right\") or self.opts.fullscreen then\n    bottom = (vim.o.cmdheight + (vim.o.laststatus == 3 and 1 or 0)) or 0\n    top = (vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)) and 1 or 0\n  end\n\n  local parent_width = layout.relative == \"win\" and vim.api.nvim_win_get_width(self.root.opts.win or 0) or vim.o.columns\n  local parent_height = layout.relative == \"win\" and vim.api.nvim_win_get_height(self.root.opts.win or 0)\n    or vim.o.lines - top - bottom\n\n  self:update_box(layout, {\n    col = 0,\n    row = self.opts.fullscreen and self.split and top or 0, -- only needed for fullscreen splits\n    width = parent_width,\n    height = parent_height,\n  })\n\n  -- fix fullscreen float layouts\n  if self.opts.fullscreen and not self.split then\n    self.root.opts.row = self.root.opts.row + top\n  end\n\n  if self.opts.on_update_pre then\n    self.opts.on_update_pre(self)\n  end\n\n  for _, win in pairs(self:get_wins()) do\n    if win:valid() then\n      -- update windows with eventignore=all\n      -- to fix issues with syntax being reset\n      local ei = vim.o.eventignore\n      vim.o.eventignore = \"all\"\n      win:update()\n      vim.o.eventignore = ei\n    else\n      win:show()\n    end\n  end\n  for w, win in pairs(self.wins) do\n    if not self:is_enabled(w) and win:win_valid() then\n      win:close()\n    end\n  end\n  vim.o.lazyredraw = false\n  if self.opts.on_update then\n    self.opts.on_update(self)\n  end\nend\n\n---@param box snacks.layout.Box\n---@param parent snacks.win.Dim\n---@private\nfunction M:update_box(box, parent)\n  local size_main = box.box == \"horizontal\" and \"width\" or \"height\"\n  local pos_main = box.box == \"horizontal\" and \"col\" or \"row\"\n  local is_root = box.id == 1\n\n  if not is_root then\n    box.col = box.col or 0\n    box.row = box.row or 0\n  end\n\n  local children = {} ---@type snacks.layout.Widget[]\n  for c, child in ipairs(box) do\n    if not child.win or self:needs_layout(child.win) then\n      children[#children + 1] = child\n    end\n    box[c] = nil\n  end\n  for c, child in ipairs(children) do\n    box[c] = child\n  end\n\n  local dim, border = self:dim_box(box, parent)\n  local orig_dim = vim.deepcopy(dim)\n  if is_root then\n    dim.col = parent.col\n    dim.row = parent.row\n  else\n    dim.col = dim.col + border.left + parent.col\n    dim.row = dim.row + border.top + parent.row\n  end\n  local free = vim.deepcopy(dim)\n\n  local box_win = self.box_wins[box.id]\n\n  local function size(child)\n    local ret = child[size_main] or 0\n    if type(ret) == \"function\" then\n      ret = ret(box_win)\n    end\n    return ret\n  end\n\n  local dims = {} ---@type table<number, snacks.win.Dim>\n  local flex = 0\n\n  -- fixed\n  for c, child in ipairs(box) do\n    if size(child) > 0 then\n      dims[c] = self:resolve(child, dim)\n      free[size_main] = free[size_main] - dims[c][size_main]\n    else\n      flex = flex + 1\n    end\n  end\n\n  -- flex\n  local free_main = free[size_main]\n  for c, child in ipairs(box) do\n    if not dims[c] then\n      -- alocate at least 1 cell\n      free[size_main] = math.max(math.floor(free_main / flex), 1)\n      flex = flex - 1\n      free_main = free_main - free[size_main]\n      dims[c] = self:resolve(child, free)\n    end\n  end\n\n  -- fix positions\n  local offset = 0\n  for c, child in ipairs(box) do\n    dims[c][pos_main] = offset\n    local wins = self:get_wins(child, { layout = true })\n    for _, win in ipairs(wins) do\n      win.opts[pos_main] = win.opts[pos_main] + offset\n    end\n    offset = offset + dims[c][size_main]\n  end\n\n  -- if we still have free space, shrink the root box\n  -- if we have negative space, enlarge the root box\n  if free_main ~= 0 and is_root then\n    orig_dim[size_main] = orig_dim[size_main] - free_main\n  end\n\n  -- update box win\n  if box_win then\n    if not is_root then\n      box_win.opts.win = self.root.win\n    end\n    box_win.opts.col = parent.col + orig_dim.col\n    box_win.opts.row = parent.row + orig_dim.row\n    box_win.opts.width = orig_dim.width\n    box_win.opts.height = orig_dim.height\n  end\n\n  -- return outer dimensions\n  orig_dim.width = orig_dim.width + border.left + border.right\n  orig_dim.height = orig_dim.height + border.top + border.bottom\n  return orig_dim\nend\n\n---@param widget? snacks.layout.Widget\n---@param opts? {layout: boolean}\n---@package\nfunction M:get_wins(widget, opts)\n  opts = opts or {}\n  local ret = {} ---@type snacks.win[]\n  self:each(function(w)\n    if w.box and self.box_wins[w.id] then\n      table.insert(ret, self.box_wins[w.id])\n    elseif w.win and self:is_enabled(w.win) then\n      local win = self.wins[w.win]\n      if not (opts.layout and win.layout == false) then\n        table.insert(ret, self.wins[w.win])\n      end\n    end\n  end, { box = widget })\n  return ret\nend\n\n---@param widget snacks.layout.Widget\n---@param parent snacks.win.Dim\n---@private\nfunction M:resolve(widget, parent)\n  if widget.box then\n    ---@cast widget snacks.layout.Box\n    return self:update_box(widget, parent)\n  else\n    assert(widget.win, \"widget must have win or box\")\n    ---@cast widget snacks.layout.Win\n    return self:update_win(widget, parent)\n  end\nend\n\n---@param widget snacks.layout.Box\n---@param parent snacks.win.Dim\n---@private\nfunction M:dim_box(widget, parent)\n  -- honor the actual window size for split layouts\n  if not self.opts.fullscreen and widget.id == 1 and self.split and self.root:valid() then\n    return {\n      height = vim.api.nvim_win_get_height(self.root.win) - (vim.wo[self.root.win].winbar == \"\" and 0 or 1),\n      width = vim.api.nvim_win_get_width(self.root.win),\n      col = 0,\n      row = 0,\n    }, { left = 0, right = 0, top = 0, bottom = 0 }\n  end\n  local opts = vim.deepcopy(widget) --[[@as snacks.win.Config]]\n  -- adjust max width / height\n  opts.max_width = math.min(parent.width, opts.max_width or parent.width)\n  opts.max_height = math.min(parent.height, opts.max_height or parent.height)\n  local fake_win = setmetatable({ opts = opts }, Snacks.win)\n  local ret = fake_win:dim(parent)\n  return ret, fake_win:border_size()\nend\n\n---@param win snacks.layout.Win\n---@param parent snacks.win.Dim\n---@private\nfunction M:update_win(win, parent)\n  local w = self.wins[win.win]\n  w.enabled = true\n  assert(w, (\"win %s not part of layout\"):format(win.win))\n  -- add win opts from layout\n  w.opts = Snacks.config.merge(\n    vim.deepcopy(self.win_opts[win.win] or {}),\n    {\n      width = 0,\n      height = 0,\n      enter = false,\n    },\n    win,\n    {\n      relative = \"win\",\n      win = self.root.win,\n      backdrop = false,\n      resize = false,\n      zindex = (self.opts.layout.zindex or 50) + win.depth + 1,\n      w = { snacks_layout = true },\n    }\n  )\n  -- fix fullscreen for splits\n  if self.opts.fullscreen and self.split then\n    w.opts.relative = \"editor\"\n    w.opts.win = nil\n  end\n  -- adjust max width / height\n  w.opts.max_width = math.max(math.min(parent.width, w.opts.max_width or parent.width), 1)\n  w.opts.max_height = math.max(math.min(parent.height, w.opts.max_height or parent.height), 1)\n  -- resolve width / height relative to parent box\n  local dim = w:dim(parent)\n  w.opts.width, w.opts.height = dim.width, dim.height\n  local border = w:border_size()\n  w.opts.col, w.opts.row = parent.col, parent.row\n  dim.width = dim.width + border.left + border.right\n  dim.height = dim.height + border.top + border.bottom\n  -- dim.col = dim.col + border.left\n  -- dim.row = dim.row + border.top\n  return dim\nend\n\n--- Toggle fullscreen\nfunction M:maximize()\n  self.opts.fullscreen = not self.opts.fullscreen\n  self:update()\nend\n\n--- Close the layout\n---@param opts? {wins?: boolean}\nfunction M:close(opts)\n  if self.closed then\n    return\n  end\n  opts = opts or {}\n  self.closed = true\n  for w, win in pairs(self.wins) do\n    if opts.wins == false then\n      win.opts = self.win_opts[w]\n    else\n      win:destroy()\n    end\n  end\n  for _, win in pairs(self.box_wins) do\n    win:destroy()\n  end\n  vim.schedule(function()\n    if self.opts.on_close then\n      self.opts.on_close(self)\n    end\n    self.opts = nil\n    self.root = nil\n    self.wins = nil\n    self.box_wins = nil\n    self.win_opts = nil\n  end)\nend\n\n--- Check if layout is valid (visible)\nfunction M:valid()\n  return not self.closed and self.root:valid()\nend\n\n--- Check if the window has been used in the layout\n---@param w string\nfunction M:is_enabled(w)\n  return not self:is_hidden(w) and (self.wins[w].enabled or self.wins[w].layout == false)\nend\n\nfunction M:hide()\n  for _, win in ipairs(self:get_wins()) do\n    if win:valid() then\n      vim.api.nvim_win_set_config(win.win, { hide = true })\n      if win.backdrop and win.backdrop:valid() then\n        vim.api.nvim_win_set_config(win.backdrop.win, { hide = true })\n      end\n    end\n  end\nend\n\nfunction M:unhide()\n  for _, win in ipairs(self:get_wins()) do\n    if win:valid() then\n      vim.api.nvim_win_set_config(win.win, { hide = false })\n      if win.backdrop and win.backdrop:valid() then\n        vim.api.nvim_win_set_config(win.backdrop.win, { hide = false })\n      end\n    end\n  end\nend\n\n--- Show the layout\nfunction M:show()\n  if self:valid() then\n    return\n  end\n  self:update()\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/lazygit.lua",
    "content": "---@class snacks.lazygit\n---@overload fun(opts?: snacks.lazygit.Config): snacks.win\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.open(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Open LazyGit in a float, auto-configure colorscheme and integration with Neovim\",\n}\n\n---@alias snacks.lazygit.Color {fg?:string, bg?:string, bold?:boolean}\n\n---@class snacks.lazygit.Theme: table<number, snacks.lazygit.Color>\n---@field activeBorderColor snacks.lazygit.Color\n---@field cherryPickedCommitBgColor snacks.lazygit.Color\n---@field cherryPickedCommitFgColor snacks.lazygit.Color\n---@field defaultFgColor snacks.lazygit.Color\n---@field inactiveBorderColor snacks.lazygit.Color\n---@field optionsTextColor snacks.lazygit.Color\n---@field searchingActiveBorderColor snacks.lazygit.Color\n---@field selectedLineBgColor snacks.lazygit.Color\n---@field unstagedChangesColor snacks.lazygit.Color\n\n---@class snacks.lazygit.Config: snacks.terminal.Opts\n---@field args? string[]\n---@field theme? snacks.lazygit.Theme\nlocal defaults = {\n  -- automatically configure lazygit to use the current colorscheme\n  -- and integrate edit with the current neovim instance\n  configure = true,\n  -- extra configuration for lazygit that will be merged with the default\n  -- snacks does NOT have a full yaml parser, so if you need `\"test\"` to appear with the quotes\n  -- you need to double quote it: `\"\\\"test\\\"\"`\n  config = {\n    os = { editPreset = \"nvim-remote\" },\n    gui = {\n      -- set to an empty string \"\" to disable icons\n      nerdFontsVersion = \"3\",\n    },\n  },\n  theme_path = svim.fs.normalize(vim.fn.stdpath(\"cache\") .. \"/lazygit-theme.yml\"),\n  -- Theme for lazygit\n  -- stylua: ignore\n  theme = {\n    [241]                      = { fg = \"Special\" },\n    activeBorderColor          = { fg = \"MatchParen\", bold = true },\n    cherryPickedCommitBgColor  = { fg = \"Identifier\" },\n    cherryPickedCommitFgColor  = { fg = \"Function\" },\n    defaultFgColor             = { fg = \"Normal\" },\n    inactiveBorderColor        = { fg = \"FloatBorder\" },\n    optionsTextColor           = { fg = \"Function\" },\n    searchingActiveBorderColor = { fg = \"MatchParen\", bold = true },\n    selectedLineBgColor        = { bg = \"Visual\" }, -- set to `default` to have no background colour\n    unstagedChangesColor       = { fg = \"DiagnosticError\" },\n  },\n  win = {\n    style = \"lazygit\",\n  },\n}\n\nSnacks.config.style(\"lazygit\", {})\n\n-- re-create config file on startup\nlocal dirty = true\nlocal config_dir ---@type string?\n\n-- re-create theme file on ColorScheme change\nvim.api.nvim_create_autocmd(\"ColorScheme\", {\n  callback = function()\n    dirty = true\n  end,\n})\n\n---@param opts snacks.lazygit.Config\nlocal function env(opts)\n  if not config_dir then\n    local out = vim.fn.system({ \"lazygit\", \"-cd\" })\n    local lines = vim.split(out, \"\\n\", { plain = true })\n\n    if vim.v.shell_error == 0 and #lines > 1 then\n      config_dir = vim.split(lines[1], \"\\n\", { plain = true })[1]\n\n      ---@type string[]\n      local config_files = vim.tbl_filter(function(v)\n        return v:match(\"%S\")\n      end, vim.split(vim.env.LG_CONFIG_FILE or \"\", \",\", { plain = true }))\n\n      -- add the default config file if it exists and is not already there\n      if #config_files == 0 then\n        local default_config = svim.fs.normalize(config_dir .. \"/config.yml\")\n        if vim.loop.fs_stat(default_config) then\n          config_files[1] = default_config\n        end\n      end\n\n      -- add the theme file if it's not already there\n      if not vim.tbl_contains(config_files, opts.theme_path) then\n        table.insert(config_files, opts.theme_path)\n      end\n\n      vim.env.LG_CONFIG_FILE = table.concat(config_files, \",\")\n    else\n      local msg = {\n        \"Failed to get **lazygit** config directory.\",\n        \"Will not apply **lazygit** config.\",\n        \"\",\n        \"# Error:\",\n        vim.trim(out),\n      }\n      Snacks.notify.error(msg, { title = \"lazygit\" })\n    end\n  end\nend\n\n---@param v snacks.lazygit.Color\n---@return string[]\nlocal function get_color(v)\n  ---@type string[]\n  local color = {}\n  for _, c in ipairs({ \"fg\", \"bg\" }) do\n    if v[c] then\n      local name = v[c]\n      local hl = vim.api.nvim_get_hl(0, { name = name, link = false })\n      local hl_color ---@type number?\n      if c == \"fg\" then\n        hl_color = hl and hl.fg or hl.foreground\n      else\n        hl_color = hl and hl.bg or hl.background\n      end\n      if hl_color then\n        table.insert(color, string.format(\"#%06x\", hl_color))\n      end\n    end\n  end\n  if v.bold then\n    table.insert(color, \"bold\")\n  end\n  return color\nend\n\n---@param opts snacks.lazygit.Config\nlocal function update_config(opts)\n  ---@type table<string, string[]>\n  local theme = {}\n\n  for k, v in pairs(opts.theme) do\n    if type(k) == \"number\" then\n      local color = get_color(v)\n      -- LazyGit uses color 241 a lot, so also set it to a nice color\n      -- pcall, since some terminals don't like this\n      pcall(io.write, (\"\\27]4;%d;%s\\7\"):format(k, color[1]))\n    else\n      theme[k] = get_color(v)\n    end\n  end\n\n  local config = vim.tbl_deep_extend(\"force\", { gui = { theme = theme } }, opts.config or {})\n\n  local function yaml_val(val)\n    if type(val) == \"boolean\" then\n      return tostring(val)\n    end\n    return type(val) == \"string\" and not val:find(\"^\\\"'`\") and (\"%q\"):format(val) or val\n  end\n\n  local function to_yaml(tbl, indent)\n    indent = indent or 0\n    local lines = {}\n    for k, v in pairs(tbl) do\n      table.insert(lines, string.rep(\" \", indent) .. k .. (type(v) == \"table\" and \":\" or \": \" .. yaml_val(v)))\n      if type(v) == \"table\" then\n        if (vim.islist or vim.tbl_islist)(v) then\n          for _, item in ipairs(v) do\n            table.insert(lines, string.rep(\" \", indent + 2) .. \"- \" .. yaml_val(item))\n          end\n        else\n          vim.list_extend(lines, to_yaml(v, indent + 2))\n        end\n      end\n    end\n    return lines\n  end\n  vim.fn.writefile(to_yaml(config), opts.theme_path)\n  dirty = false\nend\n\n-- Opens lazygit, properly configured to use the current colorscheme\n-- and integrate with the current neovim instance\n---@param opts? snacks.lazygit.Config\nfunction M.open(opts)\n  ---@type snacks.lazygit.Config\n  opts = Snacks.config.get(\"lazygit\", defaults, opts)\n\n  local cmd = { \"lazygit\" }\n  vim.list_extend(cmd, opts.args or {})\n\n  if opts.configure then\n    if dirty then\n      update_config(opts)\n    end\n    env(opts)\n  end\n\n  return Snacks.terminal(cmd, opts)\nend\n\n-- Opens lazygit with the log view\n---@param opts? snacks.lazygit.Config\nfunction M.log(opts)\n  opts = opts or {}\n  opts.args = opts.args or { \"log\" }\n  return M.open(opts)\nend\n\n-- Opens lazygit with the log of the current file\n---@param opts? snacks.lazygit.Config|{}\nfunction M.log_file(opts)\n  local file = vim.trim(vim.api.nvim_buf_get_name(0))\n  opts = opts or {}\n  opts.args = vim.list_extend(opts.args or {}, { \"-f\", file })\n  opts.cwd = vim.fn.fnamemodify(file, \":h\")\n  return M.open(opts)\nend\n\n---@private\nfunction M.health()\n  local ok = vim.fn.executable(\"lazygit\") == 1\n  Snacks.health[ok and \"ok\" or \"error\"]((\"{lazygit} %sinstalled\"):format(ok and \"\" or \"not \"))\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/meta/docs.lua",
    "content": "local M = {}\n\nM.meta = {\n  desc = \"Doc-gen for Snacks\",\n  hide = true,\n}\n\nlocal query = vim.treesitter.query.parse(\n  \"lua\",\n  [[\n    ;; top-level locals\n    ((variable_declaration (\n      assignment_statement \n        (variable_list name: (identifier) @local_name)\n        (expression_list value: (_) @local_value)\n        (#match? @local_value \"(setmetatable|\\\\{)\")\n      )) @local\n      (#any-of? @local_name \"M\" \"defaults\" \"config\")\n      (#has-parent? @local chunk))\n\n    ;; top-level functions/methods\n    (function_declaration \n      name: (_) @fun_name (#match? @fun_name \"^M\")\n      parameters: (_) @fun_params\n    ) @fun\n\n    ;; styles\n    (function_call\n      name: (dot_index_expression) @_sf (#eq? @_sf \"Snacks.config.style\")\n      arguments: (arguments\n        (string content: (string_content) @style_name)\n        (table_constructor) @style_config)\n    ) @style\n\n    ;; examples\n    (assignment_statement\n      (variable_list\n        name: (dot_index_expression\n          field: (identifier) @example_name) \n          @_en (#lua-match? @_en \"^M%.examples%.%w+\"))\n      (expression_list\n        value: (table_constructor) @example_config)\n    ) @example\n\n    ;; props\n    (assignment_statement\n      (variable_list\n        name: (dot_index_expression\n          field: (identifier) @prop_name) \n          @_pn (#lua-match? @_pn \"^M%.\"))\n      (expression_list\n        value: (_) @prop_value)\n    ) @prop\n  ]]\n)\n\n---@class snacks.docs.Capture\n---@field name string\n---@field line number\n---@field node TSNode\n---@field text string\n---@field comment string\n---@field fields table<string, string>\n\n---@class snacks.docs.Parse\n---@field captures snacks.docs.Capture[]\n---@field comments string[]\n\n---@class snacks.docs.Method\n---@field mod string\n---@field name string\n---@field args string\n---@field comment? string\n---@field types? string\n---@field type \"method\"|\"function\"}[]\n\n---@class snacks.docs.Info\n---@field config? string\n---@field mod? string\n---@field modname? string\n---@field methods snacks.docs.Method[]\n---@field types string[]\n---@field setup? string\n---@field examples table<string, string>\n---@field styles {name:string, opts:string, comment?:string}[]\n---@field props table<string, string>\n\n---@param lines string[]\nfunction M.parse(lines)\n  local source = table.concat(lines, \"\\n\")\n  local parser = vim.treesitter.get_string_parser(source, \"lua\")\n  parser:parse()\n\n  local comments = {} ---@type string[]\n  for l, line in ipairs(lines) do\n    if line:find(\"^%-%-\") then\n      comments[l] = line\n      if comments[l - 1] then\n        comments[l] = comments[l - 1] .. \"\\n\" .. comments[l]\n        comments[l - 1] = nil\n      end\n    end\n  end\n\n  ---@type snacks.docs.Parse\n  local ret = { captures = {}, comments = {} }\n\n  local used_comments = {} ---@type table<number, boolean>\n  for id, node in query:iter_captures(parser:trees()[1]:root(), source) do\n    local name = query.captures[id]\n    if not name:find(\"_\") then\n      -- add fields\n      local fields = {}\n      for id2, node2 in query:iter_captures(node, source) do\n        local c = query.captures[id2]\n        if c:find(name .. \"_\") then\n          fields[c:gsub(\"^.*_\", \"\")] = vim.treesitter.get_node_text(node2, source)\n        end\n      end\n\n      -- add comments\n      local comment = \"\" ---@type string\n      if comments[node:start()] then\n        comment = comments[node:start()]\n        used_comments[node:start()] = true\n      end\n\n      if not comment:find(\"@deprecated\") then\n        table.insert(ret.captures, {\n          text = vim.treesitter.get_node_text(node, source),\n          name = name,\n          comment = comment,\n          line = node:start() + 1,\n          node = node,\n          fields = fields,\n        })\n      end\n    end\n  end\n  for l in pairs(used_comments) do\n    comments[l] = nil\n  end\n\n  -- remove comments that are followed by code\n  for l in pairs(comments) do\n    if lines[l + 1] and lines[l + 1]:find(\"^.+$\") then\n      comments[l] = nil\n    end\n  end\n  for l in ipairs(lines) do\n    if comments[l] then\n      table.insert(ret.comments, comments[l])\n    end\n  end\n\n  return ret\nend\n\n---@param lines string[]\n---@param opts {prefix: string, name:string}\nfunction M.extract(lines, opts)\n  local fqn = opts.prefix .. \".\" .. opts.name\n  local parse = M.parse(lines)\n  ---@type snacks.docs.Info\n  local ret = {\n    methods = {},\n    types = vim.tbl_filter(function(c)\n      return not c:find(\"@private\")\n    end, parse.comments),\n    styles = {},\n    examples = {},\n    props = {},\n  }\n\n  for _, c in ipairs(parse.captures) do\n    if\n      c.comment:find(\"@private\")\n      or c.comment:find(\"@protected\")\n      or c.comment:find(\"@package\")\n      or c.comment:find(\"@hide\")\n    then\n      -- skip private\n    elseif c.name == \"local\" then\n      if vim.tbl_contains({ \"defaults\", \"config\" }, c.fields.name) then\n        ret.config = vim.trim(c.comment .. \"\\n\" .. c.fields.value)\n      elseif c.fields.name == \"M\" then\n        ret.mod = c.comment\n      end\n    elseif c.name == \"prop\" then\n      local name = c.fields.name:sub(1)\n      local value = c.fields.value\n      ret.props[name] = c.comment == \"\" and value or c.comment .. \"\\n\" .. value\n    elseif c.name == \"fun\" then\n      local name = c.fields.name:sub(2)\n      local args = (c.fields.params or \"\"):sub(2, -2)\n      local type = name:sub(1, 1)\n      name = name:sub(2)\n      if not name:find(\"^_\") then\n        table.insert(ret.methods, {\n          mod = type == \":\" and opts.name or fqn,\n          name = name,\n          args = args,\n          comment = c.comment,\n          type = type,\n        })\n      end\n    elseif c.name == \"style\" then\n      table.insert(ret.styles, { name = c.fields.name, opts = c.fields.config, comment = c.comment })\n    elseif c.name == \"example\" then\n      ret.examples[c.fields.name] = c.comment .. \"\\n\" .. c.fields.config\n    end\n  end\n\n  if ret.mod then\n    local mod_lines = vim.split(ret.mod, \"\\n\")\n    mod_lines = vim.tbl_filter(function(line)\n      local overload = line:match(\"^%-%-%-%s*@overload (.*)(%s*)$\") --[[@as string?]]\n      if overload then\n        table.insert(ret.methods, {\n          mod = fqn,\n          name = \"\",\n          args = \"\",\n          type = \"\",\n          comment = \"---@type \" .. overload,\n        })\n        return false\n      elseif line:find(\"^%s*$\") then\n        return false\n      end\n      return true\n    end, mod_lines)\n    ret.mod = table.concat(mod_lines, \"\\n\")\n  end\n\n  return ret\nend\n\n---@param tag string\n---@param readme string\n---@param content string\nfunction M.replace(tag, readme, content)\n  content = vim.trim(content)\n  local pattern = \"(<%!%-%- \" .. tag .. \":start %-%->).*(<%!%-%- \" .. tag .. \":end %-%->)\"\n  if not readme:find(pattern) then\n    error(\"tag \" .. tag .. \" not found\")\n  end\n  return readme:gsub(pattern, \"%1\\n\\n\" .. content .. \"\\n\\n%2\")\nend\n\n---@param str string\n---@param opts? {extract_comment: boolean} -- default true\nfunction M.md(str, opts)\n  str = str or \"\"\n  str = str:gsub(\"\\r\", \"\")\n  opts = opts or {}\n  if opts.extract_comment == nil then\n    opts.extract_comment = true\n  end\n  str = str:gsub(\"\\n%s*%-%-%s*stylua: ignore\\n\", \"\\n\")\n  str = str:gsub(\"\\n%s*debug = false,\\n\", \"\\n\")\n  str = str:gsub(\"\\n%s*debug = true,\\n\", \"\\n\")\n  local comments = {} ---@type string[]\n  local lines = vim.split(str, \"\\n\", { plain = true })\n\n  if opts.extract_comment then\n    while lines[1] and lines[1]:find(\"^%-%-\") and not lines[1]:find(\"^%-%-%-%s*@\") do\n      local line = table.remove(lines, 1):gsub(\"^[%-]*%s*\", \"\")\n      table.insert(comments, line)\n    end\n  end\n\n  local ret = {} ---@type string[]\n  if #comments > 0 then\n    table.insert(ret, vim.trim(table.concat(comments, \"\\n\")))\n    table.insert(ret, \"\")\n  end\n  if #lines > 0 then\n    table.insert(ret, \"```lua\")\n    table.insert(ret, vim.trim(table.concat(lines, \"\\n\")))\n    table.insert(ret, \"```\")\n  end\n\n  return vim.trim(table.concat(ret, \"\\n\")) .. \"\\n\"\nend\n\nfunction M.examples(name)\n  local fname = (\"docs/examples/%s.lua\"):format(name)\n  if not vim.uv.fs_stat(fname) then\n    return {}\n  end\n  local lines = vim.fn.readfile(fname)\n  local info = M.extract(lines, { prefix = \"Snacks.examples\", name = name })\n  return info.examples\nend\n\n---@param name string\n---@param info snacks.docs.Info\n---@param opts? {setup?:boolean, config?:boolean, styles?:boolean, types?:boolean, prefix?:string, examples?:boolean}\nfunction M.render(name, info, opts)\n  opts = opts or {}\n  local lines = {} ---@type string[]\n  local function add(line)\n    table.insert(lines, line)\n  end\n\n  local prefix = (\"Snacks.%s\"):format(name)\n  if name == \"init\" then\n    prefix = \"Snacks\"\n  end\n  if info.modname then\n    prefix = \"local M\"\n  end\n\n  if name ~= \"init\" and (info.config or info.setup) and opts.setup ~= false then\n    add(\"## 📦 Setup\\n\")\n    add(([[\n```lua\n-- lazy.nvim\n{\n  \"folke/snacks.nvim\",\n  ---@type snacks.Config\n  opts = {\n    %s = {\n      -- your %s configuration comes here\n      -- or leave it empty to use the default settings\n      -- refer to the configuration section below\n    }\n  }\n}\n```\n]]):format(info.setup or name, name))\n  end\n\n  if info.config and opts.config ~= false then\n    add(\"## ⚙️ Config\\n\")\n    add(M.md(info.config))\n  end\n\n  if opts.examples ~= false then\n    local examples = M.examples(name)\n    local names = vim.tbl_keys(examples)\n    table.sort(names)\n    if not vim.tbl_isempty(examples) then\n      add(\"## 🚀 Examples\\n\")\n      for _, n in ipairs(names) do\n        local example = examples[n]\n        add((\"### `%s`\\n\"):format(n))\n        add(M.md(example))\n      end\n    end\n  end\n\n  if #info.styles > 0 and opts.styles ~= false then\n    table.sort(info.styles, function(a, b)\n      return a.name < b.name\n    end)\n    add(\"## 🎨 Styles\\n\")\n\n    if name == \"styles\" then\n      add([[These are the default styles that Snacks provides.\nYou can customize them by adding your own styles to `opts.styles`.\n\n]])\n    else\n      add([[Check the [styles](https://github.com/folke/snacks.nvim/blob/main/docs/styles.md)\ndocs for more information on how to customize these styles\n]])\n    end\n\n    for _, style in pairs(info.styles) do\n      add((\"### `%s`\\n\"):format(style.name))\n      if style.comment and style.comment ~= \"\" then\n        add(M.md(style.comment))\n      end\n      add(M.md(style.opts))\n    end\n  end\n\n  if #info.types > 0 and opts.types ~= false then\n    add(\"## 📚 Types\\n\")\n    for _, t in ipairs(info.types) do\n      add(M.md(t))\n    end\n  end\n\n  local mod_lines = info.mod and not info.mod:find(\"^%s*$\") and vim.split(info.mod, \"\\n\") or {}\n  local hide = #mod_lines == 0 or (#mod_lines == 1 and mod_lines[1]:find(\"@class\"))\n\n  if not hide or #info.methods > 0 then\n    local title = info.modname and (\"`%s`\"):format(info.modname) or \"Module\"\n    add((\"## 📦 %s\\n\"):format(title))\n  end\n\n  if info.mod and not hide then\n    table.insert(mod_lines, prefix .. \" = {}\")\n    add(M.md(table.concat(mod_lines, \"\\n\")))\n  end\n\n  table.sort(info.methods, function(a, b)\n    if a.mod ~= b.mod then\n      return a.mod < b.mod\n    end\n    if a.type == b.type then\n      return a.name < b.name\n    end\n    return a.type < b.type\n  end)\n\n  local last ---@type string?\n  for _, method in ipairs(info.methods) do\n    local title = (\"### `%s%s%s()`\\n\"):format(method.mod, method.type, method.name)\n    if title ~= last then\n      last = title\n      add(title)\n    end\n    local code = (\"%s\\n%s%s%s(%s)\"):format(method.comment or \"\", method.mod, method.type, method.name, method.args)\n    add(M.md(code))\n  end\n\n  lines = vim.split(vim.trim(table.concat(lines, \"\\n\")), \"\\n\")\n  return lines\nend\n\nfunction M.write(name, lines)\n  local path = (\"docs/%s.md\"):format(name)\n  local ok, text = pcall(vim.fn.readfile, path)\n\n  local docgen = \"<!-- docgen -->\"\n  local top = {} ---@type string[]\n\n  if not ok then\n    table.insert(top, \"# 🍿 \" .. name)\n    table.insert(top, \"\")\n  else\n    for _, line in ipairs(text) do\n      if line == docgen then\n        break\n      end\n      table.insert(top, line)\n    end\n  end\n  table.insert(top, docgen)\n  table.insert(top, \"\")\n  vim.list_extend(top, lines)\n\n  vim.fn.writefile(vim.split(table.concat(top, \"\\n\"), \"\\n\"), path)\nend\n\n---@param ret string[]\nfunction M.picker(ret)\n  local lines = vim.fn.readfile(\"lua/snacks/picker/config/sources.lua\")\n  local info = M.extract(lines, { prefix = \"Snacks.picker\", name = \"sources\" })\n  local sources = vim.tbl_keys(info.props)\n  table.sort(sources)\n  local source_types = {} ---@type table<string, string>\n  table.insert(ret, \"## 🔍 Sources\\n\")\n  for _, source in ipairs(sources) do\n    local opts = info.props[source]\n    local opts_lines = vim.split(opts, \"\\n\")\n    for _, l in ipairs(opts_lines) do\n      local t = l:match(\"^---@type (.*)$\")\n      t = t or l:match(\"^---@class (.*)$\")\n      if t then\n        t = vim.trim(t:gsub(\":.*\", \"\"))\n        source_types[source] = t\n        break\n      end\n    end\n    table.insert(ret, (\"### `%s`\"):format(source))\n    table.insert(ret, \"\")\n    table.insert(ret, (\"```vim\\n:lua Snacks.picker.%s(opts?)\\n```\\n\"):format(source))\n    table.insert(ret, M.md(opts))\n  end\n  M.picker_types(source_types)\n  lines = vim.fn.readfile(\"lua/snacks/picker/config/layouts.lua\")\n  info = M.extract(lines, { prefix = \"Snacks.picker\", name = \"layouts\" })\n  sources = vim.tbl_keys(info.props)\n  table.sort(sources)\n  table.insert(ret, \"## 🖼️ Layouts\\n\")\n  for _, source in ipairs(sources) do\n    local opts = info.props[source]\n    table.insert(ret, (\"### `%s`\"):format(source))\n    table.insert(ret, \"\")\n    table.insert(ret, M.md(opts))\n  end\nend\n\nfunction M._build()\n  local plugins = Snacks.meta.get()\n  ---@class snacks.docs.Types\n  local types = {\n    fields = {}, ---@type string[]\n    config = {}, ---@type string[]\n  }\n\n  ---@type snacks.docs.Info\n  local styles = {\n    methods = {},\n    types = {},\n    examples = {},\n    styles = {},\n    setup = \"---@type table<string, snacks.win.Config>\\n    styles\",\n    props = {},\n  }\n\n  for _, plugin in pairs(plugins) do\n    if plugin.meta.docs then\n      local name = plugin.name\n      print(\"[gen] \" .. name .. \".md\")\n      local lines = vim.fn.readfile(plugin.file)\n      local info = M.extract(lines, { prefix = \"Snacks\", name = name })\n\n      local children = {} ---@type snacks.docs.Info[]\n      local to_merge = {} ---@type {child:string, name:string}[]\n\n      for c, child in pairs(plugin.meta.merge or {}) do\n        local child_name = type(c) == \"number\" and child or c --[[@as string]]\n        table.insert(to_merge, { child = child, name = child_name })\n      end\n      table.sort(to_merge, function(a, b)\n        return a.child < b.child\n      end)\n\n      for _, item in ipairs(to_merge) do\n        local child = item.child\n        local child_name = item.name\n        local child_file = (\"%s/%s/%s\"):format(Snacks.meta.root, name, child:gsub(\"%.\", \"/\"))\n        for _, f in ipairs({ \".lua\", \"/init.lua\" }) do\n          if vim.uv.fs_stat(child_file .. f) then\n            child_file = child_file .. f\n            break\n          end\n        end\n        assert(vim.uv.fs_stat(child_file), (\"file not found: %s\"):format(child_file))\n        local child_lines = vim.fn.readfile(child_file)\n        local child_info = M.extract(child_lines, { prefix = \"Snacks.\" .. name, name = child_name })\n        child_info.modname = \"snacks.\" .. name .. \".\" .. child\n        if child_info.config then\n          assert(not info.config, \"config already exists\")\n          info.config = child_info.config\n        end\n        vim.list_extend(info.types, child_info.types)\n        table.insert(children, child_info)\n      end\n\n      vim.list_extend(styles.styles, info.styles)\n      info.config = name ~= \"init\" and info.config or nil\n      plugin.meta.config = info.config ~= nil\n\n      local rendered = {} ---@type string[]\n      vim.list_extend(rendered, M.render(name, info))\n      if name == \"picker\" then\n        M.picker(rendered)\n      end\n\n      for _, child in ipairs(children) do\n        table.insert(rendered, \"\")\n        vim.list_extend(\n          rendered,\n          M.render(name, child, {\n            setup = false,\n            config = false,\n            styles = false,\n            types = false,\n            examples = false,\n          })\n        )\n      end\n\n      M.write(name, rendered)\n\n      if plugin.meta.types then\n        table.insert(types.fields, (\"---@field %s snacks.%s\"):format(plugin.name, plugin.name))\n      end\n      if plugin.meta.config then\n        table.insert(types.config, (\"---@field %s? snacks.%s.Config\"):format(plugin.name, plugin.name))\n      end\n    end\n  end\n  M.write(\"styles\", M.render(\"styles\", styles))\n\n  M.readme(plugins, types)\n  M.types(types)\n\n  vim.cmd.checktime()\nend\n\n---@param types snacks.docs.Types\nfunction M.types(types)\n  local lines = {} ---@type string[]\n  lines[#lines + 1] = \"---@meta _\"\n  lines[#lines + 1] = \"\"\n  lines[#lines + 1] = \"---@class snacks.plugins\"\n  vim.list_extend(lines, types.fields)\n  lines[#lines + 1] = \"\"\n  lines[#lines + 1] = \"---@class snacks.plugins.Config\"\n  vim.list_extend(\n    lines,\n    vim.tbl_map(function(field)\n      -- make all fields optional\n      return field .. \"|{}\"\n    end, types.config)\n  )\n\n  vim.fn.writefile(lines, \"lua/snacks/meta/types.lua\")\nend\n\n---@param types table<string,string>\nfunction M.picker_types(types)\n  local opts = Snacks.picker.config.get() --[[@as table<string,unknown>]]\n  local sources = vim.tbl_keys(opts.sources) ---@type string[]\n  table.sort(sources)\n  local lines = {} ---@type string[]\n  lines[#lines + 1] = \"---@meta _\"\n  lines[#lines + 1] = \"\"\n  lines[#lines + 1] = \"---@class snacks.picker\"\n  for _, source in ipairs(sources) do\n    if source ~= \"select\" then\n      local t = types[source] or \"snacks.picker.Config\"\n      t = t:gsub(\"|.*\", \"\") .. \"|{}\"\n      if source == \"resume\" then\n        lines[#lines + 1] = (\"---@field %s fun(): snacks.Picker\"):format(source)\n      else\n        lines[#lines + 1] = (\"---@field %s fun(opts?: %s): snacks.Picker\"):format(source, t)\n      end\n    end\n  end\n  vim.fn.writefile(lines, \"lua/snacks/picker/types.lua\")\nend\n\n---@param plugins snacks.meta.Plugin[]\n---@param types snacks.docs.Types\nfunction M.readme(plugins, types)\n  local path = \"lua/snacks/init.lua\"\n  local lines = vim.fn.readfile(path) --[[ @as string[] ]]\n  local info = M.extract(lines, { prefix = \"Snacks\", name = \"init\" })\n  local readme = table.concat(vim.fn.readfile(\"README.md\"), \"\\n\")\n  local example = table.concat(vim.fn.readfile(\"docs/examples/init.lua\"), \"\\n\")\n  local e = M.examples(\"picker\").general or \"\"\n  local l = vim.split(e, \"\\n\")\n  table.remove(l)\n  table.remove(l)\n  local start = false\n  l = vim.tbl_filter(function(line)\n    if line:find(\"^%s*keys =\") then\n      start = true\n      return false\n    end\n    return start\n  end, l)\n  l[1] = vim.trim(l[1])\n  e = table.concat(l, \"\\n\")\n  example = example:gsub(\"%-%- EXTRA_KEYS\", e)\n\n  -- config type\n  lines = {}\n  lines[1] = \"---@class snacks.Config\"\n  vim.list_extend(lines, types.config)\n  local config_lines = vim.split(info.config or \"\", \"\\n\")\n  table.remove(config_lines, 1)\n  vim.list_extend(lines, config_lines)\n  info.config = table.concat(lines, \"\\n\")\n\n  -- snacks type\n  lines = {}\n  lines[#lines + 1] = \"---@class Snacks\"\n  vim.list_extend(lines, types.fields)\n  info.mod = table.concat(lines, \"\\n\")\n\n  -- toc\n  lines = {}\n  lines[#lines + 1] = \"| Snack | Description | Setup |\"\n  lines[#lines + 1] = \"| ----- | ----------- | :---: |\"\n  for _, plugin in ipairs(plugins) do\n    if plugin.meta.readme then\n      lines[#lines + 1] = (\"| %s | %s | %s |\"):format(\n        (\"[%s](https://github.com/folke/snacks.nvim/blob/main/docs/%s.md)\"):format(plugin.name, plugin.name),\n        plugin.meta.desc,\n        plugin.meta.needs_setup and \"‼️\" or \"\"\n      )\n    end\n  end\n\n  M.write(\"init\", M.render(\"init\", info))\n  example = example:gsub(\".*\\nreturn {\", \"{\", 1)\n  readme = M.replace(\"config\", readme, M.md(info.config))\n  readme = M.replace(\"example\", readme, M.md(example))\n  readme = M.replace(\"toc\", readme, table.concat(lines, \"\\n\"))\n  vim.fn.writefile(vim.split(readme, \"\\n\"), \"README.md\")\nend\n\nfunction M.fix_titles()\n  for file, t in vim.fs.dir(\"doc\", { depth = 1 }) do\n    if t == \"file\" and file:find(\"%.txt$\") then\n      local lines = vim.fn.readfile(\"doc/\" .. file) --[[@as string[] ]]\n      lines[1] = lines[1]:gsub(\"%.txt\", \"\"):gsub(\"%.nvim\", \"\")\n      for i, line in ipairs(lines) do\n        -- Example: SNACKS.GIT.BLAME_LINE()            *snacks-git-module-snacks.git.blame_line()*\n        local func = line:gsub(\"^SNACKS.*module%-snacks(.+%(%))%*$\", \"Snacks%1\")\n        if func ~= line then\n          local left = (\"`%s`\"):format(func)\n          local right = (\"*%s*\"):format(func)\n          line = left .. string.rep(\" \", #line - #left - #right) .. right\n          lines[i] = line\n        end\n      end\n      vim.fn.writefile(lines, \"doc/\" .. file)\n    end\n  end\n  vim.cmd.helptags(\"doc\")\nend\n\nfunction M.build()\n  local ok, err = pcall(M._build)\n  if not ok then\n    vim.api.nvim_err_writeln(err)\n    os.exit(1)\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/meta/init.lua",
    "content": "---@class snacks.meta\nlocal M = {}\n\nM.meta = {\n  desc = \"Meta functions for Snacks\",\n  readme = false,\n}\n\n---@class snacks.meta.Meta\n---@field desc string\n---@field needs_setup? boolean\n---@field hide? boolean\n---@field readme? boolean\n---@field docs? boolean\n---@field health? boolean\n---@field types? boolean\n---@field config? boolean\n---@field merge? { [string|number]: string }\n\n---@class snacks.meta.Plugin\n---@field name string\n---@field file string\n---@field meta snacks.meta.Meta\n---@field health? fun()\n\nM.root = vim.fn.fnamemodify(debug.getinfo(1, \"S\").source:sub(2), \":h:h\")\n\nfunction M.file(name)\n  return svim.fs.normalize((\"%s/%s\"):format(M.root, name))\nend\n\n--- Get the metadata for all snacks plugins\n---@return snacks.meta.Plugin[]\nfunction M.get()\n  local ret = {} ---@type snacks.meta.Plugin[]\n  for file, t in vim.fs.dir(M.root, { depth = 1 }) do\n    if file:sub(1, 1) ~= \".\" then\n      local name = vim.fn.fnamemodify(file, \":t:r\")\n      file = t == \"directory\" and (\"%s/init.lua\"):format(file) or file\n      file = M.root .. \"/\" .. file\n      local mod = name == \"init\" and setmetatable({ meta = { desc = \"Snacks\", hide = true } }, { __index = Snacks })\n        or Snacks[name] --[[@as snacks.meta.Plugin]]\n      assert(type(mod) == \"table\", (\"`Snacks.%s` not found\"):format(name))\n      assert(type(mod.meta) == \"table\", (\"`Snacks.%s.meta` not found\"):format(name))\n      assert(type(mod.meta.desc) == \"string\", (\"`Snacks.%s.meta.desc` not found\"):format(name))\n\n      for _, prop in ipairs({ \"readme\", \"docs\", \"health\", \"types\" }) do\n        if mod.meta[prop] == nil then\n          mod.meta[prop] = not mod.meta.hide\n        end\n      end\n\n      ret[#ret + 1] = setmetatable({\n        name = name,\n        file = file,\n      }, {\n        __index = mod,\n        __tostring = function(self)\n          return \"snacks.\" .. self.name\n        end,\n      })\n    end\n  end\n  table.sort(ret, function(a, b)\n    return a.name < b.name\n  end)\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/meta/types.lua",
    "content": "---@meta _\n\n---@class snacks.plugins\n---@field animate snacks.animate\n---@field bigfile snacks.bigfile\n---@field bufdelete snacks.bufdelete\n---@field dashboard snacks.dashboard\n---@field debug snacks.debug\n---@field dim snacks.dim\n---@field explorer snacks.explorer\n---@field gh snacks.gh\n---@field git snacks.git\n---@field gitbrowse snacks.gitbrowse\n---@field health snacks.health\n---@field image snacks.image\n---@field indent snacks.indent\n---@field input snacks.input\n---@field keymap snacks.keymap\n---@field layout snacks.layout\n---@field lazygit snacks.lazygit\n---@field meta snacks.meta\n---@field notifier snacks.notifier\n---@field notify snacks.notify\n---@field picker snacks.picker\n---@field profiler snacks.profiler\n---@field quickfile snacks.quickfile\n---@field rename snacks.rename\n---@field scope snacks.scope\n---@field scratch snacks.scratch\n---@field scroll snacks.scroll\n---@field statuscolumn snacks.statuscolumn\n---@field terminal snacks.terminal\n---@field toggle snacks.toggle\n---@field util snacks.util\n---@field win snacks.win\n---@field words snacks.words\n---@field zen snacks.zen\n\n---@class snacks.plugins.Config\n---@field animate? snacks.animate.Config|{}\n---@field bigfile? snacks.bigfile.Config|{}\n---@field dashboard? snacks.dashboard.Config|{}\n---@field dim? snacks.dim.Config|{}\n---@field explorer? snacks.explorer.Config|{}\n---@field gh? snacks.gh.Config|{}\n---@field gitbrowse? snacks.gitbrowse.Config|{}\n---@field image? snacks.image.Config|{}\n---@field indent? snacks.indent.Config|{}\n---@field input? snacks.input.Config|{}\n---@field layout? snacks.layout.Config|{}\n---@field lazygit? snacks.lazygit.Config|{}\n---@field notifier? snacks.notifier.Config|{}\n---@field picker? snacks.picker.Config|{}\n---@field profiler? snacks.profiler.Config|{}\n---@field quickfile? snacks.quickfile.Config|{}\n---@field scope? snacks.scope.Config|{}\n---@field scratch? snacks.scratch.Config|{}\n---@field scroll? snacks.scroll.Config|{}\n---@field statuscolumn? snacks.statuscolumn.Config|{}\n---@field terminal? snacks.terminal.Config|{}\n---@field toggle? snacks.toggle.Config|{}\n---@field win? snacks.win.Config|{}\n---@field words? snacks.words.Config|{}\n---@field zen? snacks.zen.Config|{}\n"
  },
  {
    "path": "lua/snacks/notifier.lua",
    "content": "---@class snacks.notifier\n---@overload fun(msg: string, level?: snacks.notifier.level|number, opts?: snacks.notifier.Notif.opts): number|string\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.notify(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Pretty `vim.notify`\",\n  needs_setup = true,\n}\n\nlocal uv = vim.uv or vim.loop\n\n--- Render styles:\n--- * compact: use border for icon and title\n--- * minimal: no border, only icon and message\n--- * fancy: similar to the default nvim-notify style\n---@alias snacks.notifier.style snacks.notifier.render|\"compact\"|\"fancy\"|\"minimal\"\n\n--- ### Notifications\n---\n--- Notification options\n---@class snacks.notifier.Notif.opts\n---@field id? number|string\n---@field msg? string\n---@field level? number|snacks.notifier.level\n---@field title? string\n---@field icon? string\n---@field timeout? number|boolean timeout in ms. Set to 0|false to keep until manually closed\n---@field ft? string\n---@field keep? fun(notif: snacks.notifier.Notif): boolean\n---@field style? snacks.notifier.style\n---@field opts? fun(notif: snacks.notifier.Notif) -- dynamic opts\n---@field hl? snacks.notifier.hl -- highlight overrides\n---@field history? boolean\n\n--- Notification object\n---@class snacks.notifier.Notif: snacks.notifier.Notif.opts\n---@field id number|string\n---@field msg string\n---@field win? snacks.win\n---@field icon string\n---@field level snacks.notifier.level\n---@field timeout number\n---@field dirty? boolean\n---@field added number timestamp with nano precision\n---@field updated number timestamp with nano precision\n---@field shown? number timestamp with nano precision\n---@field hidden? number timestamp with nano precision\n---@field layout? { top?: number, width: number, height: number }\n\n--- ### Rendering\n---@alias snacks.notifier.render fun(buf: number, notif: snacks.notifier.Notif, ctx: snacks.notifier.ctx)\n\n---@class snacks.notifier.hl\n---@field title string\n---@field icon string\n---@field border string\n---@field footer string\n---@field msg string\n\n---@class snacks.notifier.ctx\n---@field opts snacks.win.Config\n---@field notifier snacks.notifier.Class\n---@field hl snacks.notifier.hl\n---@field ns number\n\n--- ### History\n---@class snacks.notifier.history\n---@field filter? vim.log.levels|snacks.notifier.level|fun(notif: snacks.notifier.Notif): boolean\n---@field sort? string[] # sort fields, default: {\"added\"}\n---@field reverse? boolean\n\n---@type snacks.notifier.history\nlocal history_opts = {\n  sort = { \"added\" },\n}\n\nSnacks.config.style(\"notification\", {\n  border = true,\n  zindex = 100,\n  ft = \"markdown\",\n  wo = {\n    winblend = 5,\n    wrap = false,\n    conceallevel = 2,\n    colorcolumn = \"\",\n  },\n  bo = { filetype = \"snacks_notif\" },\n})\n\nSnacks.config.style(\"notification_history\", {\n  border = true,\n  zindex = 100,\n  width = 0.6,\n  height = 0.6,\n  minimal = false,\n  title = \" Notification History \",\n  title_pos = \"center\",\n  ft = \"markdown\",\n  bo = { filetype = \"snacks_notif_history\", modifiable = false },\n  wo = { winhighlight = \"Normal:SnacksNotifierHistory\" },\n  keys = { q = \"close\" },\n})\n\n---@class snacks.notifier.Config\n---@field enabled? boolean\n---@field keep? fun(notif: snacks.notifier.Notif): boolean # global keep function\n---@field filter? fun(notif: snacks.notifier.Notif): boolean # filter our unwanted notifications (return false to hide)\nlocal defaults = {\n  timeout = 3000, -- default timeout in ms\n  width = { min = 40, max = 0.4 },\n  height = { min = 1, max = 0.6 },\n  -- editor margin to keep free. tabline and statusline are taken into account automatically\n  margin = { top = 0, right = 1, bottom = 0 },\n  padding = true, -- add 1 cell of left/right padding to the notification window\n  gap = 0, -- gap between notifications\n  sort = { \"level\", \"added\" }, -- sort by level and time\n  -- minimum log level to display. TRACE is the lowest\n  -- all notifications are stored in history\n  level = vim.log.levels.TRACE,\n  icons = {\n    error = \" \",\n    warn = \" \",\n    info = \" \",\n    debug = \" \",\n    trace = \" \",\n  },\n  keep = function(notif)\n    return vim.fn.getcmdpos() > 0\n  end,\n  ---@type snacks.notifier.style\n  style = \"compact\",\n  top_down = true, -- place notifications from top to bottom\n  date_format = \"%R\", -- time format for notifications\n  -- format for footer when more lines are available\n  -- `%d` is replaced with the number of lines.\n  -- only works for styles with a border\n  ---@type string|boolean\n  more_format = \" ↓ %d lines \",\n  refresh = 50, -- refresh at most every 50ms\n}\n\n---@class snacks.notifier.Class\n---@field queue table<string|number, snacks.notifier.Notif>\n---@field history table<string|number, snacks.notifier.Notif>\n---@field sorted? snacks.notifier.Notif[]\n---@field opts snacks.notifier.Config\nlocal N = {}\n\nN.ns = vim.api.nvim_create_namespace(\"snacks.notifier\")\n\n---@param str string\nlocal function cap(str)\n  return str:sub(1, 1):upper() .. str:sub(2):lower()\nend\n\n---@param name string\n---@param level? snacks.notifier.level\nlocal function hl(name, level)\n  return \"SnacksNotifier\" .. name .. (level and cap(level) or \"\")\nend\n\n---@type table<string, snacks.notifier.render>\nN.styles = {\n  -- style using border title\n  compact = function(buf, notif, ctx)\n    local title = vim.trim(notif.icon .. \" \" .. (notif.title or \"\"))\n    if title ~= \"\" then\n      ctx.opts.title = { { \" \" .. title .. \" \", ctx.hl.title } }\n      ctx.opts.title_pos = \"center\"\n    end\n    vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(notif.msg, \"\\n\"))\n  end,\n  minimal = function(buf, notif, ctx)\n    ctx.opts.border = \"none\"\n    local whl = ctx.opts.wo.winhighlight\n    ctx.opts.wo.winhighlight = whl:gsub(ctx.hl.msg, \"SnacksNotifierMinimal\")\n    vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(notif.msg, \"\\n\"))\n    vim.api.nvim_buf_set_extmark(buf, ctx.ns, 0, 0, {\n      virt_text = { { notif.icon, ctx.hl.icon } },\n      virt_text_pos = \"right_align\",\n    })\n  end,\n  history = function(buf, notif, ctx)\n    local lines = vim.split(notif.msg, \"\\n\", { plain = true })\n    local prefix = {\n      { os.date(ctx.notifier.opts.date_format, notif.added), hl(\"HistoryDateTime\") },\n      { notif.icon, ctx.hl.icon },\n      { notif.level:upper(), ctx.hl.title },\n      { notif.title, hl(\"HistoryTitle\") },\n    }\n    prefix = vim.tbl_filter(function(v)\n      return (v[1] or \"\") ~= \"\"\n    end, prefix)\n    local prefix_width = 0\n    for i = 1, #prefix do\n      prefix_width = prefix_width + vim.fn.strdisplaywidth(prefix[i * 2 - 1][1]) + 1\n      table.insert(prefix, i * 2, { \" \" })\n    end\n    local top = vim.api.nvim_buf_line_count(buf)\n    local empty = top == 1 and #vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] == 0\n    top = empty and 0 or top\n    lines[1] = string.rep(\" \", prefix_width) .. (lines[1] or \"\")\n    vim.api.nvim_buf_set_lines(buf, top, -1, false, lines)\n    vim.api.nvim_buf_set_extmark(buf, ctx.ns, top, 0, {\n      virt_text = prefix,\n      virt_text_pos = \"overlay\",\n      priority = 10,\n    })\n  end,\n  -- similar to the default nvim-notify style\n  fancy = function(buf, notif, ctx)\n    vim.api.nvim_buf_set_lines(buf, 0, 1, false, { \"\", \"\" })\n    vim.api.nvim_buf_set_lines(buf, 2, -1, false, vim.split(notif.msg, \"\\n\"))\n    vim.api.nvim_buf_set_extmark(buf, ctx.ns, 0, 0, {\n      virt_text = { { \" \" }, { notif.icon, ctx.hl.icon }, { \" \" }, { notif.title or \"\", ctx.hl.title } },\n      virt_text_win_col = 0,\n      priority = 10,\n    })\n    vim.api.nvim_buf_set_extmark(buf, ctx.ns, 0, 0, {\n      virt_text = { { \" \" }, { os.date(ctx.notifier.opts.date_format, notif.added), ctx.hl.title }, { \" \" } },\n      virt_text_pos = \"right_align\",\n      priority = 10,\n    })\n    vim.api.nvim_buf_set_extmark(buf, ctx.ns, 1, 0, {\n      virt_text = { { string.rep(\"━\", vim.o.columns - 2), ctx.hl.border } },\n      virt_text_win_col = 0,\n      priority = 10,\n    })\n  end,\n}\n\n---@alias snacks.notifier.level \"trace\"|\"debug\"|\"info\"|\"warn\"|\"error\"\n\n---@type table<number, snacks.notifier.level>\nN.levels = {\n  [vim.log.levels.TRACE] = \"trace\",\n  [vim.log.levels.DEBUG] = \"debug\",\n  [vim.log.levels.INFO] = \"info\",\n  [vim.log.levels.WARN] = \"warn\",\n  [vim.log.levels.ERROR] = \"error\",\n}\nN.level_names = vim.tbl_values(N.levels) ---@type snacks.notifier.level[]\nlocal MAX_SKIPPED = 10\n\n---@param level number|string\n---@return snacks.notifier.level\nlocal function normlevel(level)\n  return type(level) == \"string\" and (vim.tbl_contains(N.level_names, level:lower()) and level:lower() or \"info\")\n    or N.levels[level]\n    or \"info\"\nend\n\n---@param level number|string\n---@return integer\nlocal function numlevel(level)\n  return type(level) == \"number\" and level or vim.log.levels[normlevel(level):upper()] or 0\nend\n\nlocal function ts()\n  if uv.clock_gettime then\n    local ret = assert(uv.clock_gettime(\"realtime\"))\n    return ret.sec + ret.nsec / 1e9\n  end\n  local sec, usec = uv.gettimeofday()\n  return sec + usec / 1e6\nend\n\nlocal _id = 0\n\nlocal function next_id()\n  _id = _id + 1\n  return _id\nend\n\n---@param opts? snacks.notifier.Config\n---@return snacks.notifier.Class\nfunction N.new(opts)\n  local self = setmetatable({}, { __index = N })\n  self.opts = Snacks.config.get(\"notifier\", defaults, opts)\n  self.queue = {}\n  self.history = {}\n  self:init()\n  self:start()\n  return self\nend\n\nfunction N:init()\n  local links = {\n    [hl(\"History\")] = \"Normal\",\n    [hl(\"HistoryTitle\")] = \"Title\",\n    [hl(\"HistoryDateTime\")] = \"Special\",\n    SnacksNotifierMinimal = \"NormalFloat\",\n  }\n  for _, level in ipairs(N.level_names) do\n    local Level = cap(level)\n    local link = vim.tbl_contains({ \"Trace\", \"Debug\" }, Level) and \"NonText\" or nil\n    links[hl(\"\", level)] = \"Normal\"\n    links[hl(\"Icon\", level)] = link or (\"DiagnosticSign\" .. Level)\n    links[hl(\"Border\", level)] = link or (\"Diagnostic\" .. Level)\n    links[hl(\"Title\", level)] = link or (\"Diagnostic\" .. Level)\n    links[hl(\"Footer\", level)] = link or (\"Diagnostic\" .. Level)\n  end\n  Snacks.util.set_hl(links, { default = true })\n\n  -- resize handler\n  vim.api.nvim_create_autocmd(\"VimResized\", {\n    group = vim.api.nvim_create_augroup(\"snacks_notifier\", {}),\n    callback = function()\n      for _, notif in pairs(self.queue) do\n        notif.dirty = true\n      end\n      self.sorted = nil\n    end,\n  })\nend\n\nfunction N:start()\n  local running = false\n  uv.new_timer():start(self.opts.refresh, self.opts.refresh, function()\n    if running or not next(self.queue) then\n      return\n    end\n    running = true\n    vim.schedule(function()\n      if self.in_search() then\n        running = false\n        return\n      end\n      xpcall(function()\n        self:process()\n      end, function(err)\n        if err:find(\"E565\") then\n          return\n        end\n        local trace = debug.traceback(2)\n        vim.schedule(function()\n          vim.api.nvim_err_writeln(\n            (\"Snacks notifier failed. Dropping queue. Error:\\n%s\\n\\nTrace:\\n%s\"):format(err, trace)\n          )\n        end)\n        self.queue = {}\n      end)\n      running = false\n    end)\n  end)\nend\n\nfunction N:process()\n  self:update()\n  self:layout()\nend\n\nfunction N:is_blocking()\n  local mode = vim.api.nvim_get_mode()\n  for _, m in ipairs({ \"ic\", \"ix\", \"c\", \"no\", \"r%?\", \"rm\" }) do\n    if mode.mode:find(m) == 1 then\n      return true\n    end\n  end\n  return mode.blocking\nend\n\nlocal health_msg = false\n\n---@param opts snacks.notifier.Notif.opts\nfunction N:add(opts)\n  if opts.checkhealth then\n    health_msg = true\n    return\n  end\n  local now = ts()\n  local notif = vim.deepcopy(opts) --[[@as snacks.notifier.Notif]]\n  notif.msg = notif.msg or \"\"\n\n  -- NOTE: support nvim-notify style replace\n  ---@diagnostic disable-next-line: undefined-field\n  if not notif.id and notif.replace then\n    ---@diagnostic disable-next-line: undefined-field\n    notif.id = type(notif.replace) == \"table\" and notif.replace.id or notif.replace\n  end\n\n  notif.title = (notif.title or \"\"):gsub(\"\\n\", \" \")\n  notif.id = notif.id or next_id()\n  notif.level = normlevel(notif.level)\n  notif.icon = notif.icon or self.opts.icons[notif.level]\n  notif.timeout = notif.timeout == false and 0 or notif.timeout\n  notif.timeout = notif.timeout == true and self.opts.timeout or notif.timeout\n  notif.timeout = notif.timeout or self.opts.timeout\n  notif.added = now\n\n  if opts.id and self.queue[opts.id] then\n    local n = self.queue[opts.id] --[[@as snacks.notifier.Notif]]\n    notif.added = n.added\n    notif.updated = now\n    notif.shown = n.shown and now or nil -- reset shown time\n    notif.win = n.win\n    notif.layout = n.layout\n    notif.dirty = true\n  end\n  if opts.history ~= false then\n    self.history[notif.id] = notif\n  end\n  self.sorted = nil\n  local want = numlevel(notif.level) >= numlevel(self.opts.level)\n  want = want and (not self.opts.filter or self.opts.filter(notif))\n  if not want then\n    return notif.id\n  end\n  self.queue[notif.id] = notif\n  if self:is_blocking() then\n    pcall(function()\n      self:process()\n    end)\n  end\n  return notif.id\nend\n\nfunction N:update()\n  local now = ts()\n  --- Cleanup queue\n  for id, notif in pairs(self.queue) do\n    local timeout = notif.timeout or self.opts.timeout\n    local keep = not notif.shown -- not shown yet\n      or timeout == 0 -- no timeout\n      or (notif.win and notif.win:win_valid() and vim.api.nvim_get_current_win() == notif.win.win) -- current window\n      or (notif.win and notif.win:buf_valid() and vim.api.nvim_get_current_buf() == notif.win.buf) -- current buffer\n      or (notif.keep and notif.keep(notif)) -- custom keep\n      or (self.opts.keep and self.opts.keep(notif)) -- global keep\n      or (notif.shown + timeout / 1e3 > now) -- not timed out\n    if not keep then\n      self:hide(id)\n    end\n  end\n  self.sorted = self.sorted or self:sort()\nend\n\n---@param opts? snacks.notifier.history\n---@return snacks.notifier.Notif[]\nfunction N:get_history(opts)\n  ---@type snacks.notifier.history\n  opts = vim.tbl_deep_extend(\"force\", {}, history_opts, opts or {})\n  local notifs = vim.tbl_values(self.history)\n  local filter = opts.filter\n  if type(filter) == \"string\" or type(filter) == \"number\" then\n    local level = numlevel(filter)\n    filter = function(n)\n      return numlevel(n.level) >= level\n    end\n  end\n  notifs = filter and vim.tbl_filter(filter, notifs) or notifs\n  local ret = self:sort(notifs, opts.sort)\n  if opts.reverse then\n    local rev = {}\n    for i = #ret, 1, -1 do\n      table.insert(rev, ret[i])\n    end\n    ret = rev\n  end\n  return ret\nend\n\n---@param opts? snacks.notifier.history\nfunction N:show_history(opts)\n  if vim.bo.filetype == \"snacks_notif_history\" then\n    vim.cmd(\"close\")\n    return\n  end\n  local win = Snacks.win({ style = \"notification_history\", enter = true, show = false })\n  local buf = win:open_buf()\n  opts = opts or {}\n  if opts.reverse == nil then\n    opts.reverse = true\n  end\n  for _, notif in ipairs(self:get_history(opts)) do\n    N.styles.history(buf, notif, {\n      opts = win.opts,\n      notifier = self,\n      ns = N.ns,\n      hl = self:hl(notif),\n    })\n  end\n  return win:show()\nend\n\n---@param id? number|string\nfunction N:hide(id)\n  if not id then\n    for i in pairs(self.queue) do\n      self:hide(i)\n    end\n    return\n  end\n  local notif = self.queue[id]\n  if not notif then\n    return\n  end\n  self.queue[id], self.sorted = nil, nil\n  notif.hidden = ts()\n  if notif.win then\n    notif.win:close()\n    notif.win = nil\n  end\nend\n\n---@param value number\n---@param min number\n---@param max number\n---@param parent number\nlocal function dim(value, min, max, parent)\n  min = math.floor(min < 1 and (parent * min) or min)\n  max = math.floor(max < 1 and (parent * max) or max)\n  return math.min(max, math.max(min, value))\nend\n\n---@param style? snacks.notifier.style\n---@return snacks.notifier.render\nfunction N:get_render(style)\n  style = style or self.opts.style\n  return type(style) == \"function\" and style or N.styles[style] or N.styles.compact\nend\n\n---@param notif snacks.notifier.Notif\nfunction N:hl(notif)\n  ---@type snacks.notifier.hl\n  return vim.tbl_extend(\"force\", {\n    title = hl(\"Title\", notif.level),\n    icon = hl(\"Icon\", notif.level),\n    border = hl(\"Border\", notif.level),\n    footer = hl(\"Footer\", notif.level),\n    msg = hl(\"\", notif.level),\n  }, notif.hl or {})\nend\n\n---@param notif snacks.notifier.Notif\nfunction N:render(notif)\n  if type(notif.opts) == \"function\" then\n    notif.opts(notif)\n  end\n\n  ---@type snacks.notifier.hl\n  local notif_hl = self:hl(notif)\n\n  local win = notif.win\n    or Snacks.win({\n      show = false,\n      style = \"notification\",\n      enter = false,\n      backdrop = false,\n      ft = notif.ft,\n      noautocmd = true,\n      keys = {\n        q = function()\n          self:hide(notif.id)\n        end,\n      },\n    })\n  win.opts.wo.winhighlight = table.concat({\n    \"Normal:\" .. notif_hl.msg,\n    \"NormalNC:\" .. notif_hl.msg,\n    \"FloatBorder:\" .. notif_hl.border,\n    \"FloatTitle:\" .. notif_hl.title,\n    \"FloatFooter:\" .. notif_hl.footer,\n  }, \",\")\n  notif.win = win\n  ---@diagnostic disable-next-line: invisible\n  local buf = win:open_buf()\n  vim.api.nvim_buf_clear_namespace(buf, N.ns, 0, -1)\n  local render = self:get_render(notif.style)\n\n  vim.bo[buf].modifiable = true\n  vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})\n  render(buf, notif, {\n    opts = win.opts,\n    notifier = self,\n    ns = N.ns,\n    hl = notif_hl,\n  })\n  vim.bo[buf].modifiable = false\n\n  local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)\n\n  -- for the minimal style, we also have to factor in the icon width\n  local icon_width = self.opts.style == \"minimal\" and vim.api.nvim_strwidth(notif.icon) or 0\n  local pad = (self.opts.padding and (win:add_padding() or 2) or 0) + icon_width\n  local width = win:border_text_width()\n  for _, line in ipairs(lines) do\n    width = math.max(width, vim.fn.strdisplaywidth(line) + pad)\n  end\n  width = dim(width, self.opts.width.min, self.opts.width.max, vim.o.columns)\n\n  local height = #lines\n  -- calculate wrapped height\n  if win.opts.wo.wrap then\n    height = 0\n    for _, line in ipairs(lines) do\n      height = height + math.ceil((vim.fn.strdisplaywidth(line) + pad) / width)\n    end\n  end\n  local wanted_height = height\n  height = dim(height, self.opts.height.min, self.opts.height.max, vim.o.lines)\n\n  if wanted_height > height and win:has_border() and self.opts.more_format and not win.opts.footer then\n    win.opts.footer = self.opts.more_format:format(wanted_height - height)\n    win.opts.footer_pos = \"right\"\n  end\n\n  win.opts.width = width\n  win.opts.height = height\nend\n\n---@param notifs? snacks.notifier.Notif[]\n---@param fields? string[]\nfunction N:sort(notifs, fields)\n  fields = fields or self.opts.sort\n  notifs = notifs or vim.tbl_values(self.queue)\n  table.sort(notifs, function(a, b)\n    for _, key in ipairs(fields) do\n      local function v(n)\n        if key == \"level\" then\n          return 10 - numlevel(n[key])\n        end\n        return n[key]\n      end\n      local av, bv = v(a), v(b)\n      if av ~= bv then\n        return av < bv\n      end\n    end\n    return false\n  end)\n  return notifs\nend\n\nfunction N:new_layout()\n  ---@class snacks.notifier.layout\n  local layout = {}\n  layout.free = 0\n  layout.rows = {} ---@type boolean[]\n  ---@param row number\n  ---@param height number\n  ---@param free boolean\n  function layout.mark(row, height, free)\n    for i = row, math.min(row + height - 1, vim.o.lines) do\n      layout.free = layout.free + (free and 1 or -1)\n      layout.rows[i] = free\n    end\n  end\n  ---@param height number\n  ---@param row? number wanted row\n  function layout.find(height, row)\n    local from, to, down = row or 1, vim.o.lines - height, self.opts.top_down\n    for i = down and from or to, down and to or from, down and 1 or -1 do\n      local ret = true\n      for j = i, i + height - 1 do\n        if not layout.rows[j] then\n          ret = false\n          break\n        end\n      end\n      if ret then\n        return i\n      end\n    end\n  end\n  layout.mark(1, vim.o.lines, true)\n  layout.mark(1, self.opts.margin.top + (vim.o.tabline == \"\" and 0 or 1), false)\n  layout.mark(vim.o.lines - (self.opts.margin.bottom + (vim.o.laststatus == 0 and 0 or 1)) + 1, vim.o.lines, false)\n  return layout\nend\n\nfunction N:layout()\n  local layout = self:new_layout()\n  local wins_updated = 0\n  local wins_created = 0\n  local wins_skipped = 0\n  local update = {} ---@type snacks.win[]\n  for _, notif in ipairs(assert(self.sorted)) do\n    if layout.free < (self.opts.height.min + 2) or wins_skipped > MAX_SKIPPED then -- not enough space\n      if notif.win then\n        notif.shown = nil\n        notif.win:hide()\n      end\n    else\n      local prev_layout = notif.layout\n        and { top = notif.layout.top, height = notif.layout.height, width = notif.layout.width }\n      if not notif.win or notif.dirty or not notif.win:buf_valid() or type(notif.opts) == \"function\" then\n        notif.dirty = true\n        self:render(notif)\n        notif.dirty = false\n        notif.layout = notif.win:size()\n        notif.layout.top = prev_layout and prev_layout.top\n        prev_layout = nil -- always re-render since opts might've changed\n      end\n      notif.layout.top = layout.find(notif.layout.height, notif.layout.top)\n      if notif.layout.top then\n        layout.mark(notif.layout.top, notif.layout.height + (self.opts.gap or 0), false)\n        if not vim.deep_equal(prev_layout, notif.layout) then\n          if notif.win:win_valid() then\n            wins_updated = wins_updated + 1\n          else\n            wins_created = wins_created + 1\n          end\n          update[#update + 1] = notif.win\n          notif.win.opts.row = notif.layout.top - 1\n          notif.win.opts.col = vim.o.columns - notif.layout.width - self.opts.margin.right\n          notif.shown = notif.shown or ts()\n          notif.win:show()\n        end\n      elseif notif.win then\n        wins_skipped = wins_skipped + 1\n        notif.shown = nil\n        notif.win:hide()\n      end\n    end\n  end\n\n  if #update > 0 and not self.in_search() then\n    if vim.api.nvim__redraw then\n      for _, win in ipairs(update) do\n        win:redraw()\n      end\n    else\n      vim.cmd.redraw()\n    end\n  end\nend\n\nfunction N.in_search()\n  return vim.tbl_contains({ \"/\", \"?\" }, vim.fn.getcmdtype())\nend\n\n---@param msg string\n---@param level? snacks.notifier.level|number\n---@param opts? snacks.notifier.Notif.opts\nfunction N:notify(msg, level, opts)\n  opts = opts or {}\n  opts.msg = msg\n  opts.level = level\n  return self:add(opts)\nend\n\n-- Global instance\nlocal notifier = N.new()\n\n---@param msg string\n---@param level? snacks.notifier.level|number\n---@param opts? snacks.notifier.Notif.opts\nfunction M.notify(msg, level, opts)\n  return notifier:notify(msg, level, opts)\nend\n\n---@param id? number|string\nfunction M.hide(id)\n  return notifier:hide(id)\nend\n\n---@param opts? snacks.notifier.history\nfunction M.get_history(opts)\n  return notifier:get_history(opts)\nend\n\n---@param opts? snacks.notifier.history\nfunction M.show_history(opts)\n  return notifier:show_history(opts)\nend\n\n---@private\nfunction M.health()\n  health_msg = false\n  vim.notify(\"\", nil, { checkhealth = true })\n  vim.wait(500, function()\n    return health_msg\n  end, 10)\n  if health_msg then\n    Snacks.health.ok(\"is ready\")\n  else\n    Snacks.health.error(\"is not ready\")\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/notify.lua",
    "content": "---@class snacks.notify\n---@overload fun(msg: string|string[], opts?: snacks.notify.Opts)\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.notify(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Utility functions to work with Neovim's `vim.notify`\",\n}\n\n---@alias snacks.notify.Opts snacks.notifier.Notif.opts|{once?: boolean}\n\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nfunction M.notify(msg, opts)\n  opts = opts or {}\n  local notify = vim[opts.once and \"notify_once\" or \"notify\"] --[[@as fun(...)]]\n  notify = vim.in_fast_event() and vim.schedule_wrap(notify) or notify\n  msg = type(msg) == \"table\" and table.concat(msg, \"\\n\") or msg --[[@as string]]\n  msg = vim.trim(msg)\n  opts.title = opts.title or \"Snacks\"\n  return notify(msg, opts.level, opts)\nend\n\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nfunction M.warn(msg, opts)\n  return M.notify(msg, vim.tbl_extend(\"keep\", { level = vim.log.levels.WARN }, opts or {}))\nend\n\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nfunction M.info(msg, opts)\n  return M.notify(msg, vim.tbl_extend(\"keep\", { level = vim.log.levels.INFO }, opts or {}))\nend\n\n---@param msg string|string[]\n---@param opts? snacks.notify.Opts\nfunction M.error(msg, opts)\n  return M.notify(msg, vim.tbl_extend(\"keep\", { level = vim.log.levels.ERROR }, opts or {}))\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/actions.lua",
    "content": "---@class snacks.picker.actions\n---@field [string] snacks.picker.Action.spec\nlocal M = {}\n\n---@class snacks.picker.jump.Action: snacks.picker.Action\n---@field cmd? snacks.picker.EditCmd\n\n---@class snacks.picker.layout.Action: snacks.picker.Action\n---@field layout? snacks.picker.layout.Config|string\n\n---@class snacks.picker.yank.Action: snacks.picker.Action\n---@field reg? string\n---@field field? string\n---@field notify? boolean\n\n---@class snacks.picker.insert.Action: snacks.picker.Action\n---@field expr string\n\n---@enum (key) snacks.picker.EditCmd\nlocal edit_cmd = {\n  edit = \"buffer\",\n  split = \"sbuffer\",\n  vsplit = \"vert sbuffer\",\n  tab = \"tab sbuffer\",\n  drop = \"drop\",\n  tabdrop = \"tab drop\",\n}\n\n--- Get `vim.v.count1`, but return 1 if in insert mode.\n--- In insert mode, you can't really pass a count, so we default to 1\nlocal function count1()\n  return vim.fn.mode():sub(1, 1) == \"i\" and 1 or vim.v.count1\nend\n\nfunction M.jump(picker, _, action)\n  ---@cast action snacks.picker.jump.Action\n  -- if we're still in insert mode, stop it and schedule\n  -- it to prevent issues with cursor position\n  if vim.fn.mode():sub(1, 1) == \"i\" then\n    vim.cmd.stopinsert()\n    vim.schedule(function()\n      M.jump(picker, _, action)\n    end)\n    return\n  end\n\n  local items = picker:selected({ fallback = true })\n\n  if picker.opts.jump.close then\n    picker:close()\n  else\n    vim.api.nvim_set_current_win(picker.main)\n  end\n\n  if #items == 0 then\n    return\n  end\n\n  local win = vim.api.nvim_get_current_win()\n\n  local current_buf = vim.api.nvim_get_current_buf()\n  local current_tab = vim.api.nvim_get_current_tabpage()\n  local current_empty = vim.bo[current_buf].buftype == \"\"\n    and vim.bo[current_buf].filetype == \"\"\n    and vim.api.nvim_buf_line_count(current_buf) == 1\n    and vim.api.nvim_buf_get_lines(current_buf, 0, -1, false)[1] == \"\"\n    and vim.api.nvim_buf_get_name(current_buf) == \"\"\n  local current_tab_windows = #vim.tbl_filter(function(w)\n    return not Snacks.util.is_float(w)\n  end, vim.api.nvim_tabpage_list_wins(current_tab))\n\n  if not current_empty then\n    -- save position in jump list\n    if picker.opts.jump.jumplist then\n      vim.api.nvim_win_call(win, function()\n        vim.cmd(\"normal! m'\")\n      end)\n    end\n\n    -- save position in tag stack\n    if picker.opts.jump.tagstack then\n      local from = vim.fn.getpos(\".\")\n      from[1] = current_buf\n      local tagstack = { { tagname = vim.fn.expand(\"<cword>\"), from = from } }\n      vim.fn.settagstack(vim.fn.win_getid(win), { items = tagstack }, \"t\")\n    end\n  end\n\n  local cmd = edit_cmd[action.cmd] or edit_cmd.edit\n  local is_drop = cmd:find(\"drop\") ~= nil\n\n  -- load the buffers\n  local first_buf ---@type number\n  for _, item in ipairs(items) do\n    local buf = item.buf ---@type number\n    if not buf then\n      local path = assert(Snacks.picker.util.path(item), \"Either item.buf or item.file is required\")\n      buf = vim.fn.bufadd(path)\n    end\n    vim.bo[buf].buflisted = true\n    first_buf = first_buf or buf\n  end\n\n  -- find an existing window showing the first buffer in the current tab\n  ---@param in_tab? boolean\n  local function find_win(in_tab)\n    if first_buf == current_buf then\n      return true\n    end\n    for _, w in ipairs(vim.fn.win_findbuf(first_buf)) do\n      if\n        vim.api.nvim_win_get_config(w).relative == \"\"\n        and (in_tab ~= true or vim.api.nvim_win_get_tabpage(w) == current_tab)\n      then\n        win = w\n        vim.api.nvim_set_current_win(win)\n        return true\n      end\n    end\n  end\n\n  -- use an existing window if reuse_win or drop\n  if is_drop then\n    if find_win() or cmd == \"drop\" then\n      cmd = \"buffer\"\n    else\n      cmd = \"tab sbuffer\"\n    end\n  elseif cmd == \"buffer\" and #items == 1 and picker.opts.jump.reuse_win then\n    find_win(true)\n  end\n\n  -- Don't open a new tab if current buffer is empty\n  if cmd == \"tab sbuffer\" and current_empty and current_tab_windows == 1 then\n    cmd = \"buffer\"\n  end\n\n  -- open the first buffer\n  vim.cmd((\"%s %d\"):format(cmd, first_buf))\n  win = vim.api.nvim_get_current_win()\n\n  -- set the cursor\n  local item = items[1]\n  local pos = item.pos\n  if picker.opts.jump.match then\n    pos = picker.matcher:bufpos(vim.api.nvim_get_current_buf(), item) or pos\n  end\n  if pos and pos[1] > 0 then\n    vim.api.nvim_win_set_cursor(win, { pos[1], pos[2] })\n    vim.cmd(\"norm! zzzv\")\n  elseif item.search then\n    vim.cmd(item.search)\n    vim.cmd(\"noh\")\n  end\n\n  -- HACK: this should fix folds\n  if vim.wo.foldmethod == \"expr\" then\n    vim.schedule(function()\n      vim.opt.foldmethod = \"expr\"\n    end)\n  end\n\n  if current_empty and vim.api.nvim_buf_is_valid(current_buf) then\n    local w = vim.fn.win_findbuf(current_buf)\n    if #w == 0 then\n      vim.api.nvim_buf_delete(current_buf, { force = true })\n    end\n  end\nend\n\nfunction M.close(picker)\n  picker:norm(function()\n    picker:close()\n  end)\nend\n\nfunction M.print_cwd(picker)\n  print(vim.fn.fnamemodify(picker:cwd(), \":p:~\"))\nend\n\nfunction M.print_dir(picker)\n  print(vim.fn.fnamemodify(picker:dir(), \":p:~\"))\nend\n\nfunction M.print_path(picker, item)\n  local path = item and Snacks.picker.util.path(item) or picker:dir()\n  print(vim.fn.fnamemodify(path, \":p:~\"))\nend\n\nfunction M.cancel(picker)\n  picker:norm(function()\n    picker.main = picker:filter().current_win\n    picker:close()\n  end)\nend\n\nM.confirm = M.jump -- default confirm action\n\nM.split = { action = \"confirm\", cmd = \"split\" }\nM.vsplit = { action = \"confirm\", cmd = \"vsplit\" }\nM.tab = { action = \"confirm\", cmd = \"tab\" }\nM.drop = { action = \"confirm\", cmd = \"drop\" }\nM.tabdrop = { action = \"confirm\", cmd = \"tabdrop\" }\n\n-- aliases\nM.edit = M.jump\nM.edit_split = M.split\nM.edit_vsplit = M.vsplit\nM.edit_tab = M.tab\n\nfunction M.layout(picker, _, action)\n  ---@cast action snacks.picker.layout.Action\n  assert(action.layout, \"Layout action requires a layout\")\n  local opts = type(action.layout) == \"table\" and { layout = action.layout } or action.layout\n  ---@cast opts snacks.picker.Config\n  local layout = Snacks.picker.config.layout(opts)\n  picker:set_layout(layout)\n  -- Adjust some options for split layouts\n  if (layout.layout.position or \"float\") ~= \"float\" then\n    picker.opts.auto_close = false\n    picker.opts.jump.close = false\n    picker:toggle(\"preview\", { enable = false })\n    picker.list.win:focus()\n  end\nend\n\nM.layout_top = { action = \"layout\", layout = \"top\" }\nM.layout_bottom = { action = \"layout\", layout = \"bottom\" }\nM.layout_left = { action = \"layout\", layout = \"left\" }\nM.layout_right = { action = \"layout\", layout = \"right\" }\n\nfunction M.toggle_maximize(picker)\n  picker.layout:maximize()\nend\n\nfunction M.insert(picker, _, action)\n  ---@cast action snacks.picker.insert.Action\n  if action.expr then\n    local value = \"\"\n    vim.api.nvim_buf_call(picker.input.filter.current_buf, function()\n      value = action.expr == \"line\" and vim.api.nvim_get_current_line() or vim.fn.expand(action.expr)\n    end)\n    vim.api.nvim_win_call(picker.input.win.win, function()\n      vim.api.nvim_put({ value }, \"c\", true, true)\n    end)\n  end\nend\nM.insert_cword = { action = \"insert\", expr = \"<cword>\" }\nM.insert_cWORD = { action = \"insert\", expr = \"<cWORD>\" }\nM.insert_filename = { action = \"insert\", expr = \"%\" }\nM.insert_file = { action = \"insert\", expr = \"<cfile>\" }\nM.insert_line = { action = \"insert\", expr = \"line\" }\nM.insert_file_full = { action = \"insert\", expr = \"<cfile>:p\" }\nM.insert_alt = { action = \"insert\", expr = \"#\" }\n\nfunction M.toggle_preview(picker)\n  picker:toggle(\"preview\")\nend\n\nfunction M.toggle_input(picker)\n  picker:toggle(\"input\", { focus = true })\nend\n\nfunction M.picker_grep(_, item)\n  if item then\n    Snacks.picker.grep({ cwd = Snacks.picker.util.dir(item) })\n  end\nend\n\nfunction M.terminal(_, item)\n  if item then\n    Snacks.terminal(nil, { cwd = Snacks.picker.util.dir(item) })\n  end\nend\n\nfunction M.cd(_, item)\n  if item then\n    vim.fn.chdir(Snacks.picker.util.dir(item))\n  end\nend\n\nfunction M.tcd(_, item)\n  if item then\n    vim.cmd.tcd(Snacks.picker.util.dir(item))\n  end\nend\n\nfunction M.lcd(_, item)\n  if item then\n    vim.cmd.lcd(Snacks.picker.util.dir(item))\n  end\nend\n\nfunction M.picker(picker, item, action)\n  if not item then\n    return\n  end\n  local source = action.source or \"files\"\n  for _, p in ipairs(Snacks.picker.get({ source = source })) do\n    p:close()\n  end\n  Snacks.picker(source, {\n    cwd = Snacks.picker.util.dir(item),\n    filter = {\n      cwd = source == \"recent\" and Snacks.picker.util.dir(item) or nil,\n    },\n    on_show = function()\n      picker:close()\n    end,\n  })\nend\n\nM.picker_files = { action = \"picker\", source = \"files\" }\nM.picker_explorer = { action = \"picker\", source = \"explorer\" }\nM.picker_recent = { action = \"picker\", source = \"recent\" }\n\nfunction M.pick_win(picker, item, action)\n  if not picker.layout.split then\n    picker.layout:hide()\n  end\n  local win = Snacks.picker.util.pick_win({ main = picker.main })\n  if not win then\n    if not picker.layout.split then\n      picker.layout:unhide()\n    end\n    return true\n  end\n  picker.main = win\n  if not picker.layout.split then\n    vim.defer_fn(function()\n      if not picker.closed then\n        picker.layout:unhide()\n      end\n    end, 100)\n  end\nend\n\nfunction M.bufdelete(picker)\n  picker.preview:reset()\n  local non_buf_delete_requested = false\n  for _, item in ipairs(picker:selected({ fallback = true })) do\n    if item.buf then\n      Snacks.bufdelete.delete(item.buf)\n    else\n      non_buf_delete_requested = true\n    end\n  end\n  if non_buf_delete_requested then\n    Snacks.notify.warn(\"Only open buffers can be deleted\", { title = \"Snacks Picker\" })\n  end\n  picker:refresh()\nend\n\nfunction M.mark_delete(picker)\n  local selected = picker:selected({ fallback = true })\n  for _, item in ipairs(selected) do\n    if item.label then\n      if item.buf then\n        vim.api.nvim_buf_del_mark(item.buf, item.label)\n      else\n        vim.api.nvim_del_mark(item.label)\n      end\n    end\n  end\n  picker:refresh()\nend\n\nfunction M.git_stage(picker)\n  local items = picker:selected({ fallback = true })\n  local first = items[1]\n  if not first or not (first.status or (first.diff and first.staged ~= nil)) then\n    Snacks.notify.error(\"Can't stage/unstage this change\", { title = \"Snacks Picker\" })\n    return\n  end\n\n  local done = 0\n  for _, item in ipairs(items) do\n    local opts = { cwd = item.cwd } ---@type snacks.picker.util.cmd.Opts\n    local cmd ---@type string[]\n    if item.diff and item.staged ~= nil then\n      opts.input = item.diff\n      cmd = { \"git\", \"apply\", \"--cached\", item.staged and \"--reverse\" or nil }\n    elseif item.status then\n      cmd = item.status:sub(2) == \" \" and { \"git\", \"restore\", \"--staged\", item.file } or { \"git\", \"add\", item.file }\n    else\n      Snacks.notify.error(\"Can't stage/unstage this change\", { title = \"Snacks Picker\" })\n      return\n    end\n    Snacks.picker.util.cmd(cmd, function()\n      done = done + 1\n      if done == #items then\n        picker:refresh()\n      end\n    end, opts)\n  end\nend\n\nfunction M.git_restore(picker)\n  local items = picker:selected({ fallback = true })\n  if #items == 0 then\n    return\n  end\n\n  local first = items[1]\n  if not first or not (first.status or (first.diff and first.staged ~= nil)) then\n    Snacks.notify.warn(\"Can't restore this change\", { title = \"Snacks Picker\" })\n    return\n  end\n\n  -- Confirm before discarding changes\n  ---@param item snacks.picker.Item\n  local files = vim.tbl_map(function(item)\n    return Snacks.picker.util.path(item)\n  end, items)\n  local msg = #items == 1 and (\"Discard changes to `%s`?\"):format(files[1])\n    or (\"Discard changes to %d files?\"):format(#items)\n\n  Snacks.picker.util.confirm(msg, function()\n    local done = 0\n    for _, item in ipairs(items) do\n      local cmd ---@type string[]\n      local opts = { cwd = item.cwd }\n\n      if item.diff and item.staged ~= nil then\n        opts.input = item.diff\n        if item.staged then\n          cmd = { \"git\", \"apply\", \"--reverse\", \"--cached\" }\n        else\n          cmd = { \"git\", \"apply\", \"--reverse\" }\n        end\n      elseif item.status then\n        cmd = { \"git\", \"restore\", item.file }\n      else\n        Snacks.notify.error(\"Can't restore this change\", { title = \"Snacks Picker\" })\n        return\n      end\n\n      Snacks.picker.util.cmd(cmd, function()\n        done = done + 1\n        if done == #items then\n          vim.schedule(function()\n            picker:refresh()\n            vim.cmd.startinsert()\n            vim.cmd.checktime()\n          end)\n        end\n      end, opts)\n    end\n  end)\nend\n\nfunction M.git_stash_apply(_, item)\n  if not item then\n    return\n  end\n  local cmd = { \"git\", \"stash\", \"apply\", item.stash }\n  Snacks.picker.util.cmd(cmd, function()\n    Snacks.notify(\"Stash applied: `\" .. item.stash .. \"`\", { title = \"Snacks Picker\" })\n  end, { cwd = item.cwd })\nend\n\nfunction M.git_checkout(picker, item)\n  picker:close()\n  if item then\n    local what = item.branch or item.commit --[[@as string?]]\n    if not what then\n      Snacks.notify.warn(\"No branch or commit found\", { title = \"Snacks Picker\" })\n      return\n    end\n    local cmd = { \"git\", \"checkout\", what }\n    local remote_branch = what:match(\"^remotes/[^/]+/(.+)$\")\n    if remote_branch then\n      cmd = { \"git\", \"checkout\", \"-b\", remote_branch, what }\n    end\n    if item.file then\n      vim.list_extend(cmd, { \"--\", item.file })\n    end\n    Snacks.picker.util.cmd(cmd, function()\n      Snacks.notify(\"Checkout \" .. what, { title = \"Snacks Picker\" })\n      vim.cmd.checktime()\n    end, { cwd = item.cwd })\n  end\nend\n\nfunction M.git_branch_add(picker)\n  Snacks.input.input({\n    prompt = \"New Branch Name\",\n    default = picker.input:get(),\n  }, function(name)\n    if (name or \"\"):match(\"^%s*$\") then\n      return\n    end\n    Snacks.picker.util.cmd({ \"git\", \"branch\", \"--list\", name }, function(data)\n      if data[1] ~= \"\" then\n        return Snacks.notify.error(\"Branch '\" .. name .. \"' already exists.\", { title = \"Snacks Picker\" })\n      end\n      Snacks.picker.util.cmd({ \"git\", \"checkout\", \"-b\", name }, function()\n        Snacks.notify(\"Created Branch `\" .. name .. \"`\", { title = \"Snacks Picker\" })\n        vim.cmd.checktime()\n        picker.list:set_target()\n        picker.input:set(\"\", \"\")\n        picker:find()\n      end, { cwd = picker:cwd() })\n    end, { cwd = picker:cwd() })\n  end)\nend\n\nfunction M.git_branch_del(picker, item)\n  if not (item and item.branch) then\n    Snacks.notify.warn(\"No branch or commit found\", { title = \"Snacks Picker\" })\n  end\n\n  local branch = item.branch\n  Snacks.picker.util.cmd({ \"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\" }, function(data)\n    -- Check if we are on the same branch\n    if data[1]:match(branch) ~= nil then\n      Snacks.notify.error(\"Cannot delete the current branch.\", { title = \"Snacks Picker\" })\n      return\n    end\n\n    Snacks.picker.util.confirm((\"Delete branch %q?\"):format(branch), function()\n      -- Proceed with deletion\n      Snacks.picker.util.cmd({ \"git\", \"branch\", \"-D\", branch }, function(_, code)\n        Snacks.notify(\"Deleted Branch `\" .. branch .. \"`\", { title = \"Snacks Picker\" })\n        vim.cmd.checktime()\n        picker:refresh()\n      end, { cwd = picker:cwd() })\n    end)\n  end, { cwd = picker:cwd() })\nend\n\n---@param items snacks.picker.Item[]\n---@param opts? {win?:number}\nlocal function setqflist(items, opts)\n  local qf = {} ---@type vim.quickfix.entry[]\n  for _, item in ipairs(items) do\n    qf[#qf + 1] = {\n      filename = Snacks.picker.util.path(item),\n      bufnr = item.buf,\n      lnum = item.pos and item.pos[1] or 1,\n      col = item.pos and item.pos[2] + 1 or 1,\n      end_lnum = item.end_pos and item.end_pos[1] or nil,\n      end_col = item.end_pos and item.end_pos[2] + 1 or nil,\n      text = item.line or item.comment or item.label or item.name or item.detail or item.text,\n      pattern = item.search,\n      type = ({ \"E\", \"W\", \"I\", \"N\" })[item.severity],\n      valid = true,\n    }\n  end\n  if opts and opts.win then\n    vim.fn.setloclist(opts.win, qf)\n    vim.cmd(\"botright lopen\")\n  else\n    vim.fn.setqflist(qf)\n    vim.cmd(\"botright copen\")\n  end\nend\n\n--- Send selected or all items to the quickfix list.\nfunction M.qflist(picker)\n  picker:close()\n  local sel = picker:selected()\n  local items = #sel > 0 and sel or picker:items()\n  setqflist(items)\nend\n\n--- Send all items to the quickfix list.\nfunction M.qflist_all(picker)\n  picker:close()\n  setqflist(picker:items())\nend\n\n--- Send selected or all items to the location list.\nfunction M.loclist(picker)\n  picker:close()\n  local sel = picker:selected()\n  local items = #sel > 0 and sel or picker:items()\n  setqflist(items, { win = picker.main })\nend\n\nfunction M.yank(picker, item, action)\n  ---@cast action snacks.picker.yank.Action\n  if item then\n    local reg = action.reg or vim.v.register\n    local value = item[action.field] or item.data or item.text\n    vim.fn.setreg(reg, value)\n    if action.notify ~= false then\n      local buf = item.buf or vim.api.nvim_win_get_buf(picker.main)\n      local ft = vim.bo[buf].filetype\n      Snacks.notify((\"Yanked to register `%s`:\\n```%s\\n%s\\n```\"):format(reg, ft, value), { title = \"Snacks Picker\" })\n    end\n  end\nend\nM.copy = M.yank\n\nfunction M.paste(picker, item, action)\n  ---@cast action snacks.picker.yank.Action\n  picker:close()\n  if item then\n    local value = item[action.field] or item.data or item.text\n    vim.api.nvim_paste(value, true, -1)\n    if picker.input.mode == \"i\" then\n      vim.schedule(function()\n        vim.cmd.startinsert({ bang = true })\n      end)\n    end\n  end\nend\nM.put = M.paste\n\nfunction M.history_back(picker)\n  picker:hist()\nend\n\nfunction M.history_forward(picker)\n  picker:hist(true)\nend\n\n--- Toggles the selection of the current item,\n--- and moves the cursor to the next item.\nfunction M.select_and_next(picker)\n  picker.list:select()\n  picker.list:_move(count1())\nend\n\n--- Toggles the selection of the current item,\n--- and moves the cursor to the prev item.\nfunction M.select_and_prev(picker)\n  picker.list:select()\n  picker.list:_move(-count1())\nend\n\n--- Selects all items in the list.\n--- Or clears the selection if all items are selected.\nfunction M.select_all(picker)\n  picker.list:select_all()\nend\n\nfunction M.cmd(picker, item)\n  picker:close()\n  if item and item.cmd then\n    vim.schedule(function()\n      vim.api.nvim_input(\":\")\n      vim.schedule(function()\n        vim.fn.setcmdline(item.cmd)\n      end)\n    end)\n  end\nend\n\nfunction M.search(picker, item)\n  picker:close()\n  if item then\n    vim.api.nvim_input(\"/\")\n    vim.schedule(function()\n      vim.fn.setcmdline(item.text)\n    end)\n  end\nend\n\n--- Tries to load the session, if it fails, it will open the picker.\nfunction M.load_session(picker, item)\n  picker:close()\n  if not item then\n    return\n  end\n  local dir = item.file\n  local session_loaded = false\n  vim.api.nvim_create_autocmd(\"SessionLoadPost\", {\n    once = true,\n    callback = function()\n      session_loaded = true\n    end,\n  })\n  vim.defer_fn(function()\n    if not session_loaded then\n      Snacks.picker.files()\n    end\n  end, 100)\n  vim.fn.chdir(dir)\n  local session = Snacks.dashboard.sections.session()\n  if session then\n    vim.cmd(session.action:sub(2))\n  end\nend\n\nfunction M.help(picker, item, action)\n  ---@cast action snacks.picker.jump.Action\n  if item then\n    picker:close()\n    local file = Snacks.picker.util.path(item) or \"\"\n    if package.loaded.lazy then\n      local plugin = file:match(\"/([^/]+)/doc/\")\n      if plugin and require(\"lazy.core.config\").plugins[plugin] then\n        require(\"lazy\").load({ plugins = { plugin } })\n      end\n    end\n\n    local cmd = \"help \" .. item.text\n    if action.cmd == \"vsplit\" then\n      cmd = \"vert \" .. cmd\n    elseif action.cmd == \"tab\" then\n      cmd = \"tab \" .. cmd\n    end\n    vim.cmd(cmd)\n  end\nend\n\nfunction M.toggle_help_input(picker)\n  picker.input.win:toggle_help()\nend\n\nfunction M.toggle_help_list(picker)\n  picker.list.win:toggle_help()\nend\n\nfunction M.preview_scroll_down(picker)\n  if picker.preview.win:valid() then\n    picker.preview.win:scroll()\n  end\nend\n\nfunction M.preview_scroll_up(picker)\n  if picker.preview.win:valid() then\n    picker.preview.win:scroll(true)\n  end\nend\n\nfunction M.preview_scroll_left(picker)\n  if picker.preview.win:valid() then\n    picker.preview.win:hscroll(true)\n  end\nend\n\nfunction M.preview_scroll_right(picker)\n  if picker.preview.win:valid() then\n    picker.preview.win:hscroll()\n  end\nend\n\nfunction M.inspect(picker, item)\n  Snacks.debug.inspect(item)\nend\n\nfunction M.toggle_live(picker)\n  if not picker.opts.supports_live then\n    Snacks.notify.warn(\"Live search is not supported for `\" .. picker.title .. \"`\", { title = \"Snacks Picker\" })\n    return\n  end\n  picker.opts.live = not picker.opts.live\n  picker.input:set()\n  picker.input:update()\nend\n\nfunction M.toggle_focus(picker)\n  if vim.api.nvim_get_current_win() == picker.input.win.win then\n    picker:focus(\"list\", { show = true })\n  else\n    picker:focus(\"input\", { show = true })\n  end\nend\n\nfunction M.cycle_win(picker)\n  local wins = { picker.input.win.win, picker.preview.win.win, picker.list.win.win }\n  -- HACK: allow specifying an additional window to cycle through\n  if type(vim.g.snacks_picker_cycle_win) == \"number\" then\n    table.insert(wins, 3, vim.g.snacks_picker_cycle_win)\n  end\n  wins = vim.tbl_filter(function(w)\n    return vim.api.nvim_win_is_valid(w)\n  end, wins)\n  local win = vim.api.nvim_get_current_win()\n  local idx = 1\n  for i, w in ipairs(wins) do\n    if w == win then\n      idx = i\n      break\n    end\n  end\n  win = wins[idx % #wins + 1] or 1 -- cycle\n  vim.api.nvim_set_current_win(win)\nend\n\nfunction M.focus_input(picker)\n  picker:focus(\"input\", { show = true })\nend\n\nfunction M.focus_list(picker)\n  picker:focus(\"list\", { show = true })\nend\n\nfunction M.focus_preview(picker)\n  picker:focus(\"preview\", { show = true })\nend\n\nfunction M.item_action(picker, item, action)\n  if item.action then\n    picker:norm(function()\n      picker:close()\n      item.action(picker, item, action)\n    end)\n  end\nend\n\nfunction M.list_top(picker)\n  picker.list:move(1, true)\nend\n\nfunction M.list_bottom(picker)\n  picker.list:move(picker.list:count(), true)\nend\n\nfunction M.list_down(picker)\n  picker.list:move(count1())\nend\n\nfunction M.list_up(picker)\n  picker.list:move(-count1())\nend\n\nfunction M.list_scroll_top(picker)\n  local cursor = picker.list.cursor\n  picker.list:view(cursor, cursor)\nend\n\nfunction M.list_scroll_bottom(picker)\n  local cursor = picker.list.cursor\n  picker.list:view(cursor, picker.list.cursor - picker.list:height() + 1)\nend\n\nfunction M.list_scroll_center(picker)\n  local cursor = picker.list.cursor\n  picker.list:view(cursor, picker.list.cursor - math.ceil(picker.list:height() / 2) + 1)\nend\n\nfunction M.list_scroll_down(picker)\n  picker.list:scroll(picker.list.state.scroll)\nend\n\nfunction M.list_scroll_up(picker)\n  picker.list:scroll(-picker.list.state.scroll)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/config/defaults.lua",
    "content": "local M = {}\n\n---@alias snacks.picker.format.resolve fun(max_width:number):snacks.picker.Highlight[]\n---@alias snacks.picker.Extmark vim.api.keyset.set_extmark|{col:number, row?:number, field?:string}\n---@alias snacks.picker.Meta {[string]:any}\n---@alias snacks.picker.Text {[1]:string, [2]:(string|string[])?, virtual?:boolean, field?:string, resolve?:snacks.picker.format.resolve, inline?:boolean}\n---@alias snacks.picker.Highlight snacks.picker.Text|snacks.picker.Extmark|{meta?:snacks.picker.Meta}\n---@alias snacks.picker.format fun(item:snacks.picker.Item, picker:snacks.Picker):snacks.picker.Highlight[]\n---@alias snacks.picker.preview fun(ctx: snacks.picker.preview.ctx):boolean?\n---@alias snacks.picker.sort fun(a:snacks.picker.Item, b:snacks.picker.Item):boolean\n---@alias snacks.picker.transform fun(item:snacks.picker.finder.Item, ctx:snacks.picker.finder.ctx):(boolean|snacks.picker.finder.Item|nil)\n---@alias snacks.picker.Pos {[1]:number, [2]:number}\n---@alias snacks.picker.toggle {icon?:string, enabled?:boolean, value?:boolean}\n\n--- Generic filter used by some finders to pre-filter items\n---@class snacks.picker.filter.Config\n---@field cwd? boolean|string only show files for the given cwd\n---@field buf? boolean|number only show items for the current or given buffer\n---@field paths? table<string, boolean> only show items that include or exclude the given paths\n---@field filter? fun(item:snacks.picker.finder.Item, filter:snacks.picker.Filter):boolean? custom filter function\n---@field transform? fun(picker:snacks.Picker, filter:snacks.picker.Filter):boolean? filter transform. Return `true` to force refresh\n\n--- This is only used when using `opts.preview = \"preview\"`.\n--- It's a previewer that shows a preview based on the item data.\n---@class snacks.picker.Item.preview\n---@field text string text to show in the preview buffer\n---@field ft? string optional filetype used tohighlight the preview buffer\n---@field extmarks? snacks.picker.Extmark[] additional extmarks\n---@field loc? boolean set to false to disable showing the item location in the preview\n\n---@class snacks.picker.Item\n---@field [string] any\n---@field idx number\n---@field score number\n---@field frecency? number\n---@field score_add? number\n---@field score_mul? number\n---@field source_id? number\n---@field file? string\n---@field text string\n---@field pos? snacks.picker.Pos\n---@field loc? snacks.picker.lsp.Loc\n---@field end_pos? snacks.picker.Pos\n---@field highlights? snacks.picker.Highlight[][]\n---@field preview? snacks.picker.Item.preview\n---@field resolve? fun(item:snacks.picker.Item)\n---@field positions? number[] indices of matched characters in `text`\n\n---@class snacks.picker.finder.Item: snacks.picker.Item\n---@field idx? number\n---@field score? number\n\n---@class snacks.picker.layout.Config\n---@field layout snacks.layout.Box\n---@field reverse? boolean when true, the list will be reversed (bottom-up)\n---@field fullscreen? boolean open in fullscreen\n---@field cycle? boolean cycle through the list\n---@field preview? \"main\" show preview window in the picker or the main window\n---@field preset? string|fun(source:string):string\n---@field hidden? (\"input\"|\"preview\"|\"list\")[] don't show the given windows when opening the picker. (only \"input\" and \"preview\" make sense)\n---@field auto_hide? (\"input\"|\"preview\"|\"list\")[] hide the given windows when not focused (only \"input\" makes real sense)\n---@field config? fun(layout:snacks.picker.layout.Config) customize the resolved layout config\n\n---@class snacks.picker.win.Config\n---@field input? snacks.win.Config|{} input window config\n---@field list? snacks.win.Config|{} result list window config\n---@field preview? snacks.win.Config|{} preview window config\n\n---@class snacks.picker.Config\n---@field multi? (string|snacks.picker.Config)[]\n---@field source? string source name and config to use\n---@field pattern? string|fun(picker:snacks.Picker):string pattern used to filter items by the matcher\n---@field search? string|fun(picker:snacks.Picker):string search string used by finders\n---@field cwd? string current working directory\n---@field live? boolean when true, typing will trigger live searches\n---@field limit? number when set, the finder will stop after finding this number of items. useful for live searches\n---@field limit_live? number when set, the finder will stop after finding this number of items during live searches. useful for performance\n---@field ui_select? boolean set `vim.ui.select` to a snacks picker\n---@field filter? snacks.picker.filter.Config generic filter used by some finders\n--- Source definition\n---@field items? snacks.picker.finder.Item[] items to show instead of using a finder\n---@field format? string|snacks.picker.format|string format function or preset\n---@field finder? string|snacks.picker.finder|snacks.picker.finder.multi finder function or preset\n---@field preview? snacks.picker.preview|string preview function or preset\n---@field matcher? snacks.picker.matcher.Config|{} matcher config\n---@field sort? snacks.picker.sort|snacks.picker.sort.Config sort function or config\n---@field transform? string|snacks.picker.transform transform/filter function\n--- UI\n---@field win? snacks.picker.win.Config\n---@field layout? snacks.picker.layout.Config|string|{}|fun(source:string):(snacks.picker.layout.Config|string)\n---@field icons? snacks.picker.icons\n---@field prompt? string prompt text / icon\n---@field title? string defaults to a capitalized source name\n---@field auto_close? boolean automatically close the picker when focusing another window (defaults to true)\n---@field show_empty? boolean show the picker even when there are no items\n---@field show_delay? number delay (in ms) to wait before showing the picker while no results yet\n---@field focus? \"input\"|\"list\" where to focus when the picker is opened (defaults to \"input\")\n---@field enter? boolean enter the picker when opening it\n---@field toggles? table<string, string|false|snacks.picker.toggle>\n--- Preset options\n---@field previewers? snacks.picker.previewers.Config|{}\n---@field formatters? snacks.picker.formatters.Config|{}\n---@field sources? snacks.picker.sources.Config|{}|table<string, snacks.picker.Config|{}>\n---@field layouts? table<string, snacks.picker.layout.Config>\n--- Actions\n---@field actions? table<string, snacks.picker.Action.spec> actions used by keymaps\n---@field confirm? snacks.picker.Action.spec shortcut for confirm action\n---@field auto_confirm? boolean automatically confirm if there is only one item\n---@field main? snacks.picker.main.Config main editor window config\n---@field on_change? fun(picker:snacks.Picker, item?:snacks.picker.Item) called when the cursor changes\n---@field on_show? fun(picker:snacks.Picker) called when the picker is shown\n---@field on_close? fun(picker:snacks.Picker) called when the picker is closed\n---@field jump? snacks.picker.jump.Config|{}\n--- Other\n---@field config? fun(opts:snacks.picker.Config):snacks.picker.Config? custom config function\n---@field db? snacks.picker.db.Config|{}\n---@field debug? snacks.picker.debug|{}\nlocal defaults = {\n  prompt = \" \",\n  sources = {},\n  focus = \"input\",\n  show_delay = 5000,\n  limit_live = 10000,\n  layout = {\n    cycle = true,\n    --- Use the default layout or vertical if the window is too narrow\n    preset = function()\n      return vim.o.columns >= 120 and \"default\" or \"vertical\"\n    end,\n  },\n  ---@class snacks.picker.matcher.Config\n  matcher = {\n    fuzzy = true, -- use fuzzy matching\n    smartcase = true, -- use smartcase\n    ignorecase = true, -- use ignorecase\n    sort_empty = false, -- sort results when the search string is empty\n    filename_bonus = true, -- give bonus for matching file names (last part of the path)\n    file_pos = true, -- support patterns like `file:line:col` and `file:line`\n    -- the bonusses below, possibly require string concatenation and path normalization,\n    -- so this can have a performance impact for large lists and increase memory usage\n    cwd_bonus = false, -- give bonus for matching files in the cwd\n    frecency = false, -- frecency bonus\n    history_bonus = false, -- give more weight to chronological order\n  },\n  sort = {\n    -- default sort is by score, text length and index\n    fields = { \"score:desc\", \"#text\", \"idx\" },\n  },\n  ui_select = true, -- replace `vim.ui.select` with the snacks picker\n  ---@class snacks.picker.formatters.Config\n  formatters = {\n    text = {\n      ft = nil, ---@type string? filetype for highlighting\n    },\n    file = {\n      filename_first = false, -- display filename before the file path\n      --- * left: truncate the beginning of the path\n      --- * center: truncate the middle of the path\n      --- * right: truncate the end of the path\n      ---@type \"left\"|\"center\"|\"right\"\n      truncate = \"center\",\n      min_width = 40, -- minimum length of the truncated path\n      filename_only = false, -- only show the filename\n      icon_width = 2, -- width of the icon (in characters)\n      git_status_hl = true, -- use the git status highlight group for the filename\n    },\n    selected = {\n      show_always = false, -- only show the selected column when there are multiple selections\n      unselected = true, -- use the unselected icon for unselected items\n    },\n    severity = {\n      icons = true, -- show severity icons\n      level = false, -- show severity level\n      ---@type \"left\"|\"right\"\n      pos = \"left\", -- position of the diagnostics\n    },\n  },\n  ---@class snacks.picker.previewers.Config\n  previewers = {\n    diff = {\n      -- fancy: Snacks fancy diff (borders, multi-column line numbers, syntax highlighting)\n      -- syntax: Neovim's built-in diff syntax highlighting\n      -- terminal: external command (git's pager for git commands, `cmd` for other diffs)\n      style = \"fancy\", ---@type \"fancy\"|\"syntax\"|\"terminal\"\n      cmd = { \"delta\" }, -- example for using `delta` as the external diff command\n      ---@type vim.wo?|{} window options for the fancy diff preview window\n      wo = {\n        breakindent = true,\n        wrap = true,\n        linebreak = true,\n        showbreak = \"\",\n      },\n    },\n    git = {\n      args = {}, -- additional arguments passed to the git command. Useful to set pager options usin `-c ...`\n    },\n    file = {\n      max_size = 1024 * 1024, -- 1MB\n      max_line_length = 500, -- max line length\n      ft = nil, ---@type string? filetype for highlighting. Use `nil` for auto detect\n    },\n    man_pager = nil, ---@type string? MANPAGER env to use for `man` preview\n  },\n  ---@class snacks.picker.jump.Config\n  jump = {\n    jumplist = true, -- save the current position in the jumplist\n    tagstack = false, -- save the current position in the tagstack\n    reuse_win = false, -- reuse an existing window if the buffer is already open\n    close = true, -- close the picker when jumping/editing to a location (defaults to true)\n    match = false, -- jump to the first match position. (useful for `lines`)\n  },\n  toggles = {\n    follow = \"f\",\n    hidden = \"h\",\n    ignored = \"i\",\n    modified = \"m\",\n    regex = { icon = \"R\", value = false },\n  },\n  win = {\n    -- input window\n    input = {\n      keys = {\n        -- to close the picker on ESC instead of going to normal mode,\n        -- add the following keymap to your config\n        -- [\"<Esc>\"] = { \"close\", mode = { \"n\", \"i\" } },\n        [\"/\"] = \"toggle_focus\",\n        [\"<C-Down>\"] = { \"history_forward\", mode = { \"i\", \"n\" } },\n        [\"<C-Up>\"] = { \"history_back\", mode = { \"i\", \"n\" } },\n        [\"<C-c>\"] = { \"cancel\", mode = \"i\" },\n        [\"<C-w>\"] = { \"<c-s-w>\", mode = { \"i\" }, expr = true, desc = \"delete word\" },\n        [\"<CR>\"] = { \"confirm\", mode = { \"n\", \"i\" } },\n        [\"<Down>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n        [\"<Esc>\"] = \"cancel\",\n        [\"<S-CR>\"] = { { \"pick_win\", \"jump\" }, mode = { \"n\", \"i\" } },\n        [\"<S-Tab>\"] = { \"select_and_prev\", mode = { \"i\", \"n\" } },\n        [\"<Tab>\"] = { \"select_and_next\", mode = { \"i\", \"n\" } },\n        [\"<Up>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n        [\"<a-d>\"] = { \"inspect\", mode = { \"n\", \"i\" } },\n        [\"<a-f>\"] = { \"toggle_follow\", mode = { \"i\", \"n\" } },\n        [\"<a-h>\"] = { \"toggle_hidden\", mode = { \"i\", \"n\" } },\n        [\"<a-i>\"] = { \"toggle_ignored\", mode = { \"i\", \"n\" } },\n        [\"<a-r>\"] = { \"toggle_regex\", mode = { \"i\", \"n\" } },\n        [\"<a-m>\"] = { \"toggle_maximize\", mode = { \"i\", \"n\" } },\n        [\"<a-p>\"] = { \"toggle_preview\", mode = { \"i\", \"n\" } },\n        [\"<a-w>\"] = { \"cycle_win\", mode = { \"i\", \"n\" } },\n        [\"<c-a>\"] = { \"select_all\", mode = { \"n\", \"i\" } },\n        [\"<c-b>\"] = { \"preview_scroll_up\", mode = { \"i\", \"n\" } },\n        [\"<c-d>\"] = { \"list_scroll_down\", mode = { \"i\", \"n\" } },\n        [\"<c-f>\"] = { \"preview_scroll_down\", mode = { \"i\", \"n\" } },\n        [\"<c-g>\"] = { \"toggle_live\", mode = { \"i\", \"n\" } },\n        [\"<c-j>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n        [\"<c-k>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n        [\"<c-n>\"] = { \"list_down\", mode = { \"i\", \"n\" } },\n        [\"<c-p>\"] = { \"list_up\", mode = { \"i\", \"n\" } },\n        [\"<c-q>\"] = { \"qflist\", mode = { \"i\", \"n\" } },\n        [\"<c-s>\"] = { \"edit_split\", mode = { \"i\", \"n\" } },\n        [\"<c-t>\"] = { \"tab\", mode = { \"n\", \"i\" } },\n        [\"<c-u>\"] = { \"list_scroll_up\", mode = { \"i\", \"n\" } },\n        [\"<c-v>\"] = { \"edit_vsplit\", mode = { \"i\", \"n\" } },\n        [\"<c-r>#\"] = { \"insert_alt\", mode = \"i\" },\n        [\"<c-r>%\"] = { \"insert_filename\", mode = \"i\" },\n        [\"<c-r><c-a>\"] = { \"insert_cWORD\", mode = \"i\" },\n        [\"<c-r><c-f>\"] = { \"insert_file\", mode = \"i\" },\n        [\"<c-r><c-l>\"] = { \"insert_line\", mode = \"i\" },\n        [\"<c-r><c-p>\"] = { \"insert_file_full\", mode = \"i\" },\n        [\"<c-r><c-w>\"] = { \"insert_cword\", mode = \"i\" },\n        [\"<c-w>H\"] = \"layout_left\",\n        [\"<c-w>J\"] = \"layout_bottom\",\n        [\"<c-w>K\"] = \"layout_top\",\n        [\"<c-w>L\"] = \"layout_right\",\n        [\"?\"] = \"toggle_help_input\",\n        [\"G\"] = \"list_bottom\",\n        [\"gg\"] = \"list_top\",\n        [\"j\"] = \"list_down\",\n        [\"k\"] = \"list_up\",\n        [\"q\"] = \"cancel\",\n      },\n      b = {\n        minipairs_disable = true,\n      },\n    },\n    -- result list window\n    list = {\n      keys = {\n        [\"/\"] = \"toggle_focus\",\n        [\"<2-LeftMouse>\"] = \"confirm\",\n        [\"<CR>\"] = \"confirm\",\n        [\"<Down>\"] = \"list_down\",\n        [\"<Esc>\"] = \"cancel\",\n        [\"<S-CR>\"] = { { \"pick_win\", \"jump\" } },\n        [\"<S-Tab>\"] = { \"select_and_prev\", mode = { \"n\", \"x\" } },\n        [\"<Tab>\"] = { \"select_and_next\", mode = { \"n\", \"x\" } },\n        [\"<Up>\"] = \"list_up\",\n        [\"<a-d>\"] = \"inspect\",\n        [\"<a-f>\"] = \"toggle_follow\",\n        [\"<a-h>\"] = \"toggle_hidden\",\n        [\"<a-i>\"] = \"toggle_ignored\",\n        [\"<a-m>\"] = \"toggle_maximize\",\n        [\"<a-p>\"] = \"toggle_preview\",\n        [\"<a-w>\"] = \"cycle_win\",\n        [\"<c-a>\"] = \"select_all\",\n        [\"<c-b>\"] = \"preview_scroll_up\",\n        [\"<c-d>\"] = \"list_scroll_down\",\n        [\"<c-f>\"] = \"preview_scroll_down\",\n        [\"<c-j>\"] = \"list_down\",\n        [\"<c-k>\"] = \"list_up\",\n        [\"<c-n>\"] = \"list_down\",\n        [\"<c-p>\"] = \"list_up\",\n        [\"<c-q>\"] = \"qflist\",\n        [\"<c-g>\"] = \"print_path\",\n        [\"<c-s>\"] = \"edit_split\",\n        [\"<c-t>\"] = \"tab\",\n        [\"<c-u>\"] = \"list_scroll_up\",\n        [\"<c-v>\"] = \"edit_vsplit\",\n        [\"<c-w>H\"] = \"layout_left\",\n        [\"<c-w>J\"] = \"layout_bottom\",\n        [\"<c-w>K\"] = \"layout_top\",\n        [\"<c-w>L\"] = \"layout_right\",\n        [\"?\"] = \"toggle_help_list\",\n        [\"G\"] = \"list_bottom\",\n        [\"gg\"] = \"list_top\",\n        [\"i\"] = \"focus_input\",\n        [\"j\"] = \"list_down\",\n        [\"k\"] = \"list_up\",\n        [\"q\"] = \"cancel\",\n        [\"zb\"] = \"list_scroll_bottom\",\n        [\"zt\"] = \"list_scroll_top\",\n        [\"zz\"] = \"list_scroll_center\",\n      },\n      wo = {\n        conceallevel = 2,\n        concealcursor = \"nvc\",\n      },\n    },\n    -- preview window\n    preview = {\n      keys = {\n        [\"<Esc>\"] = \"cancel\",\n        [\"q\"] = \"cancel\",\n        [\"i\"] = \"focus_input\",\n        [\"<a-w>\"] = \"cycle_win\",\n      },\n    },\n  },\n  ---@class snacks.picker.icons\n    -- stylua: ignore\n  icons = {\n    files = {\n      enabled = true, -- show file icons\n      dir = \"󰉋 \",\n      dir_open = \"󰝰 \",\n      file = \"󰈔 \"\n    },\n    keymaps = {\n      nowait = \"󰓅 \"\n    },\n    tree = {\n      vertical = \"│ \",\n      middle   = \"├╴\",\n      last     = \"└╴\",\n    },\n    undo = {\n      saved   = \" \",\n    },\n    ui = {\n      live        = \"󰐰 \",\n      hidden      = \"h\",\n      ignored     = \"i\",\n      follow      = \"f\",\n      selected    = \"● \",\n      unselected  = \"○ \",\n      -- selected = \" \",\n    },\n    git = {\n      enabled   = true, -- show git icons\n      commit    = \"󰜘 \", -- used by git log\n      staged    = \"●\", -- staged changes. always overrides the type icons\n      added     = \"\",\n      deleted   = \"\",\n      ignored   = \" \",\n      modified  = \"○\",\n      renamed   = \"\",\n      unmerged  = \" \",\n      untracked = \"?\",\n    },\n    diagnostics = {\n      Error = \" \",\n      Warn  = \" \",\n      Hint  = \" \",\n      Info  = \" \",\n    },\n    lsp = {\n      unavailable = \"\",\n      enabled = \" \",\n      disabled = \" \",\n      attached = \"󰖩 \"\n    },\n    kinds = {\n      Array         = \" \",\n      Boolean       = \"󰨙 \",\n      Class         = \" \",\n      Color         = \" \",\n      Control       = \" \",\n      Collapsed     = \" \",\n      Constant      = \"󰏿 \",\n      Constructor   = \" \",\n      Copilot       = \" \",\n      Enum          = \" \",\n      EnumMember    = \" \",\n      Event         = \" \",\n      Field         = \" \",\n      File          = \" \",\n      Folder        = \" \",\n      Function      = \"󰊕 \",\n      Interface     = \" \",\n      Key           = \" \",\n      Keyword       = \" \",\n      Method        = \"󰊕 \",\n      Module        = \" \",\n      Namespace     = \"󰦮 \",\n      Null          = \" \",\n      Number        = \"󰎠 \",\n      Object        = \" \",\n      Operator      = \" \",\n      Package       = \" \",\n      Property      = \" \",\n      Reference     = \" \",\n      Snippet       = \"󱄽 \",\n      String        = \" \",\n      Struct        = \"󰆼 \",\n      Text          = \" \",\n      TypeParameter = \" \",\n      Unit          = \" \",\n      Unknown        = \" \",\n      Value         = \" \",\n      Variable      = \"󰀫 \",\n    },\n  },\n  ---@class snacks.picker.db.Config\n  db = {\n    -- path to the sqlite3 library\n    -- If not set, it will try to load the library by name.\n    -- On Windows it will download the library from the internet.\n    sqlite3_path = nil, ---@type string?\n  },\n  ---@class snacks.picker.debug\n  debug = {\n    scores = false, -- show scores in the list\n    leaks = false, -- show when pickers don't get garbage collected\n    explorer = false, -- show explorer debug info\n    files = false, -- show file debug info\n    grep = false, -- show file debug info\n    proc = false, -- show proc debug info\n    extmarks = false, -- show extmarks errors\n  },\n}\n\nM.defaults = defaults\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/config/highlights.lua",
    "content": "---@class snacks.picker.config.highlights\nlocal M = {}\n\nSnacks.util.set_hl({\n  Match = \"Special\",\n  Search = \"Search\",\n  Prompt = \"Special\",\n  InputSearch = \"@keyword\",\n  Special = \"Special\",\n  Label = \"SnacksPickerSpecial\",\n  Totals = \"NonText\",\n  File = \"\", -- basename of a file path\n  Link = \"Comment\",\n  LinkBroken = \"DiagnosticError\",\n  Directory = \"Directory\", -- basename of a directory path\n  PathIgnored = \"NonText\", -- any ignored file or directory\n  PathHidden = \"NonText\", -- any hidden file or directory\n  Dir = \"NonText\", -- dirname of a path\n  Toggle = \"DiagnosticVirtualTextInfo\",\n  Dimmed = \"Conceal\",\n  Row = \"String\",\n  Col = \"LineNr\",\n  Comment = \"Comment\",\n  Desc = \"Comment\",\n  Delim = \"Delimiter\",\n  Spinner = \"Special\",\n  Selected = \"Number\",\n  Cmd = \"Function\",\n  CmdBuiltin = \"@constructor\",\n  Unselected = \"NonText\",\n  Idx = \"Number\",\n  Bold = \"Bold\",\n  Tree = \"LineNr\",\n  Italic = \"Italic\",\n  Code = \"@markup.raw.markdown_inline\",\n  AuPattern = \"String\",\n  AuEvent = \"Constant\",\n  AuGroup = \"Type\",\n  DiagnosticCode = \"Special\",\n  DiagnosticSource = \"Comment\",\n  Register = \"Number\",\n  KeymapMode = \"Number\",\n  KeymapLhs = \"Special\",\n  KeymapNowait = \"@variable.builtin\",\n  BufNr = \"Number\",\n  BufFlags = \"NonText\",\n  BufType = \"Function\",\n  FileType = \"DiagnosticHint\",\n  KeymapRhs = \"NonText\",\n  Time = \"Special\",\n  UndoAdded = \"Added\",\n  UndoRemoved = \"Removed\",\n  UndoCurrent = \"@variable.builtin\",\n  UndoSaved = \"Special\",\n  GitCommit = \"@variable.builtin\",\n  GitBreaking = \"Error\",\n  GitDetached = \"DiagnosticWarn\",\n  GitBranch = \"Title\",\n  GitBranchCurrent = \"Number\",\n  GitDate = \"Special\",\n  GitIssue = \"Number\",\n  GitAuthor = \"Constant\",\n  GitType = \"Title\", -- conventional commit type\n  GitScope = \"Italic\", -- conventional commit scope\n  GitStatus = \"Special\",\n  GitStatusAdded = \"Added\",\n  GitStatusModified = \"DiagnosticWarn\",\n  GitStatusDeleted = \"Removed\",\n  GitStatusRenamed = \"SnacksPickerGitStatus\",\n  GitStatusCopied = \"SnacksPickerGitStatus\",\n  GitStatusUntracked = \"NonText\",\n  GitStatusIgnored = \"NonText\",\n  GitStatusUnmerged = \"DiagnosticError\",\n  GitStatusStaged = \"DiagnosticHint\",\n  ManSection = \"Number\",\n  PickWin = \"Search\",\n  PickWinCurrent = \"CurSearch\",\n  LspDisabled = \"DiagnosticWarn\",\n  LspEnabled = \"Special\",\n  LspAttached = \"DiagnosticWarn\",\n  LspAttachedBuf = \"DiagnosticInfo\",\n  LspUnavailable = \"DiagnosticError\",\n  ManPage = \"Special\",\n  -- Icons\n  Icon = \"Special\",\n  IconSource = \"@constant\",\n  IconName = \"@keyword\",\n  IconCategory = \"@module\",\n  -- LSP Symbol Kinds\n  IconArray = \"@punctuation.bracket\",\n  IconBoolean = \"@boolean\",\n  IconClass = \"@type\",\n  IconConstant = \"@constant\",\n  IconConstructor = \"@constructor\",\n  IconEnum = \"@lsp.type.enum\",\n  IconEnumMember = \"@lsp.type.enumMember\",\n  IconEvent = \"Special\",\n  IconField = \"@variable.member\",\n  IconFile = \"Normal\",\n  IconFunction = \"@function\",\n  IconInterface = \"@lsp.type.interface\",\n  IconKey = \"@lsp.type.keyword\",\n  IconMethod = \"@function.method\",\n  IconModule = \"@module\",\n  IconNamespace = \"@module\",\n  IconNull = \"@constant.builtin\",\n  IconNumber = \"@number\",\n  IconObject = \"@constant\",\n  IconOperator = \"@operator\",\n  IconPackage = \"@module\",\n  IconProperty = \"@property\",\n  IconString = \"@string\",\n  IconStruct = \"@lsp.type.struct\",\n  IconTypeParameter = \"@lsp.type.typeParameter\",\n  IconVariable = \"@variable\",\n  Rule = \"@punctuation.special.markdown\",\n}, { prefix = \"SnacksPicker\", default = true })\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/config/init.lua",
    "content": "---@class snacks.picker.config\nlocal M = {}\n\n--- Source aliases\nM.alias = {\n  live_grep = \"grep\",\n  find_files = \"files\",\n  git_commits = \"git_log\",\n  git_bcommits = \"git_log_file\",\n  oldfiles = \"recent\",\n}\n\nlocal defaults ---@type snacks.picker.Config?\n\n--- Fixes keys before merging configs for correctly resolving keymaps.\n--- For example: <c-s> -> <C-S>\n---@param opts? snacks.picker.Config\nfunction M.fix_keys(opts)\n  opts = opts or {}\n  -- fix keys in sources\n  for _, source in pairs(opts.sources or {}) do\n    M.fix_keys(source)\n  end\n  if not opts.win then\n    return opts\n  end\n  -- fix keys in wins\n  for _, win in pairs(opts.win) do\n    ---@cast win snacks.win.Config\n    if win.keys then\n      local keys = vim.tbl_keys(win.keys) ---@type string[]\n      for _, key in ipairs(keys) do\n        local norm = Snacks.util.normkey(key)\n        if key ~= norm then\n          win.keys[norm], win.keys[key] = win.keys[key], nil\n        end\n      end\n    end\n  end\n  return opts\nend\n\n---@generic T:snacks.picker.Config\n---@param opts? T\n---@return T\nfunction M.get(opts)\n  M.setup()\n  opts = M.fix_keys(opts)\n\n  -- Setup defaults\n  if not defaults then\n    defaults = require(\"snacks.picker.config.defaults\").defaults\n    defaults.sources = require(\"snacks.picker.config.sources\")\n    defaults.layouts = require(\"snacks.picker.config.layouts\")\n    M.fix_keys(defaults)\n  end\n\n  local user = M.fix_keys(Snacks.config.picker or {})\n  opts.source = M.alias[opts.source] or opts.source\n\n  -- Prepare config\n  local global = Snacks.config.get(\"picker\", defaults, opts) -- defaults + global user config\n  local source = opts.source and global.sources[opts.source] or {}\n  ---@type snacks.picker.Config[]\n  local todo = {\n    vim.deepcopy(defaults),\n    vim.deepcopy(user),\n    vim.deepcopy(source),\n    opts,\n  }\n\n  -- Merge the confirm action into the actions table\n  for _, t in ipairs(todo) do\n    if t.confirm then\n      t.actions = t.actions or {}\n      t.actions.confirm = t.confirm\n    end\n  end\n\n  -- Merge the configs\n  opts = Snacks.config.merge(unpack(todo))\n  if opts.cwd == true or opts.cwd == \"\" then\n    opts.cwd = nil\n  elseif opts.cwd then\n    opts.cwd = svim.fs.normalize(vim.fn.fnamemodify(opts.cwd:gsub(\"[\\\\/]?$\", \"/\"), \":p\"))\n  end\n  for _, t in ipairs(todo) do\n    if t.config then\n      opts = t.config(opts) or opts\n    end\n  end\n\n  -- add hl groups and actions for toggles\n  opts.actions = opts.actions or {}\n  for name in pairs(opts.toggles) do\n    local hl = table.concat(vim.tbl_map(function(a)\n      return a:sub(1, 1):upper() .. a:sub(2)\n    end, vim.split(name, \"_\")))\n    Snacks.util.set_hl({ [hl] = \"SnacksPickerToggle\" }, { default = true, prefix = \"SnacksPickerToggle\" })\n    opts.actions[\"toggle_\" .. name] = function(picker)\n      picker.opts[name] = not picker.opts[name]\n      picker.list:set_target()\n      picker:find()\n    end\n  end\n\n  M.fix_old(opts)\n\n  M.multi(opts)\n  return opts\nend\n\n--- Fixes old config options\n---@param opts snacks.picker.Config\nfunction M.fix_old(opts) end\n\n---@param opts snacks.picker.Config\nfunction M.multi(opts)\n  if not opts.multi then\n    return opts\n  end\n  local Finder = require(\"snacks.picker.core.finder\")\n\n  local finders = {} ---@type snacks.picker.finder[]\n  local formats = {} ---@type snacks.picker.format[]\n  local previews = {} ---@type snacks.picker.preview[]\n  local confirms = {} ---@type snacks.picker.Action.spec[]\n\n  local sources = {} ---@type snacks.picker.Config[]\n  for _, source in ipairs(opts.multi) do\n    if type(source) == \"string\" then\n      source = { source = source }\n    end\n    ---@cast source snacks.picker.Config\n    source = Snacks.config.merge({}, opts.sources[source.source], source) --[[@as snacks.picker.Config]]\n    source.actions = source.actions or {}\n    if source.confirm then\n      source.actions.confirm = source.confirm\n    end\n    local finder = M.finder(source.finder)\n    finders[#finders + 1] = function(fopts, ctx)\n      fopts = Snacks.config.merge(vim.deepcopy(source), fopts)\n      ctx = ctx:clone(fopts)\n      -- Update source filter when needed\n      if not vim.tbl_isempty(fopts.filter or {}) then\n        ctx.filter = ctx.filter:clone():init(fopts)\n      end\n      return finder(fopts, ctx)\n    end\n    confirms[#confirms + 1] = source.actions.confirm or \"jump\"\n    previews[#previews + 1] = M.preview(source)\n    formats[#formats + 1] = M.format(source)\n    sources[#sources + 1] = source\n\n    -- merge keys\n    for w, win in pairs(source.win or {}) do\n      if win.keys then\n        opts.win = opts.win or {}\n        opts.win[w] = opts.win[w] or {}\n        opts.win[w].keys = Snacks.config.merge(opts.win[w].keys or {}, win.keys)\n      end\n    end\n  end\n\n  opts.finder = opts.finder or Finder.multi(finders)\n  opts.format = opts.format or function(item, picker)\n    return formats[item.source_id](item, picker)\n  end\n  opts.preview = opts.preview or function(ctx)\n    return previews[ctx.item.source_id](ctx)\n  end\n  opts.confirm = opts.confirm\n    or function(picker, item, action)\n      return confirms[item.source_id](picker, item, action)\n    end\nend\n\n---@param opts snacks.picker.Config\nfunction M.format(opts)\n  local ret = type(opts.format) == \"string\" and (Snacks.picker.format[opts.format] or M.field(opts.format))\n    or opts.format\n    or Snacks.picker.format.file\n  ---@cast ret snacks.picker.format\n  return ret\nend\n\n---@param opts snacks.picker.Config\nfunction M.transform(opts)\n  local ret = type(opts.transform) == \"string\" and require(\"snacks.picker.transform\")[opts.transform]\n    or opts.transform\n    or nil\n  ---@cast ret snacks.picker.transform?\n  return ret\nend\n\n---@param opts snacks.picker.Config\nfunction M.preview(opts)\n  local preview = opts.preview or Snacks.picker.preview.file\n  preview = type(preview) == \"string\" and (Snacks.picker.preview[preview] or M.field(preview)) or preview\n  ---@cast preview snacks.picker.preview\n  return preview\nend\n\n---@param opts snacks.picker.Config\nfunction M.sort(opts)\n  local sort = opts.sort or require(\"snacks.picker.sort\").default()\n  sort = type(sort) == \"table\" and require(\"snacks.picker.sort\").default(sort) or sort\n  ---@cast sort snacks.picker.sort\n  return sort\nend\n\n--- Resolve the layout configuration\n---@param opts snacks.picker.Config|string\nfunction M.layout(opts)\n  if type(opts) == \"string\" then\n    opts = M.get({ layout = { preset = opts } })\n  end\n\n  -- Resolve the layout configuration\n  local layout = M.resolve(opts.layout or {}, opts.source)\n  layout = type(layout) == \"string\" and { preset = layout } or layout\n  ---@cast layout snacks.picker.layout.Config\n\n  -- only resolve presets when the layout has no layout\n  if not (layout.layout and layout.layout[1]) then\n    -- Resolve the preset\n    local layouts = opts.layouts or M.get().layouts or {}\n    local done = {} ---@type table<string, boolean>\n    local todo = { layout } ---@type snacks.picker.layout.Config[]\n    while true do\n      local preset = M.resolve(todo[1].preset or \"custom\", opts.source)\n      if not preset or done[preset] or not layouts[preset] then\n        break\n      end\n      done[preset] = true\n      table.insert(todo, 1, vim.deepcopy(layouts[preset]))\n    end\n\n    -- Merge and return the layout\n    layout = Snacks.config.merge(unpack(todo)) --[[@as snacks.picker.layout.Config]]\n  end\n\n  -- Fix deprecated layout options\n  layout.hidden = layout.hidden or {}\n  if layout.preview == false then\n    table.insert(layout.hidden, \"preview\")\n    layout.preview = nil\n  elseif type(layout.preview) == \"table\" then\n    ---@cast layout snacks.picker.layout.Config|{preview: {enabled: boolean, main: boolean}}\n    if layout.preview.enabled == false then\n      table.insert(layout.hidden, \"preview\")\n    end\n    if layout.preview.main then\n      layout.preview = \"main\"\n    else\n      layout.preview = nil\n    end\n  end\n\n  if layout.config then\n    layout = layout.config(layout) or layout\n  end\n\n  return layout\nend\n\n---@generic T\n---@generic A\n---@param v (fun(...:A):T)|unknown\n---@param ... A\n---@return T\nfunction M.resolve(v, ...)\n  return type(v) == \"function\" and v(...) or v\nend\n\n--- Get the finder\n---@param finder string|snacks.picker.finder|snacks.picker.finder.multi\n---@return snacks.picker.finder\nfunction M.finder(finder)\n  local nop = function()\n    Snacks.notify.error(\"Finder not found:\\n```lua\\n\" .. vim.inspect(finder) .. \"\\n```\", { title = \"Snacks Picker\" })\n  end\n  if not finder or type(finder) == \"function\" then\n    return finder\n  end\n  if type(finder) == \"table\" then\n    ---@cast finder snacks.picker.finder.multi\n    ---@type snacks.picker.finder[]\n    local finders = vim.tbl_map(function(f)\n      return M.finder(f)\n    end, finder)\n    return require(\"snacks.picker.core.finder\").multi(finders)\n  end\n  ---@cast finder string\n  return M.field(finder) or nop\nend\n\n---@param picker snacks.Picker\n---@param action string\nfunction M.action(picker, action)\n  local ret = (picker.opts.actions or {})[action] or require(\"snacks.picker.actions\")[action]\n  if ret then\n    return ret\n  end\n  local source = action:match(\"^(.-)_\")\n  if source then -- source specific action\n    return (M.field((\"%s_actions\"):format(source)) or {})[action]\n  end\nend\n\n--- Resolves a module field\n---@param spec string\nfunction M.field(spec)\n  local parts = vim.split(spec, \".\", { plain = true })\n  local name, field = parts[#parts]:match(\"^(.-)[_#](.+)$\")\n  if name and field then\n    parts[#parts] = name\n  else\n    field = parts[#parts]\n  end\n  local ok, ret = pcall(function()\n    return require(\"snacks.picker.source.\" .. table.concat(parts, \".\"))[field]\n  end)\n  return ok and ret or nil\nend\n\nlocal did_setup = false\nfunction M.setup()\n  if did_setup then\n    return\n  end\n  did_setup = true\n  require(\"snacks.picker.config.highlights\")\n  for source in pairs(Snacks.picker.config.get().sources) do\n    M.wrap(source)\n  end\n  --- Automatically wrap new sources added after setup\n  setmetatable(require(\"snacks.picker.config.sources\"), {\n    __newindex = function(t, k, v)\n      rawset(t, k, v)\n      M.wrap(k)\n    end,\n  })\nend\n\n---@param source string\n---@param opts? {check?: boolean}\nfunction M.wrap(source, opts)\n  if opts and opts.check then\n    local config = M.get()\n    if not config.sources[source] then\n      return\n    end\n  end\n  if rawget(Snacks.picker, source) then\n    return Snacks.picker[source]\n  end\n  ---@type fun(opts: snacks.picker.Config): snacks.Picker\n  local ret = function(_opts)\n    return Snacks.picker.pick(source, _opts)\n  end\n  ---@diagnostic disable-next-line: no-unknown\n  Snacks.picker[source] = ret\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/config/layouts.lua",
    "content": "---@class snacks.picker.layouts\n---@field [string] snacks.picker.layout.Config\nlocal M = {}\n\nM.default = {\n  layout = {\n    box = \"horizontal\",\n    width = 0.8,\n    min_width = 120,\n    height = 0.8,\n    {\n      box = \"vertical\",\n      border = true,\n      title = \"{title} {live} {flags}\",\n      { win = \"input\", height = 1, border = \"bottom\" },\n      { win = \"list\", border = \"none\" },\n    },\n    { win = \"preview\", title = \"{preview}\", border = true, width = 0.5 },\n  },\n}\n\nM.sidebar = {\n  preview = \"main\",\n  layout = {\n    backdrop = false,\n    width = 40,\n    min_width = 40,\n    height = 0,\n    position = \"left\",\n    border = \"none\",\n    box = \"vertical\",\n    {\n      win = \"input\",\n      height = 1,\n      border = true,\n      title = \"{title} {live} {flags}\",\n      title_pos = \"center\",\n    },\n    { win = \"list\", border = \"none\" },\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n  },\n}\n\nM.telescope = {\n  reverse = true,\n  layout = {\n    box = \"horizontal\",\n    backdrop = false,\n    width = 0.8,\n    height = 0.9,\n    border = \"none\",\n    {\n      box = \"vertical\",\n      { win = \"list\", title = \" Results \", title_pos = \"center\", border = true },\n      { win = \"input\", height = 1, border = true, title = \"{title} {live} {flags}\", title_pos = \"center\" },\n    },\n    {\n      win = \"preview\",\n      title = \"{preview:Preview}\",\n      width = 0.45,\n      border = true,\n      title_pos = \"center\",\n    },\n  },\n}\n\nM.ivy = {\n  layout = {\n    box = \"vertical\",\n    backdrop = false,\n    row = -1,\n    width = 0,\n    height = 0.4,\n    border = \"top\",\n    title = \" {title} {live} {flags}\",\n    title_pos = \"left\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    {\n      box = \"horizontal\",\n      { win = \"list\", border = \"none\" },\n      { win = \"preview\", title = \"{preview}\", width = 0.6, border = \"left\" },\n    },\n  },\n}\n\nM.ivy_split = {\n  preview = \"main\",\n  layout = {\n    box = \"vertical\",\n    backdrop = false,\n    width = 0,\n    height = 0.4,\n    position = \"bottom\",\n    border = \"top\",\n    title = \" {title} {live} {flags}\",\n    title_pos = \"left\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    {\n      box = \"horizontal\",\n      { win = \"list\", border = \"none\" },\n      { win = \"preview\", title = \"{preview}\", width = 0.6, border = \"left\" },\n    },\n  },\n}\n\nM.dropdown = {\n  layout = {\n    backdrop = false,\n    row = 1,\n    width = 0.4,\n    min_width = 80,\n    height = 0.8,\n    border = \"none\",\n    box = \"vertical\",\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = true },\n    {\n      box = \"vertical\",\n      border = true,\n      title = \"{title} {live} {flags}\",\n      title_pos = \"center\",\n      { win = \"input\", height = 1, border = \"bottom\" },\n      { win = \"list\", border = \"none\" },\n    },\n  },\n}\n\nM.vertical = {\n  layout = {\n    backdrop = false,\n    width = 0.5,\n    min_width = 80,\n    height = 0.8,\n    min_height = 30,\n    box = \"vertical\",\n    border = true,\n    title = \"{title} {live} {flags}\",\n    title_pos = \"center\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    { win = \"list\", border = \"none\" },\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n  },\n}\n\nM.select = {\n  hidden = { \"preview\" },\n  layout = {\n    backdrop = false,\n    width = 0.5,\n    min_width = 80,\n    max_width = 100,\n    height = 0.4,\n    min_height = 2,\n    box = \"vertical\",\n    border = true,\n    title = \"{title}\",\n    title_pos = \"center\",\n    { win = \"input\", height = 1, border = \"bottom\" },\n    { win = \"list\", border = \"none\" },\n    { win = \"preview\", title = \"{preview}\", height = 0.4, border = \"top\" },\n  },\n}\n\nM.vscode = {\n  hidden = { \"preview\" },\n  layout = {\n    backdrop = false,\n    row = 1,\n    width = 0.4,\n    min_width = 80,\n    height = 0.4,\n    border = \"none\",\n    box = \"vertical\",\n    { win = \"input\", height = 1, border = true, title = \"{title} {live} {flags}\", title_pos = \"center\" },\n    { win = \"list\", border = \"hpad\" },\n    { win = \"preview\", title = \"{preview}\", border = true },\n  },\n}\n\nM.left = M.sidebar\nM.right = { preset = \"sidebar\", layout = { position = \"right\" } }\nM.top = { preset = \"ivy\", layout = { position = \"top\" } }\nM.bottom = { preset = \"ivy\", layout = { position = \"bottom\" } }\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/config/sources.lua",
    "content": "---@class snacks.picker.Config\n---@field supports_live? boolean\n\n---@class snacks.picker.sources.Config\n---@field [string] snacks.picker.Config|{}\nlocal M = {}\n\nM.autocmds = {\n  finder = \"vim_autocmds\",\n  format = \"autocmd\",\n  preview = \"preview\",\n}\n\n---@class snacks.picker.buffers.Config: snacks.picker.Config\n---@field hidden? boolean show hidden buffers (unlisted)\n---@field unloaded? boolean show loaded buffers\n---@field current? boolean show current buffer\n---@field nofile? boolean show `buftype=nofile` buffers\n---@field modified? boolean show only modified buffers\n---@field sort_lastused? boolean sort by last used\n---@field filter? snacks.picker.filter.Config\nM.buffers = {\n  finder = \"buffers\",\n  format = \"buffer\",\n  hidden = false,\n  unloaded = true,\n  current = true,\n  sort_lastused = true,\n  win = {\n    input = {\n      keys = {\n        [\"<c-x>\"] = { \"bufdelete\", mode = { \"n\", \"i\" } },\n      },\n    },\n    list = { keys = { [\"dd\"] = \"bufdelete\" } },\n  },\n}\n\n---@class snacks.picker.explorer.Config: snacks.picker.files.Config|{}\n---@field follow_file? boolean follow the file from the current buffer\n---@field tree? boolean show the file tree (default: true)\n---@field git_status? boolean show git status (default: true)\n---@field git_status_open? boolean show recursive git status for open directories\n---@field git_untracked? boolean needed to show untracked git status\n---@field diagnostics? boolean show diagnostics\n---@field diagnostics_open? boolean show recursive diagnostics for open directories\n---@field watch? boolean watch for file changes\n---@field exclude? string[] exclude glob patterns\n---@field include? string[] include glob patterns. These take precedence over `exclude`, `ignored` and `hidden`\nM.explorer = {\n  finder = \"explorer\",\n  sort = { fields = { \"sort\" } },\n  supports_live = true,\n  tree = true,\n  watch = true,\n  diagnostics = true,\n  diagnostics_open = false,\n  git_status = true,\n  git_status_open = false,\n  git_untracked = true,\n  follow_file = true,\n  focus = \"list\",\n  auto_close = false,\n  jump = { close = false },\n  layout = { preset = \"sidebar\", preview = false },\n  -- to show the explorer to the right, add the below to\n  -- your config under `opts.picker.sources.explorer`\n  -- layout = { layout = { position = \"right\" } },\n  formatters = {\n    file = { filename_only = true },\n    severity = { pos = \"right\" },\n  },\n  matcher = { sort_empty = false, fuzzy = false },\n  config = function(opts)\n    return require(\"snacks.picker.source.explorer\").setup(opts)\n  end,\n  win = {\n    list = {\n      keys = {\n        [\"<BS>\"] = \"explorer_up\",\n        [\"l\"] = \"confirm\",\n        [\"h\"] = \"explorer_close\", -- close directory\n        [\"a\"] = \"explorer_add\",\n        [\"d\"] = \"explorer_del\",\n        [\"r\"] = \"explorer_rename\",\n        [\"c\"] = \"explorer_copy\",\n        [\"m\"] = \"explorer_move\",\n        [\"o\"] = \"explorer_open\", -- open with system application\n        [\"P\"] = \"toggle_preview\",\n        [\"y\"] = { \"explorer_yank\", mode = { \"n\", \"x\" } },\n        [\"p\"] = \"explorer_paste\",\n        [\"u\"] = \"explorer_update\",\n        [\"<c-c>\"] = \"tcd\",\n        [\"<leader>/\"] = \"picker_grep\",\n        [\"<c-t>\"] = \"terminal\",\n        [\".\"] = \"explorer_focus\",\n        [\"I\"] = \"toggle_ignored\",\n        [\"H\"] = \"toggle_hidden\",\n        [\"Z\"] = \"explorer_close_all\",\n        [\"]g\"] = \"explorer_git_next\",\n        [\"[g\"] = \"explorer_git_prev\",\n        [\"]d\"] = \"explorer_diagnostic_next\",\n        [\"[d\"] = \"explorer_diagnostic_prev\",\n        [\"]w\"] = \"explorer_warn_next\",\n        [\"[w\"] = \"explorer_warn_prev\",\n        [\"]e\"] = \"explorer_error_next\",\n        [\"[e\"] = \"explorer_error_prev\",\n      },\n    },\n  },\n}\n\nM.cliphist = {\n  finder = \"system_cliphist\",\n  format = \"text\",\n  preview = \"preview\",\n  confirm = { \"copy\", \"close\" },\n}\n\n-- Neovim colorschemes with live preview\nM.colorschemes = {\n  finder = \"vim_colorschemes\",\n  format = \"text\",\n  preview = \"colorscheme\",\n  preset = \"vertical\",\n  confirm = function(picker, item)\n    picker:close()\n    if item then\n      picker.preview.state.colorscheme = nil\n      vim.schedule(function()\n        vim.cmd(\"colorscheme \" .. item.text)\n      end)\n    end\n  end,\n}\n\n-- Neovim command history\n---@type snacks.picker.history.Config\nM.command_history = {\n  finder = \"vim_history\",\n  name = \"cmd\",\n  format = \"text\",\n  preview = \"none\",\n  main = { current = true },\n  layout = {\n    preset = \"vscode\",\n  },\n  confirm = \"cmd\",\n  formatters = { text = { ft = \"vim\" } },\n}\n\n-- Neovim commands\nM.commands = {\n  finder = \"vim_commands\",\n  format = \"command\",\n  preview = \"preview\",\n  confirm = \"cmd\",\n}\n\n---@class snacks.picker.diagnostics.Config: snacks.picker.Config\n---@field filter? snacks.picker.filter.Config\n---@field severity? vim.diagnostic.SeverityFilter\nM.diagnostics = {\n  finder = \"diagnostics\",\n  format = \"diagnostic\",\n  sort = {\n    fields = {\n      \"is_current\",\n      \"is_cwd\",\n      \"severity\",\n      \"file\",\n      \"lnum\",\n    },\n  },\n  matcher = { sort_empty = true },\n  -- only show diagnostics from the cwd by default\n  filter = { cwd = true },\n}\n\n---@type snacks.picker.diagnostics.Config\nM.diagnostics_buffer = {\n  finder = \"diagnostics\",\n  format = \"diagnostic\",\n  sort = {\n    fields = { \"severity\", \"file\", \"lnum\" },\n  },\n  matcher = { sort_empty = true },\n  filter = { buf = true },\n}\n\n---@class snacks.picker.files.Config: snacks.picker.proc.Config\n---@field cmd? \"fd\"| \"rg\"| \"find\" command to use. Leave empty to auto-detect\n---@field hidden? boolean show hidden files\n---@field ignored? boolean show ignored files\n---@field dirs? string[] directories to search\n---@field follow? boolean follow symlinks\n---@field exclude? string[] exclude patterns\n---@field args? string[] additional arguments\n---@field ft? string|string[] file extension(s)\n---@field rtp? boolean search in runtimepath\nM.files = {\n  finder = \"files\",\n  format = \"file\",\n  show_empty = true,\n  hidden = false,\n  ignored = false,\n  follow = false,\n  supports_live = true,\n}\n\n---@class snacks.picker.gh.Config: snacks.picker.Config\n---@field app? string GitHub App author\n---@field assignee? string filter by assignee\n---@field author? string filter by author\n---@field jq? string custom jq filter\n---@field label? string filter by label(s)\n---@field limit? number number of items to fetch (default: 50)\n---@field repo? string GitHub repository (owner/repo). Defaults to current git repo\n\n---@class snacks.picker.gh.issue.Config: snacks.picker.gh.Config\n---@field state \"open\" | \"closed\" | \"all\"\n---@field mention? string filter by mention\n---@field milestone? string filter by milestone\nM.gh_issue = {\n  title = \"  Issues\",\n  finder = \"gh_issue\",\n  format = \"gh_format\",\n  preview = \"gh_preview\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n  supports_live = true,\n  live = true,\n  confirm = \"gh_actions\",\n  win = {\n    input = {\n      keys = {\n        [\"<a-b>\"] = { \"gh_browse\", mode = { \"n\", \"i\" } },\n        [\"<c-y>\"] = { \"gh_yank\", mode = { \"n\", \"i\" } },\n      },\n    },\n    list = {\n      keys = {\n        [\"y\"] = { \"gh_yank\", mode = { \"n\", \"x\" } },\n      },\n    },\n  },\n}\n\n---@class snacks.picker.gh.pr.Config: snacks.picker.gh.Config\n---@field state \"open\" | \"closed\" | \"merged\" | \"all\"\n---@field draft? boolean filter draft PRs\n---@field base? string filter by base branch\nM.gh_pr = {\n  title = \"  Pull Requests\",\n  finder = \"gh_pr\",\n  format = \"gh_format\",\n  preview = \"gh_preview\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n  supports_live = true,\n  live = true,\n  confirm = \"gh_actions\",\n  win = {\n    input = {\n      keys = {\n        [\"<a-b>\"] = { \"gh_browse\", mode = { \"n\", \"i\" } },\n        [\"<c-y>\"] = { \"gh_yank\", mode = { \"n\", \"i\" } },\n      },\n    },\n    list = {\n      keys = {\n        [\"y\"] = { \"gh_yank\", mode = { \"n\", \"x\" } },\n      },\n    },\n  },\n}\n\n---@class snacks.picker.gh.diff.Config: snacks.picker.Config\n---@field group? boolean group changes by file (when false, show individual hunks)\n---@field pr number number PR number to diff against\n---@field repo? string GitHub repository (owner/repo). Defaults to current git repo\nM.gh_diff = {\n  title = \"  Pull Request Diff\",\n  group = true,\n  finder = \"gh_diff\",\n  format = \"git_status\",\n  preview = \"gh_preview_diff\",\n  win = {\n    preview = {\n      keys = {\n        [\"a\"] = { \"gh_comment\", mode = { \"n\", \"x\" } },\n        [\"<cr>\"] = { \"gh_actions\", mode = { \"n\", \"x\" } },\n      },\n    },\n  },\n}\n\n---@class snacks.picker.gh.reactions.Config: snacks.picker.Config\n---@field number number issue or PR number\n---@field repo string GitHub repository (owner/repo). Defaults to current git repo\nM.gh_reactions = {\n  layout = { preset = \"select\", layout = { max_width = 50 } },\n  title = \"  Reactions\",\n  main = { current = true },\n  group = true,\n  finder = \"gh_reactions\",\n  format = \"gh_format_reaction\",\n}\n\n---@class snacks.picker.gh.labels.Config: snacks.picker.Config\n---@field number number issue or PR number\n---@field repo string GitHub repository (owner/repo). Defaults to current git repo\nM.gh_labels = {\n  layout = { preset = \"select\", layout = { max_width = 50 } },\n  title = \"  Labels\",\n  main = { current = true },\n  group = true,\n  finder = \"gh_labels\",\n  format = \"gh_format_label\",\n}\n\n---@class snacks.picker.gh.actions.Config: snacks.picker.Config\n---@field number number issue or PR number\n---@field repo string GitHub repository (owner/repo). Defaults to current git repo\n---@field type \"issue\" | \"pr\"\n---@field item? snacks.picker.gh.Item\nM.gh_actions = {\n  layout = { preset = \"select\", layout = { max_width = 50 } },\n  title = \"  Actions\",\n  main = { current = true },\n  finder = \"gh_get_actions\",\n  format = \"gh_format_action\",\n  confirm = \"gh_perform_action\",\n}\n\n--- Git arguments are use like this:\n---  * git [<cmd_args>] <cmd> [<args>]\n---  * cmd may be `status`, `log`, `diff`, etc.\n---@class snacks.picker.git.Config: snacks.picker.Config,snacks.picker.git.Args\n---@field args? string[] additional arguments to pass to `git`\n---@field cmd_args? string[] additional arguments to pass to the `git <cmd>``\n\n---@class snacks.picker.git.branches.Config: snacks.picker.git.Config\n---@field all? boolean show all branches, including remote\nM.git_branches = {\n  all = false,\n  finder = \"git_branches\",\n  format = \"git_branch\",\n  preview = \"git_log\",\n  confirm = \"git_checkout\",\n  win = {\n    input = {\n      keys = {\n        [\"<c-a>\"] = { \"git_branch_add\", mode = { \"n\", \"i\" } },\n        [\"<c-x>\"] = { \"git_branch_del\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n  ---@param picker snacks.Picker\n  on_show = function(picker)\n    for i, item in ipairs(picker:items()) do\n      if item.current then\n        picker.list:view(i)\n        Snacks.picker.actions.list_scroll_center(picker)\n        break\n      end\n    end\n  end,\n}\n\n-- Find git files\n---@class snacks.picker.git.files.Config: snacks.picker.git.Config\n---@field untracked? boolean show untracked files\n---@field submodules? boolean show submodule files\nM.git_files = {\n  finder = \"git_files\",\n  show_empty = true,\n  format = \"file\",\n  untracked = false,\n  submodules = false,\n}\n\n-- Grep in git files\n---@class snacks.picker.git.grep.Config: snacks.picker.git.Config\n---@field untracked? boolean search in untracked files\n---@field submodules? boolean search in submodule files\n---@field need_search? boolean require a search pattern\n---@field pathspec? string|string[] pathspec pattern(s)\n---@field ignorecase? boolean ignore case\nM.git_grep = {\n  finder = \"git_grep\",\n  format = \"file\",\n  untracked = false,\n  need_search = true,\n  submodules = false,\n  show_empty = true,\n  supports_live = true,\n  live = true,\n}\n\n-- Git log\n---@class snacks.picker.git.log.Config: snacks.picker.git.Config\n---@field follow? boolean track file history across renames\n---@field current_file? boolean show current file log\n---@field current_line? boolean show current line log\n---@field author? string filter commits by author\nM.git_log = {\n  finder = \"git_log\",\n  format = \"git_log\",\n  preview = \"git_show\",\n  confirm = \"git_checkout\",\n  supports_live = true,\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n\n---@type snacks.picker.git.log.Config\nM.git_log_file = {\n  finder = \"git_log\",\n  format = \"git_log\",\n  preview = \"git_show\",\n  current_file = true,\n  follow = true,\n  confirm = \"git_checkout\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n\n---@type snacks.picker.git.log.Config\nM.git_log_line = {\n  finder = \"git_log\",\n  format = \"git_log\",\n  preview = \"git_show\",\n  current_line = true,\n  follow = true,\n  confirm = \"git_checkout\",\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n\nM.git_stash = {\n  finder = \"git_stash\",\n  format = \"git_stash\",\n  preview = \"git_stash\",\n  confirm = \"git_stash_apply\",\n}\n\n---@class snacks.picker.git.status.Config: snacks.picker.git.Config\n---@field ignored? boolean show ignored files\nM.git_status = {\n  finder = \"git_status\",\n  format = \"git_status\",\n  preview = \"git_status\",\n  win = {\n    input = {\n      keys = {\n        [\"<Tab>\"] = { \"git_stage\", mode = { \"n\", \"i\" } },\n        [\"<c-r>\"] = { \"git_restore\", mode = { \"n\", \"i\" }, nowait = true },\n      },\n    },\n  },\n}\n\n---@class snacks.picker.git.diff.Config: snacks.picker.git.Config\n---@field group? boolean group changes by file (when false, show individual hunks)\n---@field staged? boolean show staged changes\n---@field base? string base commit/branch/tag to diff against (default: HEAD)\nM.git_diff = {\n  group = false,\n  finder = \"git_diff\",\n  format = \"git_status\",\n  preview = \"diff\",\n  matcher = { sort_empty = true },\n  sort = { fields = { \"score:desc\", \"file\", \"idx\" } },\n  win = {\n    input = {\n      keys = {\n        [\"<Tab>\"] = { \"git_stage\", mode = { \"n\", \"i\" } },\n        [\"<c-r>\"] = { \"git_restore\", mode = { \"n\", \"i\" }, nowait = true },\n      },\n    },\n  },\n}\n\n---@class snacks.picker.grep.Config: snacks.picker.proc.Config\n---@field cmd? string\n---@field hidden? boolean show hidden files\n---@field ignored? boolean show ignored files\n---@field dirs? string[] directories to search\n---@field follow? boolean follow symlinks\n---@field glob? string|string[] glob file pattern(s)\n---@field ft? string|string[] ripgrep file type(s). See `rg --type-list`\n---@field regex? boolean use regex search pattern (defaults to `true`)\n---@field buffers? boolean search in open buffers\n---@field need_search? boolean require a search pattern\n---@field exclude? string[] exclude patterns\n---@field args? string[] additional arguments\n---@field rtp? boolean search in runtimepath\nM.grep = {\n  finder = \"grep\",\n  regex = true,\n  format = \"file\",\n  show_empty = true,\n  live = true, -- live grep by default\n  supports_live = true,\n}\n\n---@type snacks.picker.grep.Config|{}\nM.grep_buffers = {\n  finder = \"grep\",\n  format = \"file\",\n  live = true,\n  buffers = true,\n  need_search = false,\n  supports_live = true,\n}\n\n---@type snacks.picker.grep.Config|{}\nM.grep_word = {\n  finder = \"grep\",\n  regex = false,\n  args = { \"--word-regexp\" },\n  format = \"file\",\n  search = function(picker)\n    return picker:word()\n  end,\n  live = false,\n  supports_live = true,\n}\n\n-- Neovim help tags\n---@class snacks.picker.help.Config: snacks.picker.Config\n---@field lang? string[] defaults to `vim.opt.helplang`\nM.help = {\n  finder = \"help\",\n  format = \"text\",\n  previewers = {\n    file = { ft = \"help\" },\n  },\n  win = { preview = { minimal = true } },\n  confirm = \"help\",\n}\n\nM.highlights = {\n  finder = \"vim_highlights\",\n  format = \"hl\",\n  preview = \"preview\",\n  confirm = \"close\",\n}\n\n---@class snacks.picker.icons.Config: snacks.picker.Config\n---@field icon_sources? string[] list of sources to use\n--- Custom icon sources can be added here. The key is the source name,\n--- and the value is the file path or URL to load icons from.\n--- The file should be a JSON array of:\n--- `{[1]:string, [2]:string}|{icon:string, name:string, category:string}`\n--- The format is compatible with https://github.com/nvim-telescope/telescope-symbols.nvim\n---@field custom_sources? table<string,string> additional icon sources `table<source,file|url>`\nM.icons = {\n  main = { current = true },\n  finder = \"icons\",\n  format = \"icon\",\n  layout = { preset = \"vscode\" },\n  confirm = \"put\",\n}\n\nM.jumps = {\n  finder = \"vim_jumps\",\n  format = \"file\",\n  main = { current = true },\n}\n\n---@class snacks.picker.keymaps.Config: snacks.picker.Config\n---@field global? boolean show global keymaps\n---@field local? boolean show buffer keymaps\n---@field plugs? boolean show plugin keymaps\n---@field modes? string[]\nM.keymaps = {\n  finder = \"vim_keymaps\",\n  format = \"keymap\",\n  preview = \"preview\",\n  global = true,\n  plugs = false,\n  [\"local\"] = true,\n  modes = { \"n\", \"v\", \"x\", \"s\", \"o\", \"i\", \"c\", \"t\" },\n  ---@param picker snacks.Picker\n  confirm = function(picker, item)\n    picker:norm(function()\n      if item then\n        picker:close()\n        vim.api.nvim_input(item.item.lhs)\n      end\n    end)\n  end,\n  actions = {\n    toggle_global = function(picker)\n      picker.opts.global = not picker.opts.global\n      picker:find()\n    end,\n    toggle_buffer = function(picker)\n      picker.opts[\"local\"] = not picker.opts[\"local\"]\n      picker:find()\n    end,\n  },\n  win = {\n    input = {\n      keys = {\n        [\"<a-g>\"] = { \"toggle_global\", mode = { \"n\", \"i\" }, desc = \"Toggle Global Keymaps\" },\n        [\"<a-b>\"] = { \"toggle_buffer\", mode = { \"n\", \"i\" }, desc = \"Toggle Buffer Keymaps\" },\n      },\n    },\n  },\n}\n\n--- Search for a lazy.nvim plugin spec\nM.lazy = {\n  finder = \"lazy_spec\",\n  pattern = \"'\",\n}\n\n-- Search lines in the current buffer\n---@class snacks.picker.lines.Config: snacks.picker.Config\n---@field buf? number\nM.lines = {\n  finder = \"lines\",\n  format = \"lines\",\n  layout = {\n    preview = \"main\",\n    preset = \"ivy\",\n  },\n  jump = { match = true },\n  -- allow any window to be used as the main window\n  main = { current = true },\n  ---@param picker snacks.Picker\n  on_show = function(picker)\n    local cursor = vim.api.nvim_win_get_cursor(picker.main)\n    local info = vim.api.nvim_win_call(picker.main, vim.fn.winsaveview)\n    picker.list:view(cursor[1], info.topline)\n    picker:show_preview()\n  end,\n  sort = { fields = { \"score:desc\", \"idx\" } },\n}\n\n-- Loclist\n---@type snacks.picker.qf.Config\nM.loclist = {\n  finder = \"qf\",\n  format = \"file\",\n  qf_win = 0,\n  main = { current = true },\n}\n\n---@class snacks.picker.lsp.Config: snacks.picker.Config\n---@field include_current? boolean default false\n---@field unique_lines? boolean include only locations with unique lines\n---@field filter? snacks.picker.filter.Config\n\n---@class snacks.picker.lsp.config.Config: snacks.picker.Config\n---@field installed? boolean only show installed servers\n---@field configured? boolean only show configured servers (setup with lspconfig)\n---@field attached? boolean|number only show attached servers. When `number`, show only servers attached to that buffer (can be 0)\nM.lsp_config = {\n  finder = \"lsp.config#find\",\n  format = \"lsp.config#format\",\n  preview = \"lsp.config#preview\",\n  confirm = \"close\",\n  sort = { fields = { \"score:desc\", \"attached_buf\", \"attached\", \"enabled\", \"installed\", \"name\" } },\n  matcher = { sort_empty = true },\n}\n\n-- LSP declarations\n---@type snacks.picker.lsp.Config\nM.lsp_declarations = {\n  finder = \"lsp_declarations\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n\n-- LSP definitions\n---@type snacks.picker.lsp.Config\nM.lsp_definitions = {\n  finder = \"lsp_definitions\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n\n-- LSP implementations\n---@type snacks.picker.lsp.Config\nM.lsp_implementations = {\n  finder = \"lsp_implementations\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n\n-- LSP incoming calls\n---@type snacks.picker.lsp.Config\nM.lsp_incoming_calls = {\n  finder = \"lsp_incoming_calls\",\n  format = \"lsp_symbol\",\n  include_current = false,\n  workspace = true, -- this ensures the file is included in the formatter\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n\n-- LSP outgoing calls\n---@type snacks.picker.lsp.Config\nM.lsp_outgoing_calls = {\n  finder = \"lsp_outgoing_calls\",\n  format = \"lsp_symbol\",\n  include_current = false,\n  workspace = true, -- this ensures the file is included in the formatter\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n\n-- LSP references\n---@class snacks.picker.lsp.references.Config: snacks.picker.lsp.Config\n---@field include_declaration? boolean default true\nM.lsp_references = {\n  finder = \"lsp_references\",\n  format = \"file\",\n  include_declaration = true,\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n\n-- LSP document symbols\n---@class snacks.picker.lsp.symbols.Config: snacks.picker.Config\n---@field tree? boolean show symbol tree\n---@field keep_parents? boolean keep parent symbols when filtering\n---@field filter table<string, string[]|boolean>? symbol kind filter\n---@field workspace? boolean show workspace symbols\nM.lsp_symbols = {\n  finder = \"lsp_symbols\",\n  format = \"lsp_symbol\",\n  tree = true,\n  filter = {\n    default = {\n      \"Class\",\n      \"Constructor\",\n      \"Enum\",\n      \"Field\",\n      \"Function\",\n      \"Interface\",\n      \"Method\",\n      \"Module\",\n      \"Namespace\",\n      \"Package\",\n      \"Property\",\n      \"Struct\",\n      \"Trait\",\n    },\n    -- set to `true` to include all symbols\n    markdown = true,\n    help = true,\n    -- you can specify a different filter for each filetype\n    lua = {\n      \"Class\",\n      \"Constructor\",\n      \"Enum\",\n      \"Field\",\n      \"Function\",\n      \"Interface\",\n      \"Method\",\n      \"Module\",\n      \"Namespace\",\n      -- \"Package\", -- remove package since luals uses it for control flow structures\n      \"Property\",\n      \"Struct\",\n      \"Trait\",\n    },\n  },\n}\n\n---@type snacks.picker.lsp.symbols.Config\nM.lsp_workspace_symbols = vim.tbl_extend(\"force\", {}, M.lsp_symbols, {\n  workspace = true,\n  tree = false,\n  supports_live = true,\n  live = true, -- live by default\n})\n\n-- LSP type definitions\n---@type snacks.picker.lsp.Config\nM.lsp_type_definitions = {\n  finder = \"lsp_type_definitions\",\n  format = \"file\",\n  include_current = false,\n  auto_confirm = true,\n  jump = { tagstack = true, reuse_win = true },\n}\n\nM.man = {\n  finder = \"system_man\",\n  format = \"man\",\n  preview = \"man\",\n  confirm = function(picker, item, action)\n    ---@cast action snacks.picker.jump.Action\n    picker:close()\n    if item then\n      vim.schedule(function()\n        local cmd = \"Man \" .. item.ref ---@type string\n        if action.cmd == \"vsplit\" then\n          cmd = \"vert \" .. cmd\n        elseif action.cmd == \"tab\" then\n          cmd = \"tab \" .. cmd\n        end\n        vim.cmd(cmd)\n      end)\n    end\n  end,\n}\n\n---@class snacks.picker.marks.Config: snacks.picker.Config\n---@field global? boolean show global marks\n---@field local? boolean show buffer marks\nM.marks = {\n  finder = \"vim_marks\",\n  format = \"file\",\n  global = true,\n  [\"local\"] = true,\n  win = {\n    input = {\n      keys = {\n        [\"<c-x>\"] = { \"mark_delete\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n}\n\n---@class snacks.picker.notifications.Config: snacks.picker.Config\n---@field filter? snacks.notifier.level|fun(notif: snacks.notifier.Notif): boolean\nM.notifications = {\n  finder = \"snacks_notifier\",\n  format = \"notification\",\n  preview = \"preview\",\n  formatters = { severity = { level = true } },\n  confirm = \"close\",\n}\n\n-- List all available sources\nM.pickers = {\n  finder = \"meta_pickers\",\n  format = \"text\",\n  confirm = function(picker, item)\n    picker:close()\n    if item then\n      vim.schedule(function()\n        Snacks.picker(item.text)\n      end)\n    end\n  end,\n}\n\nM.picker_actions = {\n  finder = \"meta_actions\",\n  format = \"text\",\n}\nM.picker_format = {\n  finder = \"meta_format\",\n  format = \"text\",\n}\nM.picker_layouts = {\n  finder = \"meta_layouts\",\n  format = \"text\",\n  on_change = function(picker, item)\n    vim.schedule(function()\n      picker:set_layout(item.text)\n    end)\n  end,\n}\nM.picker_preview = {\n  finder = \"meta_preview\",\n  format = \"text\",\n}\n\n-- Open recent projects\n---@class snacks.picker.projects.Config: snacks.picker.Config\n---@field filter? snacks.picker.filter.Config\n---@field dev? string|string[] top-level directories containing multiple projects (sub-folders that contains a root pattern)\n---@field projects? string[] list of project directories\n---@field patterns? string[] patterns to detect project root directories\n---@field recent? boolean include project directories of recent files\n---@field max_depth? number maximum depth to search in dev directories (default: 2)\nM.projects = {\n  finder = \"recent_projects\",\n  format = \"file\",\n  dev = { \"~/dev\", \"~/projects\" },\n  confirm = \"load_session\",\n  patterns = { \".git\", \"_darcs\", \".hg\", \".bzr\", \".svn\", \"package.json\", \"Makefile\" },\n  recent = true,\n  matcher = {\n    frecency = true, -- use frecency boosting\n    sort_empty = true, -- sort even when the filter is empty\n    cwd_bonus = false,\n  },\n  sort = { fields = { \"score:desc\", \"idx\" } },\n  win = {\n    preview = { minimal = true },\n    input = {\n      keys = {\n        -- every action will always first change the cwd of the current tabpage to the project\n        [\"<c-e>\"] = { { \"tcd\", \"picker_explorer\" }, mode = { \"n\", \"i\" } },\n        [\"<c-f>\"] = { { \"tcd\", \"picker_files\" }, mode = { \"n\", \"i\" } },\n        [\"<c-g>\"] = { { \"tcd\", \"picker_grep\" }, mode = { \"n\", \"i\" } },\n        [\"<c-r>\"] = { { \"tcd\", \"picker_recent\" }, mode = { \"n\", \"i\" }, nowait = true },\n        [\"<c-w>\"] = { { \"tcd\" }, mode = { \"n\", \"i\" } },\n        [\"<c-t>\"] = {\n          function(picker)\n            vim.cmd(\"tabnew\")\n            Snacks.notify(\"New tab opened\")\n            picker:close()\n            Snacks.picker.projects()\n          end,\n          mode = { \"n\", \"i\" },\n        },\n      },\n    },\n  },\n}\n\n-- Quickfix list\n---@type snacks.picker.qf.Config\nM.qflist = {\n  finder = \"qf\",\n  format = \"file\",\n}\n\n-- Find recent files\n---@class snacks.picker.recent.Config: snacks.picker.Config\n---@field filter? snacks.picker.filter.Config\nM.recent = {\n  finder = \"recent_files\",\n  format = \"file\",\n  filter = {\n    paths = {\n      [vim.fn.stdpath(\"data\")] = false,\n      [vim.fn.stdpath(\"cache\")] = false,\n      [vim.fn.stdpath(\"state\")] = false,\n    },\n  },\n}\n\n-- Neovim registers\nM.registers = {\n  finder = \"vim_registers\",\n  main = { current = true },\n  format = \"register\",\n  preview = \"preview\",\n  confirm = { \"copy\", \"close\" },\n}\n\n-- Special picker that resumes the last picker\nM.resume = {}\n\n-- Open or create scratch buffers\nM.scratch = {\n  finder = \"scratch\",\n  format = \"scratch_format\",\n  confirm = \"scratch_open\",\n  win = {\n    input = {\n      keys = {\n        [\"<c-x>\"] = { \"scratch_delete\", mode = { \"n\", \"i\" } },\n        [\"<c-n>\"] = { \"scratch_new\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n}\n\n-- Neovim search history\n---@type snacks.picker.history.Config\nM.search_history = {\n  finder = \"vim_history\",\n  name = \"search\",\n  format = \"text\",\n  preview = \"none\",\n  main = { current = true },\n  layout = { preset = \"vscode\" },\n  confirm = \"search\",\n  formatters = { text = { ft = \"regex\" } },\n}\n\n--- Config used by `vim.ui.select`.\n--- Not meant to be used directly.\n---@class snacks.picker.select.Config: snacks.picker.Config\n---@field kinds? table<string, snacks.picker.Config|{}> custom snacks picker configs for specific `vim.ui.select` kinds\nM.select = {\n  items = {}, -- these are set dynamically\n  main = { current = true },\n  layout = { preset = \"select\" },\n}\n\n---@class snacks.picker.smart.Config: snacks.picker.Config\n---@field finders? string[] list of finders to use\n---@field filter? snacks.picker.filter.Config\nM.smart = {\n  multi = { \"buffers\", \"recent\", \"files\" },\n  format = \"file\", -- use `file` format for all sources\n  matcher = {\n    cwd_bonus = true, -- boost cwd matches\n    frecency = true, -- use frecency boosting\n    sort_empty = true, -- sort even when the filter is empty\n  },\n  transform = \"unique_file\",\n}\n\nM.spelling = {\n  finder = \"vim_spelling\",\n  format = \"text\",\n  main = { current = true },\n  layout = { preset = \"vscode\" },\n  confirm = \"item_action\",\n}\n\n-- Search tags file\n---@class snacks.picker.tags.Config: snacks.picker.Config\nM.tags = {\n  workspace = true, -- search tags in the workspace\n  finder = \"vim_tags\",\n  format = \"lsp_symbol\",\n}\n\n---@class snacks.picker.treesitter.Config: snacks.picker.Config\n---@field filter table<string, string[]|boolean>? symbol kind filter\n---@field tree? boolean show symbol tree\nM.treesitter = {\n  finder = \"treesitter_symbols\",\n  format = \"lsp_symbol\",\n  tree = true,\n  filter = {\n    default = {\n      \"Class\",\n      \"Enum\",\n      \"Field\",\n      \"Function\",\n      \"Method\",\n      \"Module\",\n      \"Namespace\",\n      \"Struct\",\n      \"Trait\",\n    },\n    -- set to `true` to include all symbols\n    markdown = true,\n    help = true,\n  },\n}\n\n---@class snacks.picker.undo.Config: snacks.picker.Config\n---@field diff? vim.text.diff.Opts\nM.undo = {\n  finder = \"vim_undo\",\n  format = \"undo\",\n  preview = \"diff\",\n  confirm = \"item_action\",\n  win = {\n    preview = { wo = { number = false, relativenumber = false, signcolumn = \"no\" } },\n    input = {\n      keys = {\n        [\"<c-y>\"] = { \"yank_add\", mode = { \"n\", \"i\" } },\n        [\"<c-s-y>\"] = { \"yank_del\", mode = { \"n\", \"i\" } },\n      },\n    },\n  },\n  actions = {\n    yank_add = { action = \"yank\", field = \"added_lines\" },\n    yank_del = { action = \"yank\", field = \"removed_lines\" },\n  },\n  icons = { tree = { last = \"┌╴\" } }, -- the tree is upside down\n  diff = {\n    ctxlen = 4,\n    ignore_cr_at_eol = true,\n    ignore_whitespace_change_at_eol = true,\n    indent_heuristic = true,\n  },\n}\n\n-- Open a project from zoxide\nM.zoxide = {\n  finder = \"files_zoxide\",\n  format = \"file\",\n  confirm = \"load_session\",\n  win = {\n    preview = {\n      minimal = true,\n    },\n  },\n}\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/_health.lua",
    "content": "local M = {}\n\n---@private\nfunction M.health()\n  local config = Snacks.picker.config.get()\n  if Snacks.config.get(\"picker\", {}).enabled and config.ui_select then\n    if vim.ui.select == Snacks.picker.select then\n      Snacks.health.ok(\"`vim.ui.select` is set to `Snacks.picker.select`\")\n    else\n      Snacks.health.error(\"`vim.ui.select` is not set to `Snacks.picker.select`\")\n    end\n  else\n    Snacks.health.warn(\"`vim.ui.select` for `Snacks.picker` is not enabled\")\n  end\n\n  Snacks.health.has_lang(\"regex\")\n\n  Snacks.health.have_tool(\"git\")\n\n  local have_rg = Snacks.health.have_tool(\"rg\")\n  if not have_rg then\n    Snacks.health.error(\"'rg' is required for `Snacks.picker.grep()`\")\n  else\n    Snacks.health.ok(\"`Snacks.picker.grep()` is available\")\n  end\n\n  local have_fd, version_fd = Snacks.health.have_tool({\n    { cmd = { \"fd\", \"fdfind\" }, version = \"v8.4\" },\n  })\n  local have_find = have_fd\n    or (jit.os:find(\"Windows\") == nil and Snacks.health.have_tool({\n      { cmd = \"find\", version = false },\n    }))\n  if have_rg or have_fd or have_find then\n    Snacks.health.ok(\"`Snacks.picker.files()` is available\")\n  else\n    Snacks.health.error(\"'rg', 'fd' or 'find' is required for `Snacks.picker.files()`\")\n  end\n\n  if not have_fd or not version_fd then\n    Snacks.health.error(\"'fd' `v8.4` is required for searching with `Snacks.picker.explorer()`\")\n  else\n    Snacks.health.ok(\"`Snacks.picker.explorer()` is available\")\n  end\n\n  local ok = pcall(require, \"snacks.picker.util.db\")\n  if ok then\n    Snacks.health.ok(\"`SQLite3` is available\")\n  else\n    Snacks.health.warn(\"`SQLite3` is not available. Frecency and history will be stored in a file instead.\")\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/actions.lua",
    "content": "local M = {}\n\n---@alias snacks.picker.Action.fn fun(self: snacks.Picker, item?:snacks.picker.Item, action?:snacks.picker.Action):(boolean|string?)\n---@alias snacks.picker.Action.spec.one string|snacks.picker.Action|snacks.picker.Action.fn|{action?:snacks.picker.Action.spec.one}\n---@alias snacks.picker.Action.spec snacks.picker.Action.spec.one|snacks.picker.Action.spec.one[]\n\n---@class snacks.picker.Action\n---@field action snacks.picker.Action.fn\n---@field desc? string\n---@field name? string\n---@field [string] any additional fields\n\n---@param picker snacks.Picker\nfunction M.get(picker)\n  local ref = picker:ref()\n  ---@type table<string, snacks.win.Action>\n  local ret = {}\n  setmetatable(ret, {\n    ---@param t table<string, snacks.win.Action>\n    ---@param k string\n    __index = function(t, k)\n      if type(k) ~= \"string\" then\n        return\n      end\n      t[k] = M.wrap(k, ref, k) or false\n      return rawget(t, k)\n    end,\n  })\n  return ret\nend\n\n---@param action snacks.picker.Action.spec\n---@param ref snacks.Picker.ref\n---@param name? string\nfunction M.wrap(action, ref, name)\n  local picker = ref()\n  if not picker then\n    return\n  end\n  action = M.resolve(action, picker, name)\n  action.name = name\n  ---@type snacks.win.Action\n  return {\n    name = name,\n    action = function()\n      local p = ref()\n      if not p then\n        return\n      end\n      return action.action(p, p:current(), action)\n    end,\n    desc = action.desc,\n  }\nend\n\n---@param action snacks.picker.Action.spec\n---@param picker snacks.Picker\n---@param name? string\n---@param stack? string[]\n---@return snacks.picker.Action\nfunction M.resolve(action, picker, name, stack)\n  stack = stack or {}\n  if not action then\n    assert(name, \"Missing action without name\")\n    local fn, desc = picker.input.win[name], name\n    return {\n      action = function(p)\n        if not fn then\n          return name\n        end\n        fn(p.input.win)\n      end,\n      desc = desc,\n    }\n  elseif type(action) == \"string\" then\n    if vim.tbl_contains(stack, action) then\n      if action == \"confirm\" or name == \"confirm\" then\n        action = \"jump\"\n      else\n        Snacks.notify.error(\"Circular action reference for `\" .. action .. \"`:\\n- \" .. table.concat(stack, \"\\n- \"))\n        return {}\n      end\n    end\n    stack[#stack + 1] = action\n    return M.resolve(Snacks.picker.config.action(picker, action), picker, action, stack)\n  elseif type(action) == \"table\" and svim.islist(action) then\n    local actions = vim.tbl_map(function(a)\n      return M.resolve(a, picker, nil, stack)\n    end, action)\n    ---@type snacks.picker.Action\n    return {\n      action = function(p, i)\n        for _, a in ipairs(actions) do\n          a.action(p, i, a)\n        end\n      end,\n      desc = table.concat(\n        vim.tbl_map(function(a)\n          return a.desc or a.name or \"unknown\"\n        end, actions),\n        \", \"\n      ),\n    }\n  elseif type(action) == \"table\" then\n    if type(action.action) ~= \"function\" then\n      action = vim.deepcopy(action)\n      action.action = M.resolve(action.action, picker, nil, stack).action\n    end\n    ---@cast action snacks.picker.Action\n    action.desc = action.desc or name or nil\n    return action\n  end\n  assert(type(action) == \"function\", \"Invalid action\")\n  return {\n    action = action,\n    desc = name or nil,\n  }\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/filter.lua",
    "content": "---@class snacks.picker.Filter\n---@field pattern string Pattern used to filter items by the matcher\n---@field search string Initial search string used by finders\n---@field buf? number\n---@field file? string\n---@field cwd string\n---@field all boolean\n---@field paths {path:string, want:boolean}[]\n---@field opts snacks.picker.filter.Config\n---@field current_buf number\n---@field current_win number\n---@field source_id? number\n---@field meta table<string, any>\nlocal M = {}\nM.__index = M\n\n---@param picker snacks.Picker\nfunction M.new(picker)\n  local opts = picker.opts ---@type snacks.picker.Config|{filter?:snacks.picker.filter.Config}\n  local self = setmetatable({}, M)\n  self.current_buf = vim.api.nvim_get_current_buf()\n  self.current_win = vim.api.nvim_get_current_win()\n  self.meta = {}\n  local function gets(v)\n    return type(v) == \"function\" and v(picker) or v or \"\" --[[@as string]]\n  end\n  self.pattern = gets(opts.pattern)\n  self.search = gets(opts.search)\n  self:init(opts)\n  return self\nend\n\n---@param opts snacks.picker.Config|{filter?:snacks.picker.filter.Config}\nfunction M:init(opts)\n  self.opts = opts.filter or {}\n  self.all = not self.opts or not (self.opts.cwd or self.opts.buf or self.opts.paths or self.opts.filter)\n  self.paths = {}\n  local cwd = self.opts and self.opts.cwd\n  self.cwd = type(cwd) == \"string\" and cwd or opts.cwd or vim.fn.getcwd(0)\n  self.cwd = svim.fs.normalize(self.cwd --[[@as string]], { _fast = true })\n  if not self.all and self.opts then\n    self.buf = self.opts.buf == true and 0 or self.opts.buf --[[@as number?]]\n    self.buf = self.buf == 0 and M.current_buf or self.buf\n    self.file = self.buf and svim.fs.normalize(vim.api.nvim_buf_get_name(self.buf), { _fast = true }) or nil\n    for path, want in pairs(self.opts.paths or {}) do\n      table.insert(self.paths, { path = svim.fs.normalize(path), want = want })\n    end\n  end\n  return self\nend\n\nfunction M:is_empty()\n  return vim.trim(self.pattern) == \"\" and vim.trim(self.search) == \"\"\nend\n\n---@param cwd string\nfunction M:set_cwd(cwd)\n  self.cwd = cwd\n  self.cwd = svim.fs.normalize(self.cwd --[[@as string]], { _fast = true })\nend\n\n---@param opts? {trim?:boolean}\n---@return snacks.picker.Filter\nfunction M:clone(opts)\n  local ret = setmetatable({}, {\n    __index = self,\n    __call = M.filter,\n  })\n  if opts and opts.trim then\n    ret.pattern = vim.trim(self.pattern)\n    ret.search = vim.trim(self.search)\n  else\n    ret.pattern = self.pattern\n    ret.search = self.search\n  end\n  return ret\nend\n\n---@param item snacks.picker.finder.Item):boolean\nfunction M:match(item)\n  if self.all then\n    return true\n  end\n  if self.opts.filter and not self.opts.filter(item, self) then\n    return false\n  end\n  if self.buf and (item.buf ~= self.buf) and (item.file ~= self.file) then\n    return false\n  end\n  if not (self.opts.cwd or self.opts.paths) then\n    return true\n  end\n  local path = Snacks.picker.util.path(item)\n  if not path then\n    return false\n  end\n  if self.opts.cwd and path ~= self.cwd and not path:find(self.cwd .. \"/\", 1, true) then\n    return false\n  end\n  if self.opts.paths then\n    for _, p in ipairs(self.paths) do\n      if (path:sub(1, #p.path) == p.path) ~= p.want then\n        return false\n      end\n    end\n  end\n  return true\nend\n\n---@param items snacks.picker.finder.Item[]\nfunction M:filter(items)\n  if self.all then\n    return items\n  end\n  local ret = {} ---@type snacks.picker.finder.Item[]\n  for _, item in ipairs(items) do\n    if self:match(item) then\n      table.insert(ret, item)\n    end\n  end\n  return ret\nend\n\nM.__call = M.filter\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/finder.lua",
    "content": "local Async = require(\"snacks.picker.util.async\")\n\n---@class snacks.picker.Finder\n---@field _find snacks.picker.finder\n---@field task snacks.picker.Async\n---@field items snacks.picker.finder.Item[]\n---@field filter? snacks.picker.Filter\nlocal M = {}\nM.__index = M\n\n---@class snacks.picker.finder.ctx\n---@field picker snacks.Picker\n---@field filter snacks.picker.Filter\n---@field async snacks.picker.Async\n---@field meta table<string, any>\n---@field _opts? snacks.picker.Config\nlocal Ctx = {}\nCtx.__index = Ctx\n\n---@param picker snacks.Picker\n---@param filter snacks.picker.Filter\nfunction Ctx.new(picker, filter)\n  local notified = false\n  local self = setmetatable({}, Ctx)\n  self.picker = picker\n  self.filter = filter\n  self.meta = {}\n  self.async = setmetatable({}, {\n    __index = function()\n      if not notified then\n        notified = true\n        Snacks.notify.warn(\"You can only use the `async` object in async functions\")\n      end\n    end,\n  })\n  return self\nend\n\n---@param opts? snacks.picker.Config\n---@return snacks.picker.finder.ctx\nfunction Ctx:clone(opts)\n  return setmetatable({ _opts = opts }, { __index = self })\nend\n\n---@generic T: snacks.picker.Config\n---@param opts T\n---@return T\nfunction Ctx:opts(opts)\n  self._opts = setmetatable(opts or {}, { __index = self._opts or self.picker.opts })\n  return self._opts\nend\n\nfunction Ctx:cwd()\n  return self.filter.cwd\nend\n\nfunction Ctx:git_root()\n  return Snacks.git.get_root(self:cwd()) or self:cwd()\nend\n\n---@alias snacks.picker.finder.async fun(cb:async fun(item:snacks.picker.finder.Item))\n---@alias snacks.picker.finder.result snacks.picker.finder.Item[] | snacks.picker.finder.async\n---@alias snacks.picker.finder fun(opts: snacks.picker.Config, ctx: snacks.picker.finder.ctx): snacks.picker.finder.result\n---@alias snacks.picker.finder.multi (snacks.picker.finder|string)[]\n\nlocal YIELD_FIND = 1 -- ms\n\n---@param find snacks.picker.finder\nfunction M.new(find)\n  local self = setmetatable({}, M)\n  self._find = find\n  self.task = Async.nop()\n  self.items = {}\n  return self\nend\n\nfunction M:running()\n  return self.task:running()\nend\n\nfunction M:abort()\n  self.task:abort()\nend\n\nfunction M:count()\n  return #self.items\nend\n\nfunction M:close()\n  self.task:abort()\n  self.task = Async.nop()\n  self._find = function()\n    return {}\n  end\nend\n\n---@param picker snacks.Picker\nfunction M:ctx(picker)\n  return Ctx.new(picker, self.filter)\nend\n\n---@param filter snacks.picker.Filter\n---@return boolean changed\nfunction M:init(filter)\n  local ret = not (self.filter and (self.filter.search == filter.search and self.filter.source_id == filter.source_id))\n  self.filter = filter\n  return ret\nend\n\n---@param picker snacks.Picker\nfunction M:run(picker)\n  local default_score = require(\"snacks.picker.core.matcher\").DEFAULT_SCORE\n  self.task:abort()\n  self.items = {}\n  local yield ---@type fun()\n  local ctx = self:ctx(picker)\n  local finder = self._find(picker.opts, ctx)\n  local limit = (picker.opts.live and picker.opts.limit_live or picker.opts.limit) or math.huge\n\n  ---@param item snacks.picker.finder.Item\n  local function add(item)\n    item.idx, item.score = #self.items + 1, default_score\n    self.items[item.idx] = item\n  end\n\n  if picker.opts.transform then\n    local transform = Snacks.picker.config.transform(picker.opts)\n    ---@param item snacks.picker.finder.Item\n    function add(item)\n      local t = transform(item, ctx)\n      item = type(t) == \"table\" and t or item\n      if t ~= false then\n        item.idx, item.score = #self.items + 1, default_score\n        self.items[item.idx] = item\n      end\n    end\n  end\n\n  -- PERF: if finder is a table, we can skip the async part\n  if type(finder) == \"table\" then\n    local items = finder --[[@as snacks.picker.finder.Item[] ]]\n    for _, item in ipairs(items) do\n      add(item)\n    end\n    return\n  end\n\n  local running = true\n\n  collectgarbage(\"stop\") -- moar speed\n  ---@cast finder snacks.picker.finder.async\n  ---@diagnostic disable-next-line: await-in-sync\n  self.task = Async.new(function()\n    ctx.async = Async.running()\n    ---@async\n    finder(function(item)\n      if #self.items >= limit then\n        return self.task:abort()\n      end\n      if not running then\n        Snacks.debug.backtrace({\n          \"Finder yielded after done. This is a bug.\",\n          (\"- aborted: `%s`\"):format(self.task:aborted() or false),\n          \"\",\n          \"# Backtrace\",\n        }, {\n          level = vim.log.levels.ERROR,\n          title = \"Snacks Picker Finder\",\n        })\n        return\n      end\n      add(item)\n      picker.matcher.task:resume()\n      yield = yield or Async.yielder(YIELD_FIND)\n      yield()\n    end)\n  end):on(\"done\", function()\n    collectgarbage(\"restart\")\n    if not self.task:aborted() then\n      picker.matcher.task:resume()\n      picker:update()\n    end\n    running = false\n  end)\nend\n\n---@param finders snacks.picker.finder[]\n---@return snacks.picker.finder\nfunction M.multi(finders)\n  return function(opts, ctx)\n    local filter = ctx.filter\n    ---@type snacks.picker.finder.result[]\n    local results = {}\n    local need_async = false\n    for source_id, finder in ipairs(finders) do\n      if filter.source_id == nil or filter.source_id == source_id then\n        results[#results + 1] = finder(opts, ctx) or {}\n      else\n        results[#results + 1] = {}\n      end\n      need_async = need_async or type(results[#results]) == \"function\"\n    end\n\n    ---@async\n    ---@type snacks.picker.finder.async\n    local function collect(cb)\n      for source_id, find in ipairs(results) do\n        if type(find) == \"table\" then\n          for _, item in ipairs(find) do\n            item.source_id = source_id\n            cb(item)\n          end\n        else\n          ---@async\n          find(function(item)\n            item.source_id = source_id\n            cb(item)\n          end)\n        end\n      end\n    end\n\n    if need_async then\n      return collect\n    end\n\n    -- not async, so collect all items\n    local items = {} ---@type snacks.picker.finder.Item[]\n    collect(function(item)\n      items[#items + 1] = item\n    end)\n    return items\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/frecency.lua",
    "content": "-- Frecency based on exponential decay. Roughly based on:\n-- https://wiki.mozilla.org/User:Jesse/NewFrecency?title=User:Jesse/NewFrecency\n---@class snacks.picker.Frecency\n---@field now number\n---@field cache table<string, number>\nlocal M = {}\nM.__index = M\n\nlocal uv = vim.uv or vim.loop\nlocal store_file = vim.fn.stdpath(\"data\") .. \"/snacks/picker-frecency\"\n\nlocal HALF_LIFE = 30 * 24 * 3600 -- Half-life = 30 days (in seconds)\nlocal LAMBDA = math.log(2) / HALF_LIFE -- λ = ln(2) / half_life\nlocal SEED_VALUE = 1\nlocal DEFAULT_VALUE = 1\nlocal MAX_STORE_SIZE = 10000\n\n---@class snacks.picker.frecency.Store\n---@field set fun(self:snacks.picker.frecency.Store, key:string, value:number)\n---@field get fun(self:snacks.picker.frecency.Store, key:string):number\n---@field close fun(self:snacks.picker.frecency.Store)\n---@field get_all fun(self:snacks.picker.frecency.Store):table<string, number>\n\n-- Global store of frecency deadlinesl\n---@type snacks.picker.frecency.Store?\nM.store = nil\n\nfunction M.setup()\n  if\n    not pcall(function()\n      local db = require(\"snacks.picker.util.db\").new(store_file .. \".sqlite3\", \"number\")\n      M.store = db --[[@as snacks.picker.frecency.Store]]\n      -- Cleanup old entries\n      local cutoff = db:prepare(\"SELECT value FROM data ORDER BY value DESC LIMIT 1 OFFSET ?;\")\n      if cutoff:exec({ MAX_STORE_SIZE - 1 }) == 100 then -- 100 == SQLITE_ROW\n        db:prepare(\"DELETE FROM data WHERE value < ?;\"):exec({ cutoff:col(\"number\") })\n      end\n    end)\n  then\n    M.store = require(\"snacks.picker.util.kv\").new(store_file .. \".dat\", { max_size = MAX_STORE_SIZE }) --[[@as snacks.picker.frecency.Store]]\n  end\n\n  local group = vim.api.nvim_create_augroup(\"snacks_picker_frecency\", {})\n  vim.api.nvim_create_autocmd(\"ExitPre\", {\n    group = group,\n    callback = function()\n      if M.store then\n        M.store:close()\n        M.store = nil\n      end\n    end,\n  })\n  vim.api.nvim_create_autocmd({ \"BufWinEnter\" }, {\n    group = group,\n    callback = function(ev)\n      local current_win = vim.api.nvim_get_current_win()\n      if vim.api.nvim_win_get_config(current_win).relative ~= \"\" then\n        return\n      end\n      M.visit_buf(ev.buf)\n    end,\n  })\n  -- Visit existing buffers\n  for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n    M.visit_buf(buf)\n  end\nend\n\nfunction M.new()\n  local self = setmetatable({}, M)\n  self.now = os.time()\n  if not M.store then\n    M.setup()\n  end\n  self.cache = M.store:get_all()\n  return self\nend\n\n--- Convert from a current score s into a \"deadline date\"\n--- t = now() + (ln(s) / λ)\n---@param score number\nfunction M:to_deadline(score)\n  return self.now + (math.log(score) / LAMBDA)\nend\n\n--- Convert from a \"deadline date\" back into a current score\n--- s = e^(λ * (deadline - now))\nfunction M:to_score(deadline)\n  return math.exp(LAMBDA * (deadline - self.now))\nend\n\n--- Get the current frecency score for an item.\n--- If the item is not tracked yet, it will seed it\n--- based on the last used time or last modified time.\n---@param item snacks.picker.Item\n---@param opts? {seed?: boolean}\nfunction M:get(item, opts)\n  opts = opts or {}\n  local path = Snacks.picker.util.path(item)\n  if not path then\n    return 0\n  end\n  if item.dir then\n    -- frecency of a directory is the sum of frecencies of all files in it\n    local score = 0\n    local prefix = path .. \"/\"\n    for k, v in pairs(self.cache) do\n      if k:find(prefix, 1, true) == 1 then\n        score = score + self:to_score(v)\n      end\n    end\n    return score\n  end\n  local deadline = self.cache[path]\n  if not deadline then\n    return opts.seed ~= false and self:seed(item) or 0\n  end\n  return self:to_score(deadline)\nend\n\n---@param item snacks.picker.Item\n---@param value? number\nfunction M:seed(item, value)\n  -- only seed recent files or items with buffer info\n  if not (item.info or item.recent) then\n    return 0\n  end\n  local last_used = type(item.info) == \"table\" and item.info.lastused or nil\n  local path = Snacks.picker.util.path(item)\n  if not path then\n    return 0\n  end\n  if not last_used then\n    local stat = uv.fs_stat(path)\n    last_used = stat and stat.mtime.sec\n  end\n  if not last_used then\n    return 0\n  end\n  -- Calculate decayed single-visit score\n  local dt = self.now - last_used -- in seconds\n  return (value or SEED_VALUE) * math.exp(-LAMBDA * dt)\nend\n\n--- Add a \"visit\" to the item.\n--- If the item doesn't exist, it is created with initial score = `visit_value`.\n--- Otherwise, the new score is old_score + visit_value.\n---@param item snacks.picker.Item\n---@param value? number @the \"points\" to add (e.g. typed=2, clicked=1, etc.)\nfunction M:visit(item, value)\n  local path = Snacks.picker.util.path(item)\n  if not path then\n    return\n  end\n  local score = self:get(item, { seed = false }) + (value or DEFAULT_VALUE)\n  self.store:set(path, self:to_deadline(score))\nend\n\n---@param buf number\n---@param value? number\nfunction M.visit_buf(buf, value)\n  if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= \"\" or not vim.bo[buf].buflisted then\n    return\n  end\n  local file = vim.api.nvim_buf_get_name(buf)\n  if file == \"\" or not vim.uv.fs_stat(file) then\n    return\n  end\n  local frecency = M.new()\n  frecency:visit({\n    text = \"\",\n    idx = 1,\n    score = 0,\n    file = file,\n    buf = buf,\n    info = vim.fn.getbufinfo(buf)[1],\n  }, value)\n  return true\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/input.lua",
    "content": "---@class snacks.picker.input\n---@field win snacks.win\n---@field mode? string\n---@field totals string\n---@field picker snacks.Picker\n---@field filter snacks.picker.Filter\n---@field paused? boolean\nlocal M = {}\nM.__index = M\n\nlocal ns = vim.api.nvim_create_namespace(\"snacks.picker.input\")\n\n---@param picker snacks.Picker\nfunction M.new(picker)\n  local self = setmetatable({}, M)\n  self.totals = \"\"\n  self.picker = picker\n  self.filter = require(\"snacks.picker.core.filter\").new(picker)\n  self.mode = vim.fn.mode()\n  picker.matcher:init(self.filter.pattern)\n\n  self.win = Snacks.win(Snacks.win.resolve(picker.opts.win.input, {\n    show = false,\n    enter = false,\n    height = 1,\n    ft = \"regex\",\n    on_buf = function(win)\n      -- HACK: this is needed to prevent Neovim from stopping insert mode,\n      -- for any other picker input we are leaving.\n      local buf = vim.api.nvim_get_current_buf()\n      if buf ~= win.buf and vim.bo[buf].filetype == \"snacks_picker_input\" then\n        vim.bo[buf].buftype = \"nofile\"\n      end\n      vim.fn.prompt_setprompt(win.buf, \"\")\n      vim.bo[win.buf].modified = false\n      local text = picker.opts.live and self.filter.search or self.filter.pattern\n      vim.api.nvim_buf_set_lines(win.buf, 0, -1, false, { text })\n      vim.bo[win.buf].modified = false\n    end,\n    on_win = function()\n      self:highlights()\n    end,\n    bo = {\n      filetype = \"snacks_picker_input\",\n      buftype = \"prompt\",\n    },\n    wo = {\n      statuscolumn = self:statuscolumn(),\n      cursorline = false,\n      winhighlight = Snacks.picker.highlight.winhl(\"SnacksPickerInput\"),\n    },\n  }))\n\n  self.win:on(\"BufEnter\", function()\n    vim.bo[self.win.buf].buftype = \"prompt\"\n    if vim.fn.mode() == \"t\" then\n      vim.schedule(function()\n        vim.cmd(\"startinsert!\")\n      end)\n    else\n      vim.cmd(\"startinsert!\")\n    end\n  end, { buf = true })\n\n  local ref = Snacks.util.ref(self)\n  self.win:on(\n    { \"TextChangedI\", \"TextChanged\" },\n    Snacks.util.throttle(function()\n      local input = ref()\n      if not input or not input.win:valid() then\n        return\n      end\n      vim.bo[input.win.buf].modified = false\n      -- only one line\n      -- Can happen when someone pastes a multiline string\n      if vim.api.nvim_buf_line_count(input.win.buf) > 1 then\n        local line = vim.trim(input.win:text():gsub(\"\\n\", \" \"))\n        vim.api.nvim_buf_set_lines(input.win.buf, 0, -1, false, { line })\n        vim.api.nvim_win_set_cursor(input.win.win, { 1, #line + 1 })\n      end\n      vim.bo[input.win.buf].modified = false\n      local pattern = input:get()\n      if input.picker.opts.live then\n        input.filter.search = pattern\n      else\n        input.filter.pattern = pattern\n      end\n      vim.schedule(function()\n        input.picker:find({ refresh = false })\n      end)\n    end, { ms = picker.opts.live and 200 or 30 }),\n    { buf = true }\n  )\n  return self\nend\n\nfunction M:highlights()\n  local m = vim.fn.matchadd\n  vim.api.nvim_win_call(self.win.win, function()\n    m(\"@punctuation.delimiter\", \"\\\\v(^|\\\\s|:|\\\\!)\\\\zs['^]\")\n    m(\"@punctuation.delimiter\", \"\\\\v['$]\\\\ze(\\\\s|$)\")\n    m(\"DiagnosticWarn\", \"\\\\v(^|\\\\s|:)\\\\zs\\\\!\")\n    m(\"@keyword\", \"\\\\v(^|\\\\s)\\\\zs\\\\w+:\")\n    m(\"@operator\", \"\\\\v\\\\s\\\\zs\\\\|\\\\ze\\\\s\")\n  end)\nend\n\nfunction M:close()\n  self.win:destroy()\n  self.picker = nil -- needed for garbage collection of the picker\nend\n\nfunction M:stopinsert()\n  -- only stop insert mode if needed\n  if not vim.fn.mode():find(\"^i\") then\n    return\n  end\n  local buf = vim.api.nvim_get_current_buf()\n  -- if the other buffer is a prompt, then don't stop insert mode\n  if buf ~= self.win.buf and vim.bo[buf].buftype == \"prompt\" then\n    return\n  end\n  vim.cmd(\"stopinsert\")\nend\n\nfunction M:statuscolumn()\n  local parts = {} ---@type string[]\n  local function add(str, hl)\n    if str then\n      parts[#parts + 1] = (\"%%#%s#%s%%*\"):format(hl, str:gsub(\"%%\", \"%%\"))\n    end\n  end\n  local pattern = self.picker.opts.live and self.filter.pattern or self.filter.search\n  if pattern ~= \"\" then\n    if #pattern > 20 then\n      pattern = Snacks.picker.util.truncate(pattern, 20)\n    end\n    add(pattern, \"SnacksPickerInputSearch\")\n  end\n  add(self.picker.opts.prompt or \" \", \"SnacksPickerPrompt\")\n  return table.concat(parts, \" \")\nend\n\nfunction M:update()\n  if not self.win:valid() then\n    return\n  end\n  local sc = self:statuscolumn()\n  if self.win.opts.wo.statuscolumn ~= sc then\n    self.win.opts.wo.statuscolumn = sc\n    Snacks.util.wo(self.win.win, { statuscolumn = sc })\n  end\n  local line = {} ---@type snacks.picker.Highlight[]\n  if self.picker:is_active() and self.spinner ~= false then\n    line[#line + 1] = { Snacks.util.spinner(), \"SnacksPickerSpinner\" }\n    line[#line + 1] = { \" \" }\n  end\n  local selected = #self.picker.list.selected\n  if selected > 0 then\n    line[#line + 1] = { (\"(%d)\"):format(selected), \"SnacksPickerTotals\" }\n    line[#line + 1] = { \" \" }\n  end\n  line[#line + 1] = { (\"%d/%d\"):format(self.picker.list:count(), #self.picker.finder.items), \"SnacksPickerTotals\" }\n  line[#line + 1] = { \" \" }\n  local totals = table.concat(vim.tbl_map(function(v)\n    return v[1]\n  end, line))\n  if self.totals == totals then\n    return\n  end\n  self.totals = totals\n  vim.api.nvim_buf_set_extmark(self.win.buf, ns, 0, 0, {\n    id = 999,\n    virt_text = line,\n    virt_text_pos = \"right_align\",\n  })\nend\n\nfunction M:get()\n  return self.win:line()\nend\n\nfunction M:pause(ms)\n  self.paused = true\n  vim.defer_fn(function()\n    self.paused = false\n    self:update()\n  end, ms or 100)\nend\n\n---@param pattern? string\n---@param search? string\nfunction M:set(pattern, search)\n  self.filter.pattern = pattern or self.filter.pattern\n  self.filter.search = search or self.filter.search\n  vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, {\n    self.picker.opts.live and self.filter.search or self.filter.pattern,\n  })\n  vim.bo[self.win.buf].modified = false\n  vim.api.nvim_win_set_cursor(self.win.win, { 1, #self:get() + 1 })\n  self.totals = \"\"\n  self.win.opts.wo.statuscolumn = \"\"\n  self:update()\n  self.picker:update_titles()\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/list.lua",
    "content": "---@class snacks.picker.list\n---@field picker snacks.Picker\n---@field items snacks.picker.Item[]\n---@field top number\n---@field cursor number\n---@field win snacks.win\n---@field dirty boolean\n---@field state snacks.picker.list.State\n---@field paused boolean\n---@field topk snacks.picker.MinHeap\n---@field _current? snacks.picker.Item\n---@field did_preview? boolean\n---@field reverse? boolean\n---@field selected snacks.picker.Item[]\n---@field selected_map table<string, snacks.picker.Item>\n---@field matcher snacks.picker.Matcher matcher for formatting list items\n---@field matcher_regex snacks.picker.Matcher matcher for formatting list items\n---@field target? {cursor: number, top?: number}\n---@field visible snacks.picker.Item[]\nlocal M = {}\nM.__index = M\n\n---@class snacks.picker.list.State\n---@field height number\n---@field scrolloff number\n---@field scroll number\n---@field mousescroll number\n\nlocal ns = vim.api.nvim_create_namespace(\"snacks.picker.list\")\n\nlocal function minmax(value, min, max)\n  return math.max(min, math.min(value, max))\nend\n\nlocal SCROLL_WHEEL_UP = Snacks.util.keycode(\"<ScrollWheelUp>\")\nlocal SCROLL_WHEEL_DOWN = Snacks.util.keycode(\"<ScrollWheelDown>\")\n\n---@type table<number, snacks.picker.list>\nlocal lists = setmetatable({}, { __mode = \"v\" })\n\nlocal stats = { render = 0, render_full = 0 }\n\n-- track mouse scrolling\nvim.on_key(function(key, typed)\n  key = typed or key\n  if key ~= SCROLL_WHEEL_UP and key ~= SCROLL_WHEEL_DOWN then\n    return\n  end\n  local up = key == SCROLL_WHEEL_UP\n  local mouse_win = vim.fn.getmousepos().winid\n  local list = lists[mouse_win]\n  if list and list.win:valid() then\n    vim.schedule(function()\n      if list and list.win:valid() then\n        list:scroll((up and -1 or 1) * list.state.mousescroll)\n      end\n    end)\n    return \"\" -- on Neovim 0.11, this will prevent the default scroll\n  end\nend)\n\n---@param picker snacks.Picker\nfunction M.new(picker)\n  local self = setmetatable({}, M)\n  self.reverse = picker.resolved_layout.reverse\n  self.picker = picker\n  self.selected = {}\n  self.selected_map = {}\n  self.matcher = require(\"snacks.picker.core.matcher\").new(picker.opts.matcher)\n  self.matcher_regex = require(\"snacks.picker.core.matcher\").new({ regex = true })\n  local win_opts = Snacks.win.resolve(picker.opts.win.list, {\n    show = false,\n    enter = false,\n    on_win = function()\n      self:on_show()\n      lists[\n        self.win.win --[[@as number]]\n      ] = self\n    end,\n    minimal = true,\n    bo = { modifiable = false, filetype = \"snacks_picker_list\" },\n    wo = {\n      foldenable = false,\n      foldmethod = \"manual\",\n      cursorline = false,\n      winhighlight = Snacks.picker.highlight.winhl(\"SnacksPickerList\", { CursorLine = \"Visual\" }),\n      linebreak = true,\n      breakindent = true,\n    },\n  })\n  self.visible = {}\n  self.win = Snacks.win(win_opts)\n  self.top, self.cursor = 1, 1\n  self.items = {}\n  self.state = { height = 0, scrolloff = 0, scroll = 0, mousescroll = 1 }\n  self.dirty = true\n  self.topk = require(\"snacks.picker.util.minheap\").new({\n    capacity = 1000,\n    cmp = self.picker.sort,\n  })\n\n  self.win:on(\"CursorMoved\", function()\n    if not self.win:valid() then\n      return\n    end\n    local cursor = vim.api.nvim_win_get_cursor(self.win.win)\n    local view = vim.api.nvim_win_call(self.win.win, vim.fn.winsaveview)\n    local row = cursor[1] - view.topline + 1\n    if cursor[1] ~= self:idx2row(self.cursor) then\n      local idx = self:row2idx(row)\n      self:_move(idx, true, true)\n    end\n  end, { buf = true })\n\n  self.win:on(\"VimResized\", function()\n    self.state.height = vim.api.nvim_win_get_height(self.win.win)\n    self.dirty = true\n    self:update()\n  end)\n\n  self.win:on(\"WinResized\", function()\n    if not self.win:win_valid() then\n      return\n    end\n    local height = vim.api.nvim_win_get_height(self.win.win)\n    if height == self.state.height then\n      return\n    end\n    self.state.height = height\n    self.dirty = true\n    self:update()\n  end)\n\n  -- reset topline. Only needed for Neovim < 0.11,\n  -- but won't hurt on newer versions\n  self.win:on(\"WinScrolled\", function()\n    for win in pairs(vim.v.event) do\n      if (tonumber(win) or -1) == self.win.win then\n        vim.api.nvim_win_call(self.win.win, function()\n          vim.fn.winrestview({ topline = 1 })\n        end)\n      end\n    end\n  end)\n\n  local focused = false\n  self.win:on({ \"WinEnter\", \"WinLeave\" }, function()\n    local f = vim.api.nvim_get_current_win() == self.win.win\n    if focused ~= f then\n      focused = f\n      self:update_cursorline()\n    end\n  end)\n\n  return self\nend\n\n--- View the list at the given cursor and top.\n--- These are the normalized values, so are unaffected by reverse.\n---@param cursor number\n---@param top? number\n---@param render? boolean\nfunction M:view(cursor, top, render)\n  if top then\n    self:_scroll(top, true, false)\n  end\n  self:_move(cursor, true, render)\n  if self.cursor < cursor then\n    self.target = { cursor = cursor, top = top }\n  else\n    self.target = nil\n  end\nend\n\n--- Sets the target cursor/top for the next render.\n--- Useful to keep the cursor/top, right before triggering a `find`.\n--- If an existing target is set, it will be kept, unless `opts.force` is set.\n---@param cursor? number\n---@param top? number\n---@param opts? {force?: boolean}\nfunction M:set_target(cursor, top, opts)\n  if self.target and not (opts and opts.force) then\n    return\n  end\n  self.target = { cursor = cursor or self.cursor, top = top or self.top }\nend\n\n---@param idx number\nfunction M:idx2row(idx)\n  local ret = idx - self.top + 1\n  if not self.reverse then\n    return ret\n  end\n  return self.state.height - ret + 1\nend\n\n---@param row number\nfunction M:row2idx(row)\n  local ret = row + self.top - 1\n  if not self.reverse then\n    return ret\n  end\n  return self.state.height - ret + 1\nend\n\nfunction M:on_show()\n  self.state.scrolloff = vim.wo[self.win.win].scrolloff\n  self.state.scroll = vim.wo[self.win.win].scroll\n  self.state.height = vim.api.nvim_win_get_height(self.win.win)\n  self.state.mousescroll = tonumber(vim.o.mousescroll:match(\"ver:(%d+)\")) or 1\n  Snacks.util.wo(self.win.win, { scrolloff = 0 })\n  self.dirty = true\n  self:update_cursorline()\n  self:update({ force = true })\nend\n\nfunction M:count()\n  return #self.items\nend\n\nfunction M:close()\n  self.win:destroy()\n  self.picker = nil\n  for w, l in pairs(lists) do\n    if l == self then\n      lists[w] = nil\n    end\n  end\n  -- Keep all items so actions can be performed on them,\n  -- even when the picker closed\nend\n\nfunction M:scrolloff()\n  local scrolloff = math.min(self.state.scrolloff, math.floor((self:height() - 1) / 2))\n  local offset = math.min(self.cursor, self:count() - self.cursor)\n  return offset > scrolloff and scrolloff or 0\nend\n\n---@param to number\n---@param absolute? boolean\n---@param render? boolean\nfunction M:_scroll(to, absolute, render)\n  local old_top = self.top\n  self.top = absolute and to or self.top + to\n  local maxtop = self:count() - self:height() + 1\n  self.top = minmax(self.top, 1, maxtop)\n  if self.top == maxtop or self.top == 1 then\n    self.cursor = absolute and to or self.cursor + to\n  end\n  local scrolloff = self:scrolloff()\n  self.cursor = minmax(self.cursor, self.top + scrolloff, self.top + self:height() - 1 - scrolloff)\n  self.dirty = self.dirty or self.top ~= old_top\n  if render ~= false then\n    self:render()\n  end\nend\n\n---@param to number\n---@param absolute? boolean\n---@param render? boolean\nfunction M:scroll(to, absolute, render)\n  if self.reverse then\n    to = absolute and (self:count() - to + 1) or -1 * to\n  end\n  self:_scroll(to, absolute, render)\nend\n\n---@param to number\n---@param absolute? boolean\n---@param render? boolean\nfunction M:_move(to, absolute, render)\n  local old_top = self.top\n  local height = self:height()\n  if height <= 1 then\n    self.cursor, self.top = 1, 1\n  else\n    self.cursor = absolute and to or self.cursor + to\n    if self.picker.resolved_layout.cycle then\n      self.cursor = (self.cursor - 1) % self:count() + 1\n    end\n    self.cursor = minmax(self.cursor, 1, self:count())\n    local scrolloff = self:scrolloff()\n    self.top = minmax(self.top, self.cursor - self:height() + scrolloff + 1, self.cursor - scrolloff)\n  end\n  self.dirty = self.dirty or self.top ~= old_top\n  if render ~= false then\n    self:render()\n  end\nend\n\n---@param to number\n---@param absolute? boolean\n---@param render? boolean\nfunction M:move(to, absolute, render)\n  if self.reverse then\n    to = absolute and (self:count() - to + 1) or -1 * to\n  end\n  self:_move(to, absolute, render)\nend\n\nfunction M:clear()\n  self.topk:clear()\n  self.top, self.cursor = 1, 1\n  self.items = {}\n  self.dirty = true\n  if next(self.items) == nil then\n    return\n  end\n  self:update()\nend\n\nfunction M:pause(ms)\n  self.paused = true\n  vim.defer_fn(function()\n    self:unpause()\n  end, ms)\nend\n\n---@param item snacks.picker.Item\n---@param sort? boolean\nfunction M:add(item, sort)\n  local idx = #self.items + 1\n  self.items[idx] = item\n  -- if the visible items are less than the height, then we need to render\n  self.dirty = self.dirty or #self.visible < (self.state.height or 50)\n  if sort ~= false then\n    local added, prev = self.topk:add(item)\n    if added then\n      -- check if item is before the last visible item\n      if not self.dirty and #self.visible > 0 then\n        self.dirty = self.topk.cmp(item, self.visible[#self.visible])\n      end\n      item.match_topk = item.match_tick\n      if prev then\n        -- replace with previous item, since new item is now in topk\n        self.items[idx] = prev\n        prev.match_topk = nil\n      end\n    end\n  end\nend\n\n---@return snacks.picker.Item?\nfunction M:current()\n  return self:get(self.cursor)\nend\n\n--- Returns the item at the given sorted index.\n--- Item will be taken from topk if available, otherwise from items.\n--- In case the matcher is running, the item will be taken from the finder.\n---@param idx number\n---@return snacks.picker.Item?\nfunction M:get(idx)\n  return self.topk:get(idx) or self.items[idx]\nend\n\nfunction M:height()\n  return math.min(self.state.height, self:count())\nend\n\n---@param opts? {force?: boolean}\nfunction M:update(opts)\n  if opts and opts.force then\n    self.dirty = true\n  end\n  if vim.in_fast_event() then\n    return vim.schedule(function()\n      self:update()\n    end)\n  end\n  if self.paused and #self.items < self.state.height then\n    return\n  end\n  self:render()\nend\n\n-- Toggle selection of current item\n---@param item? snacks.picker.Item\nfunction M:select(item)\n  if item == nil and vim.fn.mode():find(\"^[vV]\") and vim.api.nvim_get_current_buf() == self.win.buf then\n    -- stop visual mode\n    vim.cmd(\"normal! \" .. vim.fn.mode():sub(1, 1))\n    local from = vim.api.nvim_buf_get_mark(0, \"<\")\n    local to = vim.api.nvim_buf_get_mark(0, \">\")\n    for i = math.min(from[1], to[1]), math.max(from[1], to[1]) do\n      local it = self:get(self:row2idx(i))\n      if it then\n        self:select(it)\n      end\n    end\n    return\n  end\n  item = item or self:current()\n  if not item then\n    return\n  end\n  if self:unselect(item) then\n    return\n  end\n  local key = self:select_key(item)\n  self.selected_map[key] = item\n  table.insert(self.selected, item)\n  self.picker.input:update()\n  self.dirty = true\n  self:render()\nend\n\n---@param item? snacks.picker.Item\nfunction M:unselect(item)\n  item = item or self:current()\n  if not item then\n    return\n  end\n  local key = self:select_key(item)\n  if not self.selected_map[key] then\n    return\n  end\n  self.selected_map[key] = nil\n  self.selected = vim.tbl_filter(function(v)\n    return self:select_key(v) ~= key\n  end, self.selected)\n  self.picker.input:update()\n  self.dirty = true\n  self:render()\n  return true\nend\n\nfunction M:select_all()\n  self:set_selected(#self.selected == self:count() and {} or self.items)\nend\n\n---@param item snacks.picker.Item\n---@return string\nfunction M:select_key(item)\n  item._select_key = item._select_key\n    or Snacks.picker.util.text(item, { \"text\", \"file\", \"key\", \"id\", \"pos\", \"end_pos\" })\n  return item._select_key\nend\n\n---@param items? snacks.picker.Item[]\nfunction M:set_selected(items)\n  items = items or {}\n  self.selected = items\n  self.selected_map = {}\n  for _, item in ipairs(items) do\n    self.selected_map[self:select_key(item)] = item\n  end\n  self.picker.input:update()\n  self.dirty = true\n  self:update()\nend\n\n---@param item snacks.picker.Item\nfunction M:is_selected(item)\n  return self.selected_map[self:select_key(item)] ~= nil\nend\n\nfunction M:unpause()\n  if not self.paused then\n    return\n  end\n  self.paused = false\n  self:update()\nend\n\n---@param item snacks.picker.Item\nfunction M:format(item)\n  Snacks.picker.util.resolve(item)\n  -- Add selected and debug info\n  local prefix = {} ---@type snacks.picker.Highlight[]\n  if #self.selected > 0 or self.picker.opts.formatters.selected.show_always then\n    vim.list_extend(prefix, Snacks.picker.format.selected(item, self.picker))\n  else\n    prefix[#prefix + 1] = { \" \" }\n  end\n  if self.picker.opts.debug.scores then\n    vim.list_extend(prefix, Snacks.picker.format.debug(item, self.picker))\n  end\n  local text, extmarks = Snacks.picker.highlight.to_text(prefix)\n\n  -- Add the formatted item\n  local line = self.picker.format(item, self.picker)\n\n  line = Snacks.picker.highlight.resolve(line, vim.api.nvim_win_get_width(self.win.win))\n\n  while #line > 0 and type(line[#line][1]) == \"string\" and line[#line][1]:find(\"^%s*$\") do\n    table.remove(line)\n  end\n  local line_text, line_extmarks = Snacks.picker.highlight.to_text(line, { offset = #text })\n  vim.list_extend(extmarks, line_extmarks)\n  text = text .. line_text\n\n  -- Highlight match positions for field patterns\n  local fields = self.matcher:fields()\n  for _, extmark in ipairs(extmarks) do\n    if extmark.col and extmark.end_col and extmark.field and vim.tbl_contains(fields, extmark.field) then\n      local field = extmark.field --[[@as string]]\n      ---@type snacks.picker.Item\n      local it = {\n        idx = 1,\n        score = 0,\n        file = item.file,\n        text = \"\",\n      }\n      it[field] = text:sub(extmark.col + 1, extmark.end_col)\n      local positions = self.matcher:positions(it)\n      Snacks.picker.highlight.matches(extmarks, positions[field] or {}, extmark.col)\n    end\n  end\n\n  -- Highlight match positions for text\n  local it = { text = text:gsub(\"%s*$\", \"\"), idx = 1, score = 0, file = item.file }\n  local positions = self.matcher:positions(it).text or {}\n  if not item.positions then\n    vim.list_extend(positions, self.matcher_regex:positions(it).text or {})\n  end\n  Snacks.picker.highlight.matches(extmarks, positions)\n  return text, extmarks\nend\n\n---@param item snacks.picker.Item\n---@param row number\nfunction M:_render(item, row)\n  local text, extmarks = self:format(item)\n  text = text:gsub(\"\\n\", \" \")\n  vim.api.nvim_buf_set_lines(self.win.buf, row - 1, row, false, { text })\n  for _, extmark in ipairs(extmarks) do\n    local col = extmark.col\n    extmark.col = nil\n    extmark.row = nil\n    extmark.field = nil\n    local ok, err = pcall(vim.api.nvim_buf_set_extmark, self.win.buf, ns, row - 1, col, extmark)\n    if not ok and self.picker.opts.debug.extmarks then\n      Snacks.notify.error(\"Failed to set extmark.\\n\" .. err .. \"\\n```lua\\n\" .. vim.inspect(extmark) .. \"\\n```\")\n    end\n  end\nend\n\nfunction M:update_cursorline()\n  if self.win:win_valid() then\n    Snacks.util.wo(self.win.win, {\n      cursorline = self:count() > 0,\n      winhighlight = Snacks.util.winhl(vim.wo[self.win.win].winhighlight, {\n        CursorLine = self.picker:is_focused() and \"SnacksPickerListCursorLine\" or \"CursorLine\",\n      }),\n    })\n  end\nend\n\nfunction M:render()\n  if not self.win:valid() then\n    return\n  end\n  stats.render = stats.render + 1\n  if self.target then\n    self:view(self.target.cursor, self.target.top, false)\n    if not self.picker:is_active() then\n      self.target = nil\n    end\n  else\n    self:move(0, false, false)\n    self:scroll(0, false, false)\n  end\n\n  local redraw = false\n  if self.dirty then\n    stats.render_full = stats.render_full + 1\n    local height = self:height()\n    self.dirty = false\n    vim.api.nvim_win_call(self.win.win, function()\n      vim.fn.winrestview({ topline = 1, leftcol = 0 })\n    end)\n\n    vim.api.nvim_buf_clear_namespace(self.win.buf, ns, 0, -1)\n\n    vim.bo[self.win.buf].modifiable = true\n    local lines = vim.split(string.rep(\"\\n\", self.state.height), \"\\n\")\n    vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, lines)\n\n    -- matcher for highlighting should include the search filter\n    local pattern = vim.trim(self.picker.input.filter.pattern)\n    if self.matcher.pattern ~= pattern then\n      self.matcher:init(pattern)\n    end\n    local search = Snacks.picker.util.parse(vim.trim(self.picker.input.filter.search))\n    if self.matcher_regex.pattern ~= search then\n      self.matcher_regex:init(search)\n    end\n\n    self.visible = {}\n    -- render items\n    for i = self.top, math.min(self:count(), self.top + height - 1) do\n      local item = assert(self:get(i), \"item not found\")\n      self.visible[i - self.top + 1] = item\n      local row = self:idx2row(i)\n      self:_render(item, row)\n    end\n\n    vim.bo[self.win.buf].modifiable = false\n    redraw = true\n  end\n\n  -- Fix cursor and cursorline\n  self:update_cursorline()\n  local cursor = vim.api.nvim_win_get_cursor(self.win.win)\n  if cursor[1] ~= self:idx2row(self.cursor) then\n    vim.api.nvim_win_set_cursor(self.win.win, { self:idx2row(self.cursor), 0 })\n  end\n\n  -- force redraw if list changed\n  if redraw then\n    self.win:redraw()\n  end\n\n  if self.target then\n    return\n  end\n\n  -- check if current item changed\n  local current = self:current()\n  if self._current ~= current then\n    self._current = current\n    if not self.did_preview then\n      -- show first preview instantly\n      self.did_preview = true\n      self.picker:show_preview()\n    else\n      vim.schedule(function()\n        if self.picker then\n          self.picker:show_preview()\n        end\n      end)\n    end\n  end\nend\n\n-- vim.uv.new_timer():start(\n--   500,\n--   500,\n--   vim.schedule_wrap(function()\n--     Snacks.notify(vim.inspect(stats), { ft = \"lua\", id = \"list_stats\" })\n--   end)\n-- )\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/main.lua",
    "content": "---@class snacks.picker.Main\n---@field opts snacks.picker.main.Config\n---@field win number\nlocal M = {}\nM.__index = M\n\n---@class snacks.picker.main.Config\n---@field float? boolean main window can be a floating window (defaults to false)\n---@field file? boolean main window should be a file (defaults to true)\n---@field current? boolean main window should be the current window (defaults to false)\n\n---@param opts? snacks.picker.main.Config\nfunction M.new(opts)\n  opts = vim.tbl_extend(\"force\", {\n    float = false,\n    file = true,\n    current = false,\n  }, opts or {})\n  local self = setmetatable({}, M)\n  self.opts = opts\n  self:update()\n  return self\nend\n\nfunction M:get()\n  if not self.win or not vim.api.nvim_win_is_valid(self.win) then\n    self.win = self:find()\n  end\n  return self.win\nend\n\nfunction M:update()\n  self.win = self:find()\nend\n\n---@param win number\nfunction M:set(win)\n  self.win = win\nend\n\n---@param extra? number[]\nfunction M:find(extra)\n  local current = vim.api.nvim_get_current_win()\n  self.win = self.win or current\n  if self.opts.current then\n    return current\n  end\n  local prev = vim.fn.win_getid(vim.fn.winnr(\"#\"))\n  local non_float = 0\n  local wins = { current, self.win, prev }\n  local all = vim.api.nvim_tabpage_list_wins(0)\n  -- sort all by lastused of the win buffer\n  table.sort(all, function(a, b)\n    local ba = vim.api.nvim_win_get_buf(a)\n    local bb = vim.api.nvim_win_get_buf(b)\n    return vim.fn.getbufinfo(ba)[1].lastused > vim.fn.getbufinfo(bb)[1].lastused\n  end)\n  vim.list_extend(wins, all)\n  ---@param win number\n  wins = vim.tbl_filter(function(win)\n    -- exclude invalid windows\n    if win == 0 or not vim.api.nvim_win_is_valid(win) then\n      return false\n    end\n    local buf = vim.api.nvim_win_get_buf(win)\n    if vim.w[win].snacks_main or vim.b[buf].snacks_main then\n      return true\n    end\n    local win_config = vim.api.nvim_win_get_config(win)\n    local is_float = win_config.relative ~= \"\"\n    if not is_float then\n      non_float = win\n    end\n    if vim.w[win].snacks_layout then\n      return false\n    end\n    -- exclude non-file buffers\n    if self.opts.file and vim.bo[buf].buftype ~= \"\" then\n      return false\n    end\n    -- exclude floating windows and non-focusable windows\n    if is_float and (not self.opts.float or not win_config.focusable) then\n      return false\n    end\n    return true\n  end, wins)\n  return wins[1] or non_float\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/matcher.lua",
    "content": "local Async = require(\"snacks.picker.util.async\")\n\n---@class snacks.picker.Item\n---@field match_tick? number\n---@field match_topk? number\n\n---@class snacks.picker.matcher.Config\n---@field regex? boolean used internally for positions of sources that use regex\n---@field on_match? fun(matcher: snacks.picker.Matcher, item: snacks.picker.Item)\n---@field on_done? fun(matcher: snacks.picker.Matcher)\n---@field keep_parents? boolean\n---@field sort? boolean\n\n---@class snacks.picker.Matcher\n---@field opts snacks.picker.matcher.Config\n---@field mods snacks.picker.matcher.Mods[][]\n---@field one? snacks.picker.matcher.Mods\n---@field pattern string\n---@field tick number\n---@field task snacks.picker.Async\n---@field live? boolean\n---@field score snacks.picker.Score\n---@field sorting? boolean\n---@field file? {path: string, pos: snacks.picker.Pos}\n---@field cwd string\n---@field frecency? snacks.picker.Frecency\n---@field subset? boolean\nlocal M = {}\nM.__index = M\nM.DEFAULT_SCORE = 1000\nM.INVERSE_SCORE = 1000\nlocal BONUS_FRECENCY = 8\nlocal BONUS_CWD = 10\n\nlocal YIELD_MATCH = 1 -- ms\n\n---@class snacks.picker.matcher.Mods\n---@field pattern string\n---@field chars string[]\n---@field entropy number higher entropy is less likely to match\n---@field field? string\n---@field ignorecase? boolean\n---@field fuzzy? boolean\n---@field regex? boolean\n---@field word? boolean\n---@field exact_suffix? boolean\n---@field exact_prefix? boolean\n---@field inverse? boolean\n\n---@param opts? snacks.picker.matcher.Config|{}\nfunction M.new(opts)\n  local self = setmetatable({}, M)\n  self.opts = vim.tbl_deep_extend(\"force\", {\n    fuzzy = true,\n    smartcase = true,\n    ignorecase = true,\n  }, opts or {})\n  self.pattern = \"\"\n  self.task = Async.nop()\n  self.mods = {}\n  self.sorting = true\n  self.tick = 0\n  self.score = require(\"snacks.picker.core.score\").new(self.opts)\n  self.frecency = self.opts.frecency and require(\"snacks.picker.core.frecency\").new() or nil\n  return self\nend\n\nfunction M:empty()\n  return not next(self.mods)\nend\n\nfunction M:running()\n  return self.task:running()\nend\n\nfunction M:abort()\n  self.task:abort()\nend\n\nfunction M:close()\n  self:abort()\n  self.task = Async.nop()\nend\n\n---@param picker snacks.Picker\n---@param item snacks.picker.Item\nfunction M:on_match(picker, item)\n  if self.opts.on_match then\n    self.opts.on_match(self, item)\n  end\n\n  if not self.opts.keep_parents or item.score == 0 then\n    return\n  end\n\n  local parent = item.parent\n  item.child_match_only = false\n  while parent and not parent.root do\n    if parent.score == 0 or parent.match_tick ~= self.tick then\n      parent.score = 1\n      parent.child_match_only = true\n      parent.match_tick = self.tick\n      parent.match_topk = nil\n      picker.list:add(parent, self.sorting)\n    else\n      break\n    end\n    parent = parent.parent\n  end\nend\n\n---@param picker snacks.Picker\nfunction M:on_done(picker)\n  vim.schedule(function()\n    if self.opts.on_done then\n      self.opts.on_done(self)\n    end\n    if not self.opts.keep_parents or picker.closed then\n      return\n    end\n    for item, idx in picker:iter() do\n      if not item.child_match_only then\n        picker.list:view(idx)\n        return\n      end\n    end\n  end)\nend\n\n---@param picker snacks.Picker\nfunction M:run(picker)\n  self.task:abort()\n  picker.list:clear()\n\n  self.cwd = svim.fs.normalize(picker.opts.cwd or (vim.uv or vim.loop).cwd() or \".\")\n  self.sorting = self.opts.sort ~= false and (not self:empty() or picker.opts.matcher.sort_empty)\n\n  -- PERF: fast path for empty pattern\n  if not self.sorting and not picker.finder.task:running() and self:empty() then\n    picker.list.items = picker.finder.items\n    picker:update({ force = true })\n    self:on_done(picker)\n    return\n  end\n\n  ---@async\n  self.task = Async.new(function()\n    local yield = Async.yielder(YIELD_MATCH)\n\n    ---@async\n    ---@param item snacks.picker.Item\n    local function check(item)\n      if self:update(picker, item) then\n        picker.list:add(item, self.sorting)\n      end\n      yield()\n    end\n\n    local count = #picker.finder.items\n\n    -- process topk first\n    for i = 1, count do\n      local item = picker.finder.items[i]\n      if item.match_topk then\n        item.match_topk = nil\n        check(item)\n      else\n        item.match_topk = nil\n      end\n    end\n\n    -- process matches next\n    for i = 1, count do\n      local item = picker.finder.items[i]\n      if item.score > 0 and item.match_tick ~= self.tick then\n        check(item)\n      end\n    end\n\n    -- if pattern is a subset of the previous pattern, then\n    -- only process items that didn't match previously\n    if self.subset then\n      for i = 1, count do\n        local item = picker.finder.items[i]\n        if item.score == 0 and item.match_tick == self.tick - 1 then\n          item.match_tick = self.tick\n        end\n      end\n    end\n\n    -- then the rest\n    local idx = 0\n    repeat\n      while idx < #picker.finder.items do\n        idx = idx + 1\n        local item = picker.finder.items[idx]\n        if item.match_tick ~= self.tick then\n          check(item)\n        end\n      end\n\n      -- suspend till we have more items\n      if picker.finder.task:running() then\n        Async.suspend()\n      end\n    until idx >= #picker.finder.items and not picker.finder.task:running()\n\n    picker:update({ force = true })\n    self:on_done(picker)\n  end)\nend\n\n---@param pattern string\n---@return boolean changed\nfunction M:init(pattern)\n  pattern = vim.trim(pattern)\n  if pattern == self.pattern then\n    return false\n  end\n  self.tick = self.tick + 1\n  self.file = nil\n  self.mods = {}\n  self.subset = self.pattern ~= \"\" and pattern:find(self.pattern, 1, true) == 1 and not pattern:find(\"[^%s%w]\")\n  self.pattern = pattern\n  self:abort()\n  self.one = nil\n  if pattern == \"\" then\n    return true\n  end\n  if self.opts.regex then\n    self.mods = { { self:_prepare(pattern) } }\n  else\n    local is_or = false\n    for _, p in ipairs(vim.split(pattern, \" +\")) do\n      if p == \"|\" then\n        is_or = true\n      else\n        local mods = self:_prepare(p)\n        if mods.pattern ~= \"\" then\n          if is_or and #self.mods > 0 then\n            table.insert(self.mods[#self.mods], mods)\n          else\n            table.insert(self.mods, { mods })\n          end\n        end\n        is_or = false\n      end\n    end\n  end\n  for _, ors in ipairs(self.mods) do\n    -- sort by entropy, lower entropy is more likely to match\n    table.sort(ors, function(a, b)\n      return a.entropy < b.entropy\n    end)\n  end\n  -- sort by entropy, higher entropy is less likely to match\n  table.sort(self.mods, function(a, b)\n    return a[1].entropy > b[1].entropy\n  end)\n  if #self.mods == 1 and #self.mods[1] == 1 then\n    self.one = self.mods[1][1]\n  end\n  return true\nend\n\n---@param pattern string\n---@return snacks.picker.matcher.Mods\nfunction M:_prepare(pattern)\n  ---@type snacks.picker.matcher.Mods\n  local mods = { pattern = pattern, entropy = 0, chars = {} }\n\n  if self.opts.regex then\n    mods.regex = true\n  else\n    local file_patterns = {\n      \"^(.*[/\\\\].*):(%d*):(%d*)$\",\n      \"^(.*[/\\\\].*):(%d*)$\",\n      \"^(.+%.[a-z_]+):(%d*):(%d*)$\",\n      \"^(.+%.[a-z_]+):(%d*)$\",\n    }\n\n    for _, p in ipairs(file_patterns) do\n      local file, line, col = pattern:match(p)\n      if file then\n        mods.field = \"file\"\n        mods.pattern = file .. \"$\"\n        self.file = {\n          path = file,\n          pos = { tonumber(line) or 1, tonumber(col) or 0 },\n        }\n        break\n      end\n    end\n\n    -- minimum two chars for field pattern\n    local field, p = pattern:match(\"^([%w_][%w_]+):(.*)$\")\n    if field then\n      mods.field = field\n      mods.pattern = p\n    end\n    mods.ignorecase = self.opts.ignorecase\n    local is_lower = mods.pattern:lower() == mods.pattern\n    if self.opts.smartcase then\n      mods.ignorecase = is_lower\n    end\n    mods.fuzzy = self.opts.fuzzy\n    if not mods.fuzzy then\n      mods.entropy = mods.entropy + 10\n    end\n    if mods.pattern:sub(1, 1) == \"!\" then\n      mods.fuzzy, mods.inverse = false, true\n      mods.pattern = mods.pattern:sub(2)\n      mods.entropy = mods.entropy - 1\n    end\n    if mods.pattern:sub(1, 1) == \"'\" then\n      mods.fuzzy = false\n      mods.pattern = mods.pattern:sub(2)\n      mods.entropy = mods.entropy + 10\n      if mods.pattern:sub(-1, -1) == \"'\" then\n        mods.word = true\n        mods.pattern = mods.pattern:sub(1, -2)\n        mods.entropy = mods.entropy + 10\n      end\n    elseif mods.pattern:sub(1, 1) == \"^\" then\n      mods.fuzzy, mods.exact_prefix = false, true\n      mods.pattern = mods.pattern:sub(2)\n      mods.entropy = mods.entropy + 20\n    end\n    if mods.pattern:sub(-1, -1) == \"$\" then\n      mods.fuzzy = false\n      mods.exact_suffix = true\n      mods.pattern = mods.pattern:sub(1, -2)\n      mods.entropy = mods.entropy + 20\n    end\n    local rare_chars = #mods.pattern:gsub(\"[%w%s]\", \"\")\n    mods.entropy = mods.entropy + math.min(#mods.pattern, 20) + rare_chars * 2\n    if not mods.ignorecase and not is_lower then\n      mods.entropy = mods.entropy * 2\n    end\n    if mods.ignorecase then\n      mods.pattern = mods.pattern:lower()\n    end\n  end\n\n  for c = 1, #mods.pattern do\n    mods.chars[c] = mods.pattern:sub(c, c)\n  end\n  return mods\nend\n\n---@param picker snacks.Picker\n---@param item snacks.picker.Item\n---@return boolean matched\nfunction M:update(picker, item)\n  if item.match_pos then\n    item.pos = nil\n  end\n  local score = self:match(item)\n  item.match_tick, item.match_topk = self.tick, nil\n  if score ~= 0 then\n    if item.score_add then\n      score = score + item.score_add\n    end\n    if item.score_mul then\n      score = score * item.score_mul\n    end\n    if self.file and not item.pos then\n      item.pos = self.file.pos\n      item.match_pos = true\n    end\n    if item.file then\n      if self.frecency then\n        item.frecency = item.frecency or self.frecency:get(item)\n        score = score + (1 - 1 / (1 + item.frecency)) * BONUS_FRECENCY\n      end\n      if\n        self.opts.cwd_bonus\n        and (self.cwd == item.cwd or Snacks.picker.util.path(item):find(self.cwd, 1, true) == 1)\n      then\n        score = score + BONUS_CWD\n      end\n    end\n    item.score = score\n    self:on_match(picker, item)\n  else\n    item.score = 0\n  end\n  return score > 0\nend\n\n--- Matches an item and returns the score.\n--- Score is 0 if no match is found.\n---@param item snacks.picker.Item\nfunction M:match(item)\n  if self:empty() then\n    return M.DEFAULT_SCORE -- empty pattern matches everything\n  end\n  local score, s = 0, nil\n  -- fast path for single pattern\n  if self.one then\n    return self:_match(item, self.one) or 0\n  end\n  for _, any in ipairs(self.mods) do\n    -- fast path for single OR pattern\n    if #any == 1 then\n      s = self:_match(item, any[1])\n    else\n      for _, mods in ipairs(any) do\n        s = self:_match(item, mods)\n        if s then\n          break\n        end\n      end\n    end\n    if not s then\n      return 0\n    end\n    score = score + s\n  end\n  return score\nend\n\n--- Returns the fields that are used in the pattern.\n---@return string[]\nfunction M:fields()\n  local ret = {} ---@type table<string,boolean>\n  for _, any in ipairs(self.mods) do\n    for _, mods in ipairs(any) do\n      ret[mods.field or \"text\"] = true\n    end\n  end\n  return vim.tbl_keys(ret)\nend\n\n--- Returns the positions of the matched pattern in the item.\n--- All search patterns are combined with OR.\n---@param item snacks.picker.Item\nfunction M:positions(item)\n  local all = {} ---@type snacks.picker.matcher.Mods[]\n  local ret = {} ---@type table<string,number[]>\n  for _, any in ipairs(self.mods) do\n    vim.list_extend(all, any)\n  end\n  for _, mods in ipairs(all) do\n    local _, from, to, str = self:_match(item, mods)\n    if from and to and str then\n      local field = mods.field or \"text\"\n      ret[field] = ret[field] or {}\n      local pos = ret[field]\n      if mods.fuzzy then\n        vim.list_extend(pos, self:fuzzy_positions(str, mods.chars, from))\n      else\n        for c = from, to do\n          pos[#pos + 1] = c\n        end\n      end\n    end\n  end\n  return ret\nend\n\n--- Returns the column of the first position of the matched pattern in the item.\n---@param buf number\n---@param item snacks.picker.Item\n---@return snacks.picker.Pos?\nfunction M:bufpos(buf, item)\n  if not item.pos then\n    return\n  end\n  local line = vim.api.nvim_buf_get_lines(buf, item.pos[1] - 1, item.pos[1], false)[1] or \"\"\n  local positions = self:positions({ text = line, idx = 1, score = 0 }).text or {}\n  table.sort(positions)\n  return #positions > 0 and { item.pos[1], positions[1] - 1 } or nil\nend\n\n---@param str string\n---@param pattern string[]\n---@param from number\nfunction M:fuzzy_positions(str, pattern, from)\n  local ret = { from } ---@type number[]\n  for i = 2, #pattern do\n    ret[#ret + 1] = string.find(str, pattern[i], ret[#ret] + 1, true)\n  end\n  return ret\nend\n\n---@param str string\n---@param pattern string\n---@return number? score, number? from, number? to, string? str\nfunction M:regex(str, pattern)\n  local ok, re = pcall(vim.regex, pattern)\n  if not ok then\n    return\n  end\n  local from, to = re:match_str(str)\n  if from and to then\n    from = from + 1\n    return self.score:get(str, from, to), from, to, str\n  end\nend\n\n---@param item snacks.picker.Item\n---@param mods snacks.picker.matcher.Mods\n---@return number? score, number? from, number? to, string? str\nfunction M:_match(item, mods)\n  self.score.is_file = item.file ~= nil\n  local str = item.text\n\n  if mods.regex then\n    return self:regex(str, mods.pattern)\n  end\n\n  if mods.field then\n    if item[mods.field] == nil then\n      if mods.inverse then\n        return M.INVERSE_SCORE\n      end\n      return\n    end\n    str = tostring(item[mods.field])\n  end\n\n  local str_orig = str\n  str = mods.ignorecase and str:lower() or str\n  local from, to ---@type number?, number?\n  if mods.fuzzy then\n    return self:fuzzy(str, str_orig, mods.chars)\n  end\n  if mods.exact_prefix then\n    if str:sub(1, #mods.pattern) == mods.pattern then\n      from, to = 1, #mods.pattern\n    end\n  elseif mods.exact_suffix then\n    if str:sub(-#mods.pattern) == mods.pattern then\n      from, to = #str - #mods.pattern + 1, #str\n    end\n  else\n    from, to = str:find(mods.pattern, 1, true)\n    -- word match\n    while mods.word and from and to do\n      local bound_left = self.score:is_left_boundary(str, from)\n      local bound_right = self.score:is_right_boundary(str, to)\n      if bound_left and bound_right then\n        break\n      end\n      from, to = str:find(mods.pattern, to + 1, true)\n    end\n  end\n  if mods.inverse then\n    if not from then\n      return M.INVERSE_SCORE\n    end\n    return\n  end\n  if from then\n    ---@cast to number\n    return self.score:get(str_orig, from, to), from, to, str\n  end\nend\n\n---@param str string\n---@param str_orig string\n---@param pattern string[]\n---@param init? number\n---@return number? from, number? to\nfunction M:fuzzy_find(str, str_orig, pattern, init)\n  local from = string.find(str, pattern[1], init or 1, true)\n  if not from then\n    return\n  end\n  self.score:init(str_orig, from)\n  ---@type number?, number\n  local last, n = from, #pattern\n  for i = 2, n do\n    last = string.find(str, pattern[i], last + 1, true)\n    if last then\n      self.score:update(last)\n    else\n      return\n    end\n  end\n  return from, last\nend\n\n--- Does a forward scan followed by a backward scan for each end position,\n--- to find the best match.\n---@param str string\n---@param str_orig string\n---@param pattern string[]\n---@return number? score, number? from, number? to, string? str\nfunction M:fuzzy(str, str_orig, pattern)\n  local from, to = self:fuzzy_find(str, str_orig, pattern)\n  if not from then\n    return\n  end\n  ---@cast to number\n\n  local best_from, best_to, best_score = from, to, self.score.score\n  while from do\n    if self.score.score > best_score then\n      best_from, best_to, best_score = from, to, self.score.score\n    end\n    from, to = self:fuzzy_find(str, str_orig, pattern, from + 1)\n  end\n  return best_score, best_from, best_to, str\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/picker.lua",
    "content": "local Async = require(\"snacks.picker.util.async\")\nlocal Finder = require(\"snacks.picker.core.finder\")\n\nlocal uv = vim.uv or vim.loop\nAsync.BUDGET = 10\nlocal _id = 0\n\n---@alias snacks.Picker.ref (fun():snacks.Picker?)|{value?: snacks.Picker}\n\n---@class snacks.Picker\n---@field id number\n---@field opts snacks.picker.Config\n---@field init_opts? snacks.picker.Config\n---@field finder snacks.picker.Finder\n---@field format snacks.picker.format\n---@field input snacks.picker.input\n---@field layout snacks.layout\n---@field resolved_layout snacks.picker.layout.Config\n---@field list snacks.picker.list\n---@field matcher snacks.picker.Matcher\n---@field main number\n---@field _main snacks.picker.Main\n---@field preview snacks.picker.Preview\n---@field shown? boolean\n---@field sort snacks.picker.sort\n---@field updater uv.uv_timer_t\n---@field start_time number\n---@field title string\n---@field closed? boolean\n---@field history snacks.picker.History\n---@field visual? snacks.picker.Visual\nlocal M = {}\n\n--- Keep track of garbage collection\n---@type table<snacks.Picker,boolean>\nM._pickers = setmetatable({}, { __mode = \"k\" })\n--- These are active, so don't garbage collect them\n---@type table<snacks.Picker,boolean>\nM._active = {}\n\n---@alias snacks.picker.history.Record {pattern: string, search: string, live?: boolean}\n\nfunction M:__index(key)\n  if M[key] then\n    return M[key]\n  end\n  if key == \"main\" then\n    return self._main:get()\n  end\nend\n\nfunction M:__newindex(key, value)\n  if key == \"main\" then\n    self._main:set(value)\n  else\n    rawset(self, key, value)\n  end\nend\n\n---@param opts? {source?: string, tab?: boolean}\nfunction M.get(opts)\n  opts = opts or {}\n  local ret = {} ---@type snacks.Picker[]\n  for picker in pairs(M._active) do\n    local want = (not opts.source or picker.opts.source == opts.source)\n      and (opts.tab == false or picker:on_current_tab())\n      and not picker.closed\n    if want then\n      ret[#ret + 1] = picker\n    end\n  end\n  table.sort(ret, function(a, b)\n    return a.id < b.id\n  end)\n  return ret\nend\n\n---@hide\n---@param opts? snacks.picker.Config\n---@return snacks.Picker\nfunction M.new(opts)\n  ---@type snacks.Picker\n  local self = setmetatable({}, M)\n  _id = _id + 1\n  self.id = _id\n  self.init_opts = opts\n  self.opts = Snacks.picker.config.get(opts)\n\n  self.history = require(\"snacks.picker.util.history\").new(\"picker_\" .. (self.opts.source or \"custom\"), {\n    ---@param hist snacks.picker.history.Record\n    filter = function(hist)\n      if hist.pattern == \"\" and hist.search == \"\" then\n        return false\n      end\n      return true\n    end,\n  })\n\n  self:cleanup()\n\n  self.visual = Snacks.picker.util.visual()\n  self.start_time = uv.hrtime()\n  self._main = require(\"snacks.picker.core.main\").new(self.opts.main)\n  local actions = require(\"snacks.picker.core.actions\").get(self)\n  self.opts.win.input.actions = actions\n  self.opts.win.list.actions = actions\n  self.opts.win.preview.actions = actions\n\n  self.sort = Snacks.picker.config.sort(self.opts)\n\n  self.updater = assert(uv.new_timer())\n  self.matcher = require(\"snacks.picker.core.matcher\").new(self.opts.matcher)\n\n  self.finder = Finder.new(Snacks.picker.config.finder(self.opts.finder) or function()\n    return self.opts.items or {}\n  end)\n\n  self.format = Snacks.picker.config.format(self.opts)\n\n  M._pickers[self] = true\n  M._active[self] = true\n\n  local layout = Snacks.picker.config.layout(self.opts)\n  self.resolved_layout = layout\n  self.list = require(\"snacks.picker.core.list\").new(self)\n  self.input = require(\"snacks.picker.core.input\").new(self)\n  self.preview = require(\"snacks.picker.core.preview\").new(self)\n\n  self.title = self.opts.title or Snacks.picker.util.title(self.opts.source or \"search\")\n\n  self:init_layout(layout)\n\n  local ref = self:ref()\n  self._throttled_preview = Snacks.util.throttle(function()\n    local this = ref()\n    if this then\n      this:_show_preview()\n    end\n  end, { ms = 60, name = \"preview\" })\n\n  if not (opts and opts.find == false) then\n    self:find()\n  end\n  return self\nend\n\nfunction M:is_focused()\n  return self:current_win() ~= nil\nend\n\n---@return string? name, snacks.win? win\nfunction M:current_win()\n  local current = vim.api.nvim_get_current_win()\n  for w, win in pairs(self.layout.wins or {}) do\n    if win.win == current then\n      return w, win\n    end\n  end\nend\n\n--- Check if any remnants of previous pickers need to be cleaned up.\n--- Normally not needed.\n---@private\nfunction M:cleanup()\n  local picker_count = vim.tbl_count(M._pickers) - vim.tbl_count(M._active)\n  if picker_count > 0 then\n    -- clear items from previous pickers for garbage collection\n    for picker, _ in pairs(M._pickers) do\n      if not M._active[picker] then\n        picker.finder.items = {}\n        picker.list.items = {}\n        picker.list:clear()\n        picker.list.picker = nil\n      end\n    end\n  end\n\n  if self.opts.debug.leaks and picker_count > 0 then\n    collectgarbage(\"collect\")\n    picker_count = vim.tbl_count(M._pickers)\n    if picker_count > 0 then\n      local pickers = vim.tbl_keys(M._pickers) ---@type snacks.Picker[]\n      table.sort(pickers, function(a, b)\n        return a.id < b.id\n      end)\n      local lines = { (\"# ` %d ` active pickers:\"):format(picker_count) }\n      for _, picker in ipairs(pickers) do\n        lines[#lines + 1] = (\"- [%s]: **pattern**=%q, **search**=%q\"):format(\n          picker.opts.source or \"custom\",\n          picker.input.filter.pattern,\n          picker.input.filter.search\n        )\n      end\n      Snacks.notify.error(lines, { title = \"Snacks Picker\", id = \"snacks_picker_leaks\" })\n      Snacks.debug.metrics()\n    else\n      Snacks.notify(\n        \"Picker leaks cleared after `collectgarbage`\",\n        { title = \"Snacks Picker\", id = \"snacks_picker_leaks\" }\n      )\n    end\n  end\nend\n\nfunction M:on_current_tab()\n  return self.layout:valid() and self.layout.root:on_current_tab()\nend\n\n--- Execute the callback in normal mode.\n--- When still in insert mode, stop insert mode first,\n--- and then`vim.schedule` the callback.\n---@param cb fun()\nfunction M:norm(cb)\n  if vim.fn.mode():sub(1, 1) == \"i\" then\n    vim.cmd.stopinsert()\n    vim.schedule(cb)\n    return\n  end\n  cb()\n  return true\nend\n\n---@param layout? snacks.picker.layout.Config\n---@private\nfunction M:init_layout(layout)\n  layout = layout or Snacks.picker.config.layout(self.opts)\n  self.resolved_layout = vim.deepcopy(layout)\n  self.resolved_layout.cycle = self.resolved_layout.cycle == true\n  self.preview:update(self)\n  local opts = layout --[[@as snacks.layout.Config]]\n  local backdrop = nil\n  if self.preview.main then\n    backdrop = false\n  end\n  self.layout = Snacks.layout.new(vim.tbl_deep_extend(\"force\", opts, {\n    show = false,\n    win = {\n      wo = {\n        winhighlight = Snacks.picker.highlight.winhl(\"SnacksPicker\"),\n      },\n    },\n    wins = {\n      input = self.input.win,\n      list = self.list.win,\n      preview = self.preview.win,\n    },\n    hidden = layout.hidden,\n    on_close = function()\n      self:close()\n    end,\n    on_update = function()\n      self.preview:refresh(self)\n      self.input:update()\n      self.list:update({ force = true })\n      self:update_titles()\n    end,\n    on_update_pre = function()\n      self:update_titles()\n    end,\n    layout = {\n      backdrop = backdrop,\n    },\n  }))\n  self:attach()\n\n  -- apply box highlight groups\n  local boxwhl = Snacks.picker.highlight.winhl(\"SnacksPickerBox\")\n  for _, win in pairs(self.layout.box_wins) do\n    win.opts.wo.winhighlight = Snacks.util.winhl(boxwhl, win.opts.wo.winhighlight)\n  end\n  return layout\nend\n\n--- Attaches to the layout\n---@private\nfunction M:attach()\n  -- Check if we need to load another layout\n  self.layout.root:on(\"VimResized\", function()\n    vim.schedule(function()\n      self:set_layout(Snacks.picker.config.layout(self.opts))\n    end)\n  end)\n\n  -- close if we enter a window that is not part of the picker\n  local preview = false\n  self.layout.root:on(\"WinEnter\", function()\n    if vim.v.vim_did_enter == 0 then\n      return\n    end\n    if self.closed or Snacks.util.is_float() then\n      return\n    end\n    if self:is_focused() then\n      if preview then -- re-open preview when needed\n        self:toggle(\"preview\", { enable = true })\n        preview = false\n      end\n      return\n    end\n    -- close main preview when auto_close is disabled\n    if self.opts.auto_close == false then\n      if self.preview.main and self.preview.win:valid() then\n        self:toggle(\"preview\", { enable = false })\n        preview = true\n      end\n      return\n    end\n    -- close picker when we enter another window\n    vim.schedule(function()\n      self:close()\n    end)\n  end)\n\n  -- Check if we need to auto close any picker windows\n  self.layout.root:on(\"WinEnter\", function()\n    if not self:is_focused() then\n      return\n    end\n    local current = self:current_win()\n    for name, win in pairs(self.layout.wins) do\n      local auto_hide = vim.tbl_contains(self.resolved_layout.auto_hide or {}, name)\n      if name ~= current and auto_hide and win:valid() then\n        self:toggle(name, { enable = false })\n      end\n    end\n  end)\n\n  -- prevent entering the root window for split layouts\n  local left_picker = true -- left a picker window\n  local last_pwin ---@type number?\n  self.layout.root:on(\"WinLeave\", function()\n    left_picker = self:is_focused()\n  end)\n  self.layout.root:on(\"WinEnter\", function()\n    if self:is_focused() then\n      last_pwin = vim.api.nvim_get_current_win()\n    end\n  end)\n  self.layout.root:on(\"WinEnter\", function()\n    if left_picker then\n      local pos = self.layout.root.opts.position\n      local wincmds = { left = \"l\", right = \"h\", top = \"j\", bottom = \"k\" }\n      vim.cmd(\"wincmd \" .. wincmds[pos])\n    elseif last_pwin and vim.api.nvim_win_is_valid(last_pwin) then\n      vim.api.nvim_set_current_win(last_pwin)\n    else\n      self:focus()\n    end\n  end, { buf = true, nested = true })\nend\n\n--- Set the picker layout. Can be either the name of a preset layout\n--- or a custom layout configuration.\n---@param layout? string|snacks.picker.layout.Config\nfunction M:set_layout(layout)\n  layout = layout or Snacks.picker.config.layout(self.opts)\n  layout = type(layout) == \"string\" and Snacks.picker.config.layout(layout) or layout\n  ---@cast layout snacks.picker.layout.Config\n  layout.cycle = layout.cycle == true\n  if vim.deep_equal(layout, self.resolved_layout) then\n    -- no need to update\n    return\n  end\n  if self.list.reverse ~= layout.reverse then\n    Snacks.notify.warn(\n      \"Heads up! This layout changed the list order,\\nso `up` goes down and `down` goes up.\",\n      { title = \"Snacks Picker\", id = \"snacks_picker_layout_change\" }\n    )\n  end\n  self.list.reverse = layout.reverse\n  self.layout.opts.on_close = nil -- prevent closing the picker when changing layout\n  self.layout:close({ wins = false })\n  self:init_layout(layout)\n  self.layout:show()\nend\n\n-- Get the word under the cursor or the current visual selection\nfunction M:word()\n  return self.visual and self.visual.text or vim.fn.expand(\"<cword>\")\nend\n\n--- Update title templates\n---@hide\nfunction M:update_titles()\n  local data = {\n    source = self.title,\n    title = self.title,\n    live = self.opts.live and self.opts.icons.ui.live or \"\",\n    preview = vim.trim(self.preview.title or \"\"),\n  }\n  local toggles = {} ---@type snacks.picker.Text[]\n  for name, toggle in pairs(self.opts.toggles) do\n    if toggle then\n      toggle = type(toggle) == \"string\" and { icon = toggle } or toggle\n      toggle = toggle == true and { icon = name:sub(1, 1) } or toggle\n      toggle = toggle == false and { enabled = false } or toggle\n      local want = toggle.value\n      if toggle.value == nil then\n        want = true\n      end\n      ---@cast toggle snacks.picker.toggle\n      if toggle.enabled ~= false and self.opts[name] == want then\n        local hl = table.concat(vim.tbl_map(function(a)\n          return a:sub(1, 1):upper() .. a:sub(2)\n        end, vim.split(name, \"_\")))\n        toggles[#toggles + 1] = { \" \" .. toggle.icon .. \" \", \"SnacksPickerToggle\" .. hl }\n        toggles[#toggles + 1] = { \" \", \"FloatTitle\" }\n      end\n    end\n  end\n  local wins = { self.layout.root }\n  vim.list_extend(wins, vim.tbl_values(self.layout.wins))\n  vim.list_extend(wins, vim.tbl_values(self.layout.box_wins))\n  for _, win in pairs(wins) do\n    if win.opts.title then\n      local tpl = win.meta.title_tpl or win.opts.title\n      win.meta.title_tpl = tpl\n      tpl = type(tpl) == \"string\" and { { tpl, \"FloatTitle\" } } or tpl\n      ---@cast tpl snacks.picker.Text[]\n\n      local has_flags = false\n      local ret = {} ---@type snacks.picker.Text[]\n      for _, chunk in ipairs(tpl) do\n        local text = chunk[1]\n        if text:find(\"{flags}\", 1, true) then\n          text = text:gsub(\"{flags}\", \"\")\n          has_flags = true\n        end\n        text = vim.trim(Snacks.picker.util.tpl(text, data)):gsub(\"([%w%p])%s+\", \"%1 \")\n        if text ~= \"\" then\n          -- HACK: add extra space when last char is non word like an icon\n          text = text:sub(-1):match(\"[%w%p]\") and text or text .. \" \"\n          ret[#ret + 1] = { text, chunk[2] }\n        end\n      end\n      if #ret > 0 then\n        table.insert(ret, { \" \", \"FloatTitle\" })\n        table.insert(ret, 1, { \" \", \"FloatTitle\" })\n      end\n      if has_flags and #toggles > 0 then\n        vim.list_extend(ret, toggles)\n      end\n      win:set_title(ret)\n    end\n  end\nend\n\n--- Actual preview code\n---@hide\nfunction M:_show_preview()\n  if self.closed then\n    return\n  end\n  if self.opts.on_change then\n    self.opts.on_change(self, self:current())\n  end\n  if not (self.preview and self.preview.win:valid()) then\n    return\n  end\n  self.preview:show(self)\n  self:update_titles()\nend\n\n-- Throttled preview\nM._throttled_preview = M._show_preview\n\n-- Show the preview. Show instantly when no item is yet in the preview,\n-- otherwise throttle the preview.\nfunction M:show_preview()\n  if self.closed then\n    return\n  end\n  -- don't show preview when cursor is not on target\n  if self.list.target then\n    return\n  end\n  if not self.preview.item then\n    return self:_show_preview()\n  end\n  return self:_throttled_preview()\nend\n\n---@hide\nfunction M:show()\n  if self.shown or self.closed then\n    return\n  end\n  self.shown = true\n  self.layout:show()\n  if self.opts.focus ~= false and self.opts.enter ~= false then\n    self:focus()\n  end\n  if self.opts.on_show then\n    self.opts.on_show(self)\n  end\nend\n\n--- Focuses the given or configured window.\n--- Falls back to the first available window if the window is hidden.\n---@param win? \"input\"|\"list\"|\"preview\"\n---@param opts? {show?: boolean} when enable is true, the window will be shown if hidden\nfunction M:focus(win, opts)\n  opts = opts or {}\n  if win and opts.show and self.layout:is_hidden(win) then\n    return self:toggle(win, { enable = true, focus = true })\n  end\n  win = win or self.opts.focus or \"input\"\n  local ret ---@type snacks.win?\n  for _, name in ipairs({ \"input\", \"list\", \"preview\" }) do\n    local w = self.layout.wins[name]\n    if w and w:valid() and not self.layout:is_hidden(name) then\n      if name == win then\n        ret = w\n        break\n      end\n      ret = ret or w\n    end\n  end\n  if ret then\n    ret:focus()\n  end\nend\n\n--- Toggle the given window and optionally focus\n---@param win \"input\"|\"list\"|\"preview\"\n---@param opts? {enable?: boolean, focus?: boolean|string}\nfunction M:toggle(win, opts)\n  opts = opts or {}\n  self.layout:toggle(win, opts.enable, function(enabled)\n    -- called if changed and before updating the layout\n    local focus = opts.focus == true and win or opts.focus or self:current_win() --[[@as string]]\n    if not focus then\n      return\n    end\n    if not enabled then\n      -- make sure we don't lose focus when toggling off\n      self:focus(focus)\n    else\n      --- schedule to focus after the layout is updated\n      vim.schedule(function()\n        self:focus(focus)\n      end)\n    end\n  end)\nend\n\n---@param item snacks.picker.Item?\nfunction M:resolve(item)\n  if not item then\n    return\n  end\n  Snacks.picker.util.resolve(item)\n  Snacks.picker.util.resolve_loc(item)\n  return item\nend\n\n--- Returns an iterator over the filtered items in the picker.\n--- Items will be in sorted order.\n---@return fun():(snacks.picker.Item?, number?)\nfunction M:iter()\n  local i = 0\n  local n = self.list:count()\n  return function()\n    i = i + 1\n    if i <= n then\n      return self:resolve(self.list:get(i)), i\n    end\n  end\nend\n\n--- Get all filtered items in the picker.\nfunction M:items()\n  local ret = {} ---@type snacks.picker.Item[]\n  for item in self:iter() do\n    ret[#ret + 1] = item\n  end\n  return ret\nend\n\n--- Get the current item at the cursor\n---@param opts? {resolve?: boolean} default is `true`\nfunction M:current(opts)\n  opts = opts or {}\n  local ret = self.list:current()\n  if ret and opts.resolve ~= false then\n    ret = self:resolve(ret)\n  end\n  return ret\nend\n\n--- Returns the directory of the current item or the cwd.\n--- When the item is a directory, return item path,\n--- otherwise return the directory of the item.\nfunction M:dir()\n  local item = self:current()\n  if item then\n    return Snacks.picker.util.dir(item)\n  end\n  return self:cwd()\nend\n\n--- Get the selected items.\n--- If `fallback=true` and there is no selection, return the current item.\n---@param opts? {fallback?: boolean} default is `false`\n---@return snacks.picker.Item[]\nfunction M:selected(opts)\n  opts = opts or {}\n  local ret = vim.deepcopy(self.list.selected)\n  if #ret == 0 and opts.fallback then\n    ret = { self:current() }\n  end\n  return vim.tbl_map(function(item)\n    return self:resolve(item)\n  end, ret)\nend\n\n--- Total number of items in the picker\nfunction M:count()\n  return self.finder:count()\nend\n\n--- Check if the picker is empty\nfunction M:empty()\n  return self:count() == 0\nend\n\n---@return snacks.Picker.ref\nfunction M:ref()\n  return Snacks.util.ref(self)\nend\n\n--- Close the picker\nfunction M:close()\n  self.input:stopinsert()\n  if self.closed then\n    return\n  end\n\n  if self.opts.on_close then\n    self.opts.on_close(self)\n  end\n\n  self:hist_record(true)\n  self.closed = true\n\n  for toggle in pairs(self.opts.toggles) do\n    self.init_opts[toggle] = self.opts[toggle]\n  end\n\n  require(\"snacks.picker.resume\").add(self)\n\n  local current = vim.api.nvim_get_current_win()\n  local is_picker_win = vim.tbl_contains({ self.input.win.win, self.list.win.win, self.preview.win.win }, current)\n  if is_picker_win and vim.api.nvim_win_is_valid(self.main) then\n    pcall(vim.api.nvim_set_current_win, self.main)\n  end\n  self.updater:stop()\n  if not self.updater:is_closing() then\n    self.updater:close()\n  end\n  self.finder:abort()\n  self.matcher:abort()\n  M._active[self] = nil\n  vim.schedule(function()\n    self.finder:close()\n    self.matcher:close()\n    self.layout:close()\n    self.list:close()\n    self.input:close()\n    self.preview:close()\n    self.resolved_layout = nil\n    self.preview = nil\n    self.matcher = nil\n    self.updater = nil\n    self.history = nil\n  end)\nend\n\n--- Check if the finder or matcher is running\nfunction M:is_active()\n  return self.finder:running() or self.matcher:running()\nend\n\n---@private\nfunction M:progress(ms)\n  if self.updater:is_active() or self.closed then\n    return\n  end\n  local ref = self:ref()\n  self.updater = vim.defer_fn(function()\n    local self = ref()\n    if not self then\n      return\n    end\n    self:update()\n    if not self.closed and self:is_active() then\n      -- slower progress when we filled topk\n      local topk, height = self.list.topk:count(), self.list.state.height or 50\n      self:progress(topk > height and 30 or 10)\n    end\n  end, ms or 10)\nend\n\n---@hide\n---@param opts? {force?: boolean}\nfunction M:update(opts)\n  opts = opts or {}\n  if self.closed then\n    return\n  end\n\n  -- Schedule the update if we are in a fast event\n  if vim.in_fast_event() then\n    return vim.schedule(function()\n      self:update(opts)\n    end)\n  end\n\n  local count = self.finder:count()\n  local list_count = self.list:count()\n  -- Check if we should show the picker\n  if not self.shown then\n    -- Always show live pickers\n    if self.opts.live then\n      self:show()\n    elseif not self:is_active() then\n      if count == 0 and not self.opts.show_empty then\n        -- no results found\n        local msg = \"No results\"\n        if self.opts.source then\n          msg = (\"No results found for `%s`\"):format(self.opts.source)\n        end\n        Snacks.notify.warn(msg, { title = \"Snacks Picker\" })\n        self:close()\n        return\n      elseif count == 1 and self.opts.auto_confirm then\n        -- auto confirm if only one result\n        self:action(\"confirm\")\n        self:close()\n        return\n      else\n        -- show the picker if we have results\n        self.list:unpause()\n        self:show()\n      end\n    elseif vim.uv.hrtime() - self.start_time > (self.opts.show_delay * 1e6) then\n      -- show the picker after show_delay ms if there are no results yet\n      self:show()\n    elseif list_count > 1 or (list_count == 1 and not self.opts.auto_confirm) then -- show the picker if we have results\n      self:show()\n    end\n  end\n\n  if self.shown then\n    if not self:is_active() or list_count > 3 then\n      self.list:unpause()\n    end\n    -- update list and input\n    if not self.input.paused then\n      self.input:update()\n    end\n    self.list:update(opts)\n  end\nend\n\n--- Execute the given action(s)\n---@param actions string|string[]\nfunction M:action(actions)\n  return self.input.win:execute(actions)\nend\n\n--- Add current filter to history\n---@param force? boolean\n---@private\nfunction M:hist_record(force)\n  if not force and not self.history:is_current() then\n    return\n  end\n  self.history:record({\n    pattern = self.input.filter.pattern,\n    search = self.input.filter.search,\n    live = self.opts.live,\n  })\nend\n\nfunction M:cwd()\n  return self.input.filter.cwd\nend\n\nfunction M:set_cwd(cwd)\n  self.input.filter:set_cwd(cwd)\n  self.opts.cwd = cwd\nend\n\n--- Move the history cursor\n---@param forward? boolean\nfunction M:hist(forward)\n  self:hist_record()\n  if forward then\n    self.history:next()\n  else\n    self.history:prev()\n  end\n  local hist = self.history:get() --[[@as snacks.picker.history.Record]]\n  self.opts.live = hist.live\n  self.input:set(hist.pattern, hist.search)\nend\n\n--- Clears the selection, set the target to the current item,\n--- and refresh the finder and matcher.\nfunction M:refresh()\n  self.list:set_selected()\n  self.list:set_target()\n  self:find({ refresh = true })\nend\n\n--- Check if the finder and/or matcher need to run,\n--- based on the current pattern and search string.\n---@param opts? { on_done?: fun(), refresh?: boolean }\nfunction M:find(opts)\n  if self.closed then\n    return\n  end\n  opts = opts or {}\n  local filter = self.input.filter:clone({ trim = true })\n  local refresh = opts.refresh ~= false\n  if filter.opts.transform then\n    refresh = filter.opts.transform(self, filter) or refresh\n  end\n  self:hist_record()\n\n  local finding = false\n  if self.finder:init(filter) or refresh then\n    finding = true\n    self:update_titles()\n    if self:count() > 0 then\n      -- pause rapid list updates to prevent flickering\n      self.list:pause(2000)\n    end\n    self.finder:run(self)\n  end\n\n  -- re-run matcher if finder or pattern changed\n  if self.matcher:init(filter.pattern) or finding then\n    self.matcher:run(self)\n    if opts.on_done then\n      if self.matcher.task:running() then\n        self.matcher.task:on(\"done\", vim.schedule_wrap(opts.on_done))\n      else\n        opts.on_done()\n      end\n    end\n\n    self.input:pause(60)\n\n    self:progress()\n  end\nend\n\n--- Get the active filter\nfunction M:filter()\n  return self.input.filter:clone()\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/preview.lua",
    "content": "---@class snacks.picker.Preview\n---@field item? snacks.picker.Item\n---@field pos? snacks.picker.Pos\n---@field win snacks.win\n---@field filter? snacks.picker.Filter\n---@field preview snacks.picker.preview\n---@field state table<string, any>\n---@field main? number\n---@field win_opts {main: snacks.win.Config|{}, layout: snacks.win.Config|{}, win: snacks.win.Config|{}}\n---@field winhl string\n---@field title? string\n---@field split_layout? boolean\n---@field opts? snacks.picker.previewers.Config\n---@field _spinner? snacks.util.Spinner\nlocal M = {}\nM.__index = M\n\n---@class snacks.picker.preview.ctx\n---@field picker snacks.Picker\n---@field item snacks.picker.Item\n---@field prev? snacks.picker.Item\n---@field preview snacks.picker.Preview\n---@field buf number\n---@field win number\n\nlocal ns = vim.api.nvim_create_namespace(\"snacks.picker.preview\")\nlocal ns_loc = vim.api.nvim_create_namespace(\"snacks.picker.preview.loc\")\n\n-- HACK: work-around for buffer-local window options mess. From the docs:\n-- > When editing a buffer that has been edited before, the options from the window\n-- > that was last closed are used again.  If this buffer has been edited in this\n-- > window, the values from back then are used.  Otherwise the values from the\n-- > last closed window where the buffer was edited last are used.\nvim.api.nvim_create_autocmd(\"BufWinEnter\", {\n  group = vim.api.nvim_create_augroup(\"snacks.picker.preview.wo\", { clear = true }),\n  callback = function(ev)\n    local buf = ev.buf\n    if not vim.b[buf].snacks_previewed then\n      return\n    end\n    local win = vim.api.nvim_get_current_win()\n    if buf ~= vim.api.nvim_win_get_buf(win) or vim.w[win].snacks_picker_preview or Snacks.util.is_float(win) then\n      return\n    end\n    vim.b[buf].snacks_previewed = nil\n    local reset = { \"winhighlight\", \"cursorline\", \"number\", \"relativenumber\", \"signcolumn\" }\n    for _, k in ipairs(reset) do\n      vim.api.nvim_set_option_value(k, nil, { win = win, scope = \"local\" })\n    end\n  end,\n})\n\n---@param picker snacks.Picker\nfunction M.new(picker)\n  local opts = picker.opts\n  local self = setmetatable({}, M)\n  self.opts = opts.previewers\n  self.winhl = Snacks.picker.highlight.winhl(\"SnacksPickerPreview\", { CursorLine = \"Visual\" })\n  local win_opts = Snacks.win.resolve(\n    {\n      title_pos = \"center\",\n      minimal = false,\n      wo = {\n        cursorline = false,\n        colorcolumn = \"\",\n        number = opts.win.preview.minimal ~= true,\n        relativenumber = false,\n        list = false,\n      },\n    },\n    opts.win.preview,\n    {\n      show = false,\n      enter = false,\n      width = 0,\n      height = 0,\n      on_win = function()\n        self.item = nil\n        self:reset()\n      end,\n      wo = {\n        winhighlight = self.winhl,\n      },\n      scratch_ft = \"snacks_picker_preview\",\n      w = {\n        snacks_picker_preview = true,\n      },\n    }\n  )\n  self.win_opts = {\n    main = {\n      relative = \"win\",\n      backdrop = false,\n      zindex = 40, -- Lower than default (50) so input/help windows stay on top\n    },\n    layout = {\n      backdrop = win_opts.backdrop == true,\n    },\n  }\n  self.win = Snacks.win(win_opts)\n  self:update(picker)\n  self.state = {}\n\n  self.win:on(\"WinClosed\", function()\n    self:clear(self.win.buf)\n    vim.schedule(function()\n      local ei = vim.o.eventignore\n      vim.o.eventignore = \"all\"\n      for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n        if\n          vim.api.nvim_buf_is_loaded(buf)\n          and vim.b[buf].snacks_picker_loaded\n          and not vim.bo[buf].buflisted\n          and #vim.fn.win_findbuf(buf) == 0\n        then\n          vim.api.nvim_buf_delete(buf, { force = true })\n        end\n      end\n      vim.o.eventignore = ei\n    end)\n  end, { win = true })\n\n  self.preview = Snacks.picker.config.preview(opts)\n  return self\nend\n\nfunction M:close()\n  self.win:destroy()\n  self.item = nil\n  self.win_opts = { main = {}, layout = {}, win = {} }\nend\n\n---@param picker snacks.Picker\nfunction M:update(picker)\n  local main = picker.resolved_layout.preview == \"main\" and picker.main or nil\n  self.main = main\n  self.win_opts.main.win = main\n  self.win.opts = vim.tbl_deep_extend(\"force\", self.win.opts, main and self.win_opts.main or self.win_opts.layout)\n  if not main then\n    self.win.opts.relative = nil\n    self.win.opts.win = nil\n    self.win.layout = nil\n  end\n  local winhl = self.winhl\n  if main then\n    winhl = (vim.wo[main].winhighlight .. \",Normal:Normal,\" .. \"CursorLine:SnacksPickerPreviewCursorLine\"):gsub(\n      \"^,\",\n      \"\"\n    )\n  end\n  self.win.opts.wo.winhighlight = winhl\nend\n\n--- refresh the preview after layout change\n---@param picker snacks.Picker\nfunction M:refresh(picker)\n  self.item = nil\n  self:reset()\n  if self.main then\n    self.win:update()\n  end\n  vim.schedule(function()\n    picker:show_preview()\n  end)\nend\n\n---@param picker snacks.Picker\n---@param opts? {force?: boolean}\nfunction M:show(picker, opts)\n  if not self.win:valid() then\n    return\n  end\n  opts = opts or {}\n  self.split_layout = not picker.layout.root:is_floating()\n  local item, prev = picker:current({ resolve = false }), self.item\n  if not opts.force and self.item == item and self.pos == (item and item.pos or nil) then\n    return\n  end\n  Snacks.picker.util.resolve(item)\n  self.item = item\n  self.filter = picker:filter()\n  self.pos = item and item.pos or nil\n  self:spinner(false)\n  if item then\n    local buf = self.win.buf\n    local ok, err = pcall(\n      self.preview,\n      setmetatable({\n        preview = self,\n        item = item,\n        prev = prev,\n        picker = picker,\n      }, {\n        __index = function(_, k)\n          if k == \"buf\" then\n            return self.win.buf\n          elseif k == \"win\" then\n            return self.win.win\n          end\n        end,\n      })\n    )\n    if not ok then\n      self:notify(err --[[@as string]], \"error\")\n    end\n    if self.win.buf ~= buf then\n      self:clear(buf)\n    end\n  else\n    self:reset()\n  end\nend\n\n---@param title? string\nfunction M:set_title(title)\n  self.title = title\nend\n\n---@param wo vim.wo|{}\nfunction M:wo(wo)\n  if self.win:win_valid() then\n    Snacks.util.wo(self.win.win, wo)\n  end\nend\n\n---@param buf? number\nfunction M:clear(buf)\n  if not (buf and vim.api.nvim_buf_is_valid(buf)) then\n    return\n  end\n  vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)\n  vim.api.nvim_buf_clear_namespace(buf, ns_loc, 0, -1)\nend\n\n---@param buf number\nfunction M:set_buf(buf)\n  vim.b[buf].snacks_previewed = true\n  self.win:set_buf(buf)\n  if self.item and self.item.wo and self.win:win_valid() then\n    Snacks.util.wo(self.win.win, self.item.wo)\n  end\nend\n\nfunction M:reset()\n  if not self.win:valid() then\n    return\n  end\n  if self.win.scratch_buf and vim.api.nvim_buf_is_valid(self.win.scratch_buf) then\n    self.win:set_buf(self.win.scratch_buf)\n  else\n    self.win:scratch()\n  end\n  vim.api.nvim_buf_clear_namespace(self.win.buf, -1, 0, -1)\n  self:set_title()\n  self:spinner(false)\n  vim.treesitter.stop(self.win.buf)\n  vim.bo[self.win.buf].modifiable = true\n  self:set_lines({})\n  self:clear(self.win.buf)\n  local ei = vim.o.eventignore\n  vim.o.eventignore = \"all\"\n  vim.bo[self.win.buf].filetype = \"snacks_picker_preview\"\n  vim.bo[self.win.buf].syntax = \"\"\n  vim.bo[self.win.buf].buftype = \"nofile\"\n  self:wo({ cursorline = false })\n  self:wo(self.win.opts.wo)\n  vim.o.eventignore = ei\nend\n\nfunction M:minimal()\n  self:wo({ number = false, relativenumber = false, signcolumn = \"no\" })\nend\n\n-- create a new scratch buffer\nfunction M:scratch()\n  local buf = vim.api.nvim_create_buf(false, true)\n  vim.bo[buf].bufhidden = \"wipe\"\n  local ei = vim.o.eventignore\n  vim.o.eventignore = \"all\"\n  vim.bo[buf].filetype = \"snacks_picker_preview\"\n  vim.o.eventignore = ei\n  self.win:set_buf(buf)\n  self.win:map()\n  self:minimal()\n  return buf\nend\n\n--- highlight the buffer\n---@param opts? {file?:string, buf?:number, ft?:string, lang?:string}\nfunction M:highlight(opts)\n  opts = opts or {}\n  local ft = opts.ft\n  if not ft and opts.buf then\n    local modeline = Snacks.picker.util.modeline(opts.buf)\n    ft = modeline and modeline.ft\n  end\n  if not ft and (opts.file or opts.buf) then\n    ft = vim.filetype.match({\n      buf = opts.buf or self.win.buf,\n      filename = opts.file,\n    })\n  end\n  local lang = Snacks.util.get_lang(opts.lang or ft)\n  if lang == \"markdown\" then\n    return self:markdown()\n  end\n  if not (lang and pcall(vim.treesitter.start, self.win.buf, lang)) and ft then\n    vim.bo[self.win.buf].syntax = ft\n  end\nend\n\nfunction M:ns()\n  return ns\nend\n\n-- show the item location\nfunction M:loc()\n  vim.api.nvim_buf_clear_namespace(self.win.buf, ns_loc, 0, -1)\n  if not self.item then\n    return\n  end\n\n  local line_count = vim.api.nvim_buf_line_count(self.win.buf)\n  Snacks.picker.util.resolve_loc(self.item, self.win.buf)\n\n  local function show(pos)\n    local center = true\n    if self.split_layout and self.main and self.item and self.item.buf then\n      local main_buf = vim.api.nvim_win_get_buf(self.main)\n      if main_buf == self.item.buf then\n        center = false\n        local view = vim.api.nvim_win_call(self.main, vim.fn.winsaveview)\n        vim.api.nvim_win_call(self.win.win, function()\n          vim.fn.winrestview(view)\n        end)\n      end\n    end\n    vim.api.nvim_win_set_cursor(self.win.win, pos)\n    vim.api.nvim_win_call(self.win.win, function()\n      if center then\n        vim.cmd(\"norm! zzze\")\n      end\n      self:wo({ cursorline = true })\n    end)\n  end\n\n  if self.item.pos and self.item.pos[1] > 0 and self.item.pos[1] <= line_count then\n    show(self.item.pos)\n    if self.item.positions then\n      for _, extmark in ipairs(Snacks.picker.highlight.matches({}, self.item.positions)) do\n        local col, row = extmark.col, self.item.pos[1]\n        extmark.col = nil\n        extmark.row = nil\n        extmark.field = nil\n        extmark.hl_group = \"SnacksPickerSearch\"\n        pcall(vim.api.nvim_buf_set_extmark, self.win.buf, ns_loc, row - 1, col, extmark)\n      end\n    elseif self.item.end_pos then\n      vim.api.nvim_buf_set_extmark(self.win.buf, ns_loc, self.item.pos[1] - 1, self.item.pos[2], {\n        end_row = self.item.end_pos[1] - 1,\n        end_col = self.item.end_pos[2],\n        hl_group = \"SnacksPickerSearch\",\n      })\n    elseif self.filter and vim.trim(self.filter.search) ~= \"\" then\n      local ok, re = pcall(vim.regex, vim.trim(self.filter.search))\n      if ok and re then\n        local start = self.item.pos[2]\n        local from, to ---@type number?, number?\n        pcall(function()\n          from, to = re:match_line(self.win.buf, self.item.pos[1] - 1, start)\n        end)\n        if from and to then\n          show({ self.item.pos[1], start + to }) -- make sure the to column is visible\n          vim.api.nvim_buf_set_extmark(self.win.buf, ns_loc, self.item.pos[1] - 1, start + from, {\n            end_col = start + to,\n            hl_group = \"SnacksPickerSearch\",\n          })\n        end\n      end\n    end\n  elseif self.item.search then\n    vim.api.nvim_win_call(self.win.win, function()\n      if pcall(vim.cmd, \":0;\" .. self.item.search) then\n        vim.fn.histdel(\"search\", -1) -- remove from search history\n        vim.cmd(\"norm! zzze\")\n        self:wo({ cursorline = true })\n      end\n    end)\n  else -- no position info, go to top\n    vim.api.nvim_win_set_cursor(self.win.win, { 1, 0 })\n  end\nend\n\n---@param lines string[]\n---@param offset? number\nfunction M:set_lines(lines, offset)\n  lines = vim.split(table.concat(lines, \"\\n\"), \"\\n\", { plain = true })\n  vim.bo[self.win.buf].modifiable = true\n  vim.api.nvim_buf_set_lines(self.win.buf, offset or 0, -1, false, lines)\n  vim.bo[self.win.buf].modifiable = false\nend\n\n---@param msg string\n---@param level? \"info\" | \"warn\" | \"error\"\n---@param opts? {item?:boolean}\nfunction M:notify(msg, level, opts)\n  if not self.win:buf_valid() then\n    Snacks.notify(msg, { level = level })\n    return\n  end\n  self:reset()\n  level = level or \"info\"\n  local lines = vim.split(level .. \": \" .. msg, \"\\n\", { plain = true })\n  local msg_len = #lines\n  if not (opts and opts.item == false) then\n    lines[#lines + 1] = \"\"\n    vim.list_extend(lines, vim.split(vim.inspect(self.item), \"\\n\", { plain = true }))\n  end\n  self:set_lines(lines)\n  vim.api.nvim_buf_set_extmark(self.win.buf, ns, 0, 0, {\n    hl_group = \"Diagnostic\" .. level:sub(1, 1):upper() .. level:sub(2),\n    end_row = msg_len,\n  })\n  self:highlight({ lang = \"lua\" })\nend\n\nfunction M:markdown()\n  if not self.win:valid() then\n    return\n  end\n  require(\"snacks.picker.util.markdown\").render(self.win.buf)\nend\n\nfunction M:spinner(enable)\n  if enable == false then\n    if self._spinner then\n      self._spinner:stop()\n      self._spinner = nil\n    end\n    return\n  end\n  assert(self.win:buf_valid(), \"invalid buffer\")\n  local ret = Snacks.picker.util.spinner(self.win.buf)\n  self._spinner = ret\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/core/score.lua",
    "content": "--- This is a port of the scoring logic from fzf. See:\n--- https://github.com/junegunn/fzf/blob/master/src/algo/algo.go\n---@class snacks.picker.Score\n---@field score number\n---@field consecutive number\n---@field prev? number\n---@field prev_class number\n---@field is_file boolean\n---@field first_bonus number\n---@field str string\n---@field opts snacks.picker.matcher.Config\n---@field bonus_matrix number[][]\n---@field bonus_boundary_white number\n---@field bonus_boundary_delimiter number\nlocal M = {}\nM.__index = M\n\n-- Scoring constants. Same as fzf:\nlocal SCORE_MATCH = 16\nlocal SCORE_GAP_START = -3\nlocal SCORE_GAP_EXTENSION = -1\n\nlocal BONUS_BOUNDARY = SCORE_MATCH / 2 -- 8\nlocal BONUS_NONWORD = SCORE_MATCH / 2 -- 8\nlocal BONUS_CAMEL_123 = BONUS_BOUNDARY - 1 -- 7\nlocal BONUS_CONSECUTIVE = -(SCORE_GAP_START + SCORE_GAP_EXTENSION) -- 4\nlocal BONUS_FIRST_CHAR_MULTIPLIER = 2\nlocal BONUS_NO_PATH_SEP = BONUS_BOUNDARY - 2 -- added when there is no path separator following the from position\n\nlocal PATH_SEP = package.config:sub(1, 1)\n\n-- ASCII char classes (simplified); adapt as needed:\nlocal CHAR_WHITE = 0\nlocal CHAR_NONWORD = 1\nlocal CHAR_DELIMITER = 2\nlocal CHAR_LOWER = 3\nlocal CHAR_UPPER = 4\nlocal CHAR_LETTER = 5\nlocal CHAR_NUMBER = 6\n\n-- Table to classify ASCII bytes quickly:\nlocal CHAR_CLASS = {} ---@type number[]\nfor b = 0, 255 do\n  local c = CHAR_NONWORD\n  local char = string.char(b)\n  if char:match(\"%s\") then\n    c = CHAR_WHITE\n  elseif char:match(\"[/\\\\,:;|]\") then\n    c = CHAR_DELIMITER\n  elseif b >= 48 and b <= 57 then -- '0'..'9'\n    c = CHAR_NUMBER\n  elseif b >= 65 and b <= 90 then -- 'A'..'Z'\n    c = CHAR_UPPER\n  elseif b >= 97 and b <= 122 then -- 'a'..'z'\n    c = CHAR_LOWER\n  end\n  CHAR_CLASS[b] = c\nend\n\n---@param opts? snacks.picker.matcher.Config\nfunction M.new(opts)\n  local self = setmetatable({}, M)\n  self.opts = opts or {}\n  self.score = 0\n  self.is_file = true\n  self.consecutive = 0\n  self.prev_class = CHAR_WHITE\n  self.str = \"\"\n  self.first_bonus = 0\n  self.bonus_matrix = {}\n  self.bonus_boundary_white = BONUS_BOUNDARY + 2\n  self.bonus_boundary_delimiter = BONUS_BOUNDARY + 1\n  if self.opts.history_bonus then\n    self.bonus_boundary_white = BONUS_BOUNDARY\n    self.bonus_boundary_delimiter = BONUS_BOUNDARY\n  end\n  self:compute_bonus_matrix()\n  return self\nend\n\nfunction M:compute_bonus_matrix()\n  for prev = 0, 6 do\n    self.bonus_matrix[prev] = {}\n    for curr = 0, 6 do\n      self.bonus_matrix[prev][curr] = self:compute_bonus(prev, curr)\n    end\n  end\nend\n\n-- Helper to compute boundary/camelCase bonuses (mimics fzf approach)\nfunction M:compute_bonus(prev, curr)\n  -- If transitioning from whitespace/delimiter/nonword to letter => boundary bonus\n  if curr > CHAR_NONWORD then\n    if prev == CHAR_WHITE then\n      return self.bonus_boundary_white\n    elseif prev == CHAR_DELIMITER then\n      return self.bonus_boundary_delimiter\n    elseif prev == CHAR_NONWORD then\n      return BONUS_BOUNDARY\n    end\n  end\n\n  -- camelCase transitions or letter->number transitions\n  if (prev == CHAR_LOWER and curr == CHAR_UPPER) or (prev ~= CHAR_NUMBER and curr == CHAR_NUMBER) then\n    return BONUS_CAMEL_123\n  end\n\n  if curr == CHAR_NONWORD or curr == CHAR_DELIMITER then\n    return BONUS_NONWORD\n  elseif curr == CHAR_WHITE then\n    return BONUS_BOUNDARY + 2\n  end\n  return 0\nend\n\n---@param str string\n---@param pos number\nfunction M:is_left_boundary(str, pos)\n  return pos == 1 or CHAR_CLASS[str:byte(pos - 1)] < CHAR_LOWER\nend\n\n---@param str string\n---@param pos number\nfunction M:is_right_boundary(str, pos)\n  return pos == #str or CHAR_CLASS[str:byte(pos + 1)] < CHAR_LOWER\nend\n\n---@param str string\n---@param first number\nfunction M:init(str, first)\n  self.str = str\n  self.score = 0\n  self.consecutive = 0\n  self.prev_class = CHAR_WHITE\n  self.prev = nil\n  self.first_bonus = 0\n  if first > 1 then\n    self.prev_class = CHAR_CLASS[str:byte(first - 1)] or CHAR_NONWORD\n  end\n  if\n    self.is_file\n    and self.opts.filename_bonus\n    and not str:find(PATH_SEP, first + 1, true)\n    and not (PATH_SEP ~= \"/\" and str:find(\"/\", first + 1, true))\n  then\n    self.score = self.score + BONUS_NO_PATH_SEP\n  end\n  self:update(first)\nend\n\n---@param pos number\nfunction M:update(pos)\n  local b = self.str:byte(pos)\n  local class = CHAR_CLASS[b] or CHAR_NONWORD\n  local bonus = 0\n  local gap = self.prev and pos - self.prev - 1 or 0\n\n  if gap > 0 then\n    self.prev_class = CHAR_CLASS[self.str:byte(pos - 1)] or CHAR_NONWORD\n    bonus = self.bonus_matrix[self.prev_class][class] or 0\n    self.score = self.score + SCORE_GAP_START + (gap - 1) * SCORE_GAP_EXTENSION\n    self.consecutive = 0\n    self.first_bonus = 0\n  else\n    bonus = self.bonus_matrix[self.prev_class][class] or 0\n    -- No gap => consecutive chunk\n    if self.consecutive == 0 then\n      -- New chunk => store the boundary/camel bonus\n      self.first_bonus = bonus\n    else\n      -- If we see a bigger boundary/camel bonus than what started the chunk, update\n      if bonus >= BONUS_BOUNDARY and bonus > self.first_bonus then\n        self.first_bonus = bonus\n      end\n      -- Take the max of the current bonus, the chunk's firstBonus, or BONUS_CONSECUTIVE\n      bonus = math.max(bonus, self.first_bonus, BONUS_CONSECUTIVE)\n    end\n    self.consecutive = self.consecutive + 1\n  end\n\n  if not self.prev then\n    bonus = (bonus * BONUS_FIRST_CHAR_MULTIPLIER)\n  end\n\n  self.score = self.score + SCORE_MATCH + bonus\n\n  -- Update for next iteration\n  self.prev_class = class\n  self.prev = pos\nend\n\n---@param str string\n---@param from number\n---@param to number\nfunction M:get(str, from, to)\n  self:init(str, from)\n  for i = from + 1, to do\n    self:update(i)\n  end\n  return self.score\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/format.lua",
    "content": "---@class snacks.picker.formatters\n---@field [string] snacks.picker.format\nlocal M = {}\n\nlocal uv = vim.uv or vim.loop\n\nfunction M.severity(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local severity = item.severity\n  severity = type(severity) == \"number\" and vim.diagnostic.severity[severity] or severity\n  if not severity or type(severity) == \"number\" then\n    return ret\n  end\n  ---@cast severity string\n  local lower = severity:lower()\n  local cap = severity:sub(1, 1):upper() .. lower:sub(2)\n\n  if picker.opts.formatters.severity.pos == \"right\" then\n    return {\n      {\n        col = 0,\n        virt_text = { { picker.opts.icons.diagnostics[cap], \"Diagnostic\" .. cap } },\n        virt_text_pos = \"right_align\",\n        hl_mode = \"combine\",\n      },\n    }\n  end\n\n  if picker.opts.formatters.severity.icons then\n    ret[#ret + 1] = { picker.opts.icons.diagnostics[cap], \"Diagnostic\" .. cap, virtual = true }\n    ret[#ret + 1] = { \" \", virtual = true }\n  end\n\n  if picker.opts.formatters.severity.level then\n    ret[#ret + 1] = { lower:upper(), \"Diagnostic\" .. cap, virtual = true }\n    ret[#ret + 1] = { \" \", virtual = true }\n  end\n\n  return ret\nend\n\nfunction M.filename(item, picker)\n  ---@type snacks.picker.Highlight[]\n  local ret = {}\n  if not item.file then\n    return ret\n  end\n  local path = Snacks.picker.util.path(item) or item.file\n\n  if picker.opts.icons.files.enabled ~= false then\n    local name, cat = path, (item.dir and \"directory\" or \"file\")\n    if item.buf and vim.api.nvim_buf_is_loaded(item.buf) and vim.bo[item.buf].buftype ~= \"\" then\n      name = vim.bo[item.buf].filetype\n      cat = \"filetype\"\n    end\n    local icon, hl = Snacks.util.icon(name, cat, {\n      fallback = picker.opts.icons.files,\n    })\n    if item.buftype == \"terminal\" then\n      icon, hl = \" \", \"Special\"\n    end\n    if item.dir and item.open then\n      icon = picker.opts.icons.files.dir_open\n    end\n    icon = Snacks.picker.util.align(icon, picker.opts.formatters.file.icon_width or 2)\n    ret[#ret + 1] = { icon, hl, virtual = true }\n  end\n\n  local base_hl = item.dir and \"SnacksPickerDirectory\" or \"SnacksPickerFile\"\n  local function is(prop)\n    local it = item\n    while it do\n      if it[prop] then\n        return true\n      end\n      it = it.parent\n    end\n  end\n\n  if is(\"ignored\") then\n    base_hl = \"SnacksPickerPathIgnored\"\n  elseif item.filename_hl then\n    base_hl = item.filename_hl\n  elseif is(\"hidden\") then\n    base_hl = \"SnacksPickerPathHidden\"\n  end\n  local dir_hl = \"SnacksPickerDir\"\n\n  if picker.opts.formatters.file.filename_only then\n    path = vim.fn.fnamemodify(item.file, \":t\")\n    path = path == \"\" and item.file or path\n    ret[#ret + 1] = { path, base_hl, field = \"file\" }\n  else\n    ret[#ret + 1] = {\n      \"\",\n      resolve = function(max_width)\n        local truncpath = Snacks.picker.util.truncpath(\n          path,\n          math.max(max_width, picker.opts.formatters.file.min_width or 20),\n          { cwd = picker:cwd(), kind = picker.opts.formatters.file.truncate }\n        )\n        local dir, base = truncpath:match(\"^(.*)/(.+)$\")\n        local resolved = {} ---@type snacks.picker.Highlight[]\n        if base and dir then\n          if picker.opts.formatters.file.filename_first then\n            resolved[#resolved + 1] = { base, base_hl, field = \"file\" }\n            resolved[#resolved + 1] = { \" \" }\n            resolved[#resolved + 1] = { dir, dir_hl, field = \"file\" }\n          else\n            resolved[#resolved + 1] = { dir .. \"/\", dir_hl, field = \"file\" }\n            resolved[#resolved + 1] = { base, base_hl, field = \"file\" }\n          end\n        else\n          resolved[#resolved + 1] = { truncpath, base_hl, field = \"file\" }\n        end\n        return resolved\n      end,\n    }\n  end\n  if item.pos and item.pos[1] > 0 then\n    ret[#ret + 1] = { \":\", \"SnacksPickerDelim\" }\n    ret[#ret + 1] = { tostring(item.pos[1]), \"SnacksPickerRow\" }\n    if item.pos[2] > 0 then\n      ret[#ret + 1] = { \":\", \"SnacksPickerDelim\" }\n      ret[#ret + 1] = { tostring(item.pos[2]), \"SnacksPickerCol\" }\n    end\n  end\n  ret[#ret + 1] = { \" \" }\n  if item.type == \"link\" then\n    local real = uv.fs_realpath(item.file)\n    local broken = not real\n    real = real or uv.fs_readlink(item.file)\n    if real then\n      ret[#ret + 1] = { \"-> \", \"SnacksPickerDelim\" }\n      ret[#ret + 1] =\n        { Snacks.picker.util.truncpath(real, 20), broken and \"SnacksPickerLinkBroken\" or \"SnacksPickerLink\" }\n      ret[#ret + 1] = { \" \" }\n    end\n  end\n  return ret\nend\n\nfunction M.file(item, picker)\n  ---@type snacks.picker.Highlight[]\n  local ret = {}\n\n  if item.label then\n    ret[#ret + 1] = { item.label, \"SnacksPickerLabel\" }\n    ret[#ret + 1] = { \" \", virtual = true }\n  end\n\n  if item.parent then\n    vim.list_extend(ret, M.tree(item, picker))\n  end\n\n  if item.status then\n    vim.list_extend(ret, M.file_git_status(item, picker))\n  end\n\n  if item.severity then\n    vim.list_extend(ret, M.severity(item, picker))\n  end\n\n  vim.list_extend(ret, M.filename(item, picker))\n\n  if item.comment then\n    table.insert(ret, { item.comment, \"SnacksPickerComment\" })\n    table.insert(ret, { \" \" })\n  end\n\n  if item.line then\n    if item.positions then\n      local offset = Snacks.picker.highlight.offset(ret)\n      Snacks.picker.highlight.matches(ret, item.positions, offset)\n    end\n    Snacks.picker.highlight.format(item, item.line, ret)\n    table.insert(ret, { \" \" })\n  end\n  return ret\nend\n\nfunction M.commit_message(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local msg = item.msg ---@type string\n  local type, scope, breaking, body = msg:match(\"^(%S+)%s*(%(.-%))(!?):%s*(.*)$\")\n  if not type then\n    type, breaking, body = msg:match(\"^(%S+)(!?):%s*(.*)$\")\n  end\n  local msg_hl = \"SnacksPickerGitMsg\"\n  if type and body then\n    local dimmed = vim.tbl_contains({ \"chore\", \"bot\", \"build\", \"ci\", \"style\", \"test\" }, type)\n    msg_hl = dimmed and \"SnacksPickerDimmed\" or \"SnacksPickerGitMsg\"\n    ret[#ret + 1] =\n      { type, breaking ~= \"\" and \"SnacksPickerGitBreaking\" or dimmed and \"SnacksPickerBold\" or \"SnacksPickerGitType\" }\n    if scope and scope ~= \"\" then\n      ret[#ret + 1] = { scope, \"SnacksPickerGitScope\" }\n    end\n    if breaking ~= \"\" then\n      ret[#ret + 1] = { \"!\", \"SnacksPickerGitBreaking\" }\n    end\n    ret[#ret + 1] = { \":\", \"SnacksPickerDelim\" }\n    ret[#ret + 1] = { \" \" }\n    msg = body\n  end\n  ret[#ret + 1] = { msg, msg_hl }\n  Snacks.picker.highlight.markdown(ret)\n  Snacks.picker.highlight.highlight(ret, {\n    [\"#%d+\"] = \"SnacksPickerGitIssue\",\n  })\n  return ret\nend\n\nfunction M.git_log(item, picker)\n  local a = Snacks.picker.util.align\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { picker.opts.icons.git.commit, \"SnacksPickerGitCommit\" }\n  local c = item.commit or item.branch or \"HEAD\"\n  ret[#ret + 1] = { a(c, 8, { truncate = true }), \"SnacksPickerGitCommit\" }\n  ret[#ret + 1] = { \" \" }\n\n  if item.date then\n    ret[#ret + 1] = { a(item.date, 16), \"SnacksPickerGitDate\" }\n  end\n  ret[#ret + 1] = { \" \" }\n\n  Snacks.picker.highlight.extend(ret, M.commit_message(item, picker))\n\n  if item.author then\n    ret[#ret + 1] = { \" <\" .. item.author .. \">\", \"SnacksPickerGitAuthor\" }\n  end\n  return ret\nend\n\nfunction M.git_branch(item, picker)\n  local a = Snacks.picker.util.align\n  local ret = {} ---@type snacks.picker.Highlight[]\n  if item.current then\n    ret[#ret + 1] = { a(\"\", 2), \"SnacksPickerGitBranchCurrent\" }\n  else\n    ret[#ret + 1] = { a(\"\", 2) }\n  end\n  if item.detached then\n    ret[#ret + 1] = { a(\"(detached HEAD)\", 30, { truncate = true }), \"SnacksPickerGitDetached\" }\n  else\n    ret[#ret + 1] = { a(item.branch, 30, { truncate = true }), \"SnacksPickerGitBranch\" }\n  end\n  ret[#ret + 1] = { \" \" }\n  Snacks.picker.highlight.extend(ret, M.git_log(item, picker))\n  return ret\nend\n\nfunction M.git_stash(item, picker)\n  local a = Snacks.picker.util.align\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { a(item.stash, 10), \"SnacksPickerIdx\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(item.branch, 10, { truncate = true }), \"SnacksPickerGitBranch\" }\n  ret[#ret + 1] = { \" \" }\n  Snacks.picker.highlight.extend(ret, M.git_log(item, picker))\n  return ret\nend\n\nfunction M.tree(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local icons = picker.opts.icons.tree\n  local indent = {} ---@type string[]\n  local node = item\n  while node and node.parent do\n    local is_last, icon = node.last, \"\"\n    if node ~= item then\n      icon = is_last and \"  \" or icons.vertical\n    else\n      icon = is_last and icons.last or icons.middle\n    end\n    table.insert(indent, 1, icon)\n    node = node.parent\n  end\n  ret[#ret + 1] = { table.concat(indent), \"SnacksPickerTree\" }\n  return ret\nend\n\nfunction M.undo(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local entry = item.item ---@type vim.fn.undotree.entry\n  local a = Snacks.picker.util.align\n  if item.current then\n    ret[#ret + 1] = { a(\"\", 2), \"SnacksPickerUndoCurrent\" }\n  else\n    ret[#ret + 1] = { a(\"\", 2) }\n  end\n  vim.list_extend(ret, M.tree(item, picker))\n  local w = vim.api.nvim_strwidth(ret[#ret][1])\n\n  ret[#ret + 1] = { tostring(entry.seq), \"SnacksPickerIdx\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(\" \", 8 - w - #tostring(entry.seq)) }\n  ret[#ret + 1] = { a(Snacks.picker.util.reltime(entry.time), 15), \"SnacksPickerTime\" }\n  ret[#ret + 1] = { \" \" }\n  local function num(v, prefix)\n    v = v or 0\n    return a((v and v > 0 and prefix .. v or \"\"), 4)\n  end\n  ret[#ret + 1] = { num(item.added, \"+\"), \"SnacksPickerUndoAdded\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { num(item.removed, \"-\"), \"SnacksPickerUndoRemoved\" }\n  if entry.save then\n    ret[#ret + 1] = { \" \" }\n    ret[#ret + 1] = { a(picker.opts.icons.undo.saved, 2), \"SnacksPickerUndoSaved\" }\n  end\n  return ret\nend\n\nfunction M.lsp_symbol(item, picker)\n  local opts = picker.opts --[[@as snacks.picker.lsp.symbols.Config]]\n  local ret = {} ---@type snacks.picker.Highlight[]\n  if item.tree and not opts.workspace then\n    vim.list_extend(ret, M.tree(item, picker))\n  end\n  local kind = item.lsp_kind or item.kind or \"Unknown\" ---@type string\n  kind = picker.opts.icons.kinds[kind] and kind or \"Unknown\"\n  local kind_hl = \"SnacksPickerIcon\" .. kind\n  ret[#ret + 1] = { picker.opts.icons.kinds[kind], kind_hl }\n  ret[#ret + 1] = { \" \" }\n  local name = vim.trim(item.name:gsub(\"\\r?\\n\", \" \"))\n  name = name == \"\" and item.detail or name\n  Snacks.picker.highlight.format(item, name, ret)\n\n  if opts.workspace then\n    local offset = Snacks.picker.highlight.offset(ret, { char_idx = true })\n    ret[#ret + 1] = { Snacks.picker.util.align(\" \", 40 - offset) }\n    vim.list_extend(ret, M.filename(item, picker))\n  end\n  return ret\nend\n\n---@param opts snacks.picker.ui_select.Opts\n---@return snacks.picker.format\nfunction M.ui_select(opts)\n  return function(item, picker)\n    local count = picker:count()\n    local ret = {} ---@type snacks.picker.Highlight[]\n    local idx = tostring(item.idx)\n    idx = (\" \"):rep(#tostring(count) - #idx) .. idx\n    ret[#ret + 1] = { idx .. \".\", \"SnacksPickerIdx\" }\n    ret[#ret + 1] = { \" \" }\n\n    if opts.kind == \"codeaction\" then\n      ---@type lsp.Command|lsp.CodeAction, lsp.HandlerContext\n      local action, ctx = item.item.action, item.item.ctx\n      local client = vim.lsp.get_client_by_id(ctx.client_id)\n      ret[#ret + 1] = { action.title }\n      if client then\n        ret[#ret + 1] = { \" \" }\n        ret[#ret + 1] = { (\"[%s]\"):format(client.name), \"SnacksPickerSpecial\" }\n      end\n    elseif opts.format_item then\n      local t = opts.format_item(item.item, true)\n      if type(t) == \"string\" then\n        ret[#ret + 1] = { t }\n      elseif type(t) == \"table\" then\n        vim.list_extend(ret, t)\n      end\n    else\n      ret[#ret + 1] = { tostring(item.item) }\n    end\n    return ret\n  end\nend\n\nfunction M.lines(item)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local line_count = vim.api.nvim_buf_line_count(item.buf)\n  local idx = Snacks.picker.util.align(tostring(item.idx), #tostring(line_count), { align = \"right\" })\n  ret[#ret + 1] = { idx, \"LineNr\", virtual = true }\n  ret[#ret + 1] = { \"  \", virtual = true }\n  ret[#ret + 1] = { item.text }\n\n  local offset = #idx + 2\n\n  for _, extmark in ipairs(item.highlights or {}) do\n    extmark = vim.deepcopy(extmark)\n    if type(extmark[1]) ~= \"string\" then\n      ---@cast extmark snacks.picker.Extmark\n      extmark.col = extmark.col + offset\n      if extmark.end_col then\n        extmark.end_col = extmark.end_col + offset\n      end\n    end\n    ret[#ret + 1] = extmark\n  end\n  return ret\nend\n\nfunction M.text(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local ft = item.ft or picker.opts.formatters.text.ft\n  if ft then\n    Snacks.picker.highlight.format(item, item.text, ret, { lang = ft })\n  else\n    ret[#ret + 1] = { item.text, item.text_hl }\n  end\n  return ret\nend\n\nfunction M.command(item)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { item.cmd, \"SnacksPickerCmd\" .. (item.cmd:find(\"^[a-z]\") and \"Builtin\" or \"\") }\n  if item.desc then\n    ret[#ret + 1] = { \" \" }\n    ret[#ret + 1] = { item.desc, \"SnacksPickerDesc\" }\n  end\n  return ret\nend\n\nfunction M.diagnostic(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local diag = item.item ---@type vim.Diagnostic\n  if item.severity then\n    vim.list_extend(ret, M.severity(item, picker))\n  end\n\n  local message = diag.message\n  ret[#ret + 1] = { message }\n  Snacks.picker.highlight.markdown(ret)\n  ret[#ret + 1] = { \" \" }\n\n  if diag.source then\n    ret[#ret + 1] = { diag.source, \"SnacksPickerDiagnosticSource\" }\n    ret[#ret + 1] = { \" \" }\n  end\n\n  if diag.code then\n    ret[#ret + 1] = { (\"(%s)\"):format(diag.code), \"SnacksPickerDiagnosticCode\" }\n    ret[#ret + 1] = { \" \" }\n  end\n  vim.list_extend(ret, M.filename(item, picker))\n  return ret\nend\n\nfunction M.autocmd(item)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ---@type vim.api.keyset.get_autocmds.ret\n  local au = item.item\n  local a = Snacks.picker.util.align\n  ret[#ret + 1] = { a(au.event, 15), \"SnacksPickerAuEvent\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(au.pattern, 10), \"SnacksPickerAuPattern\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(tostring(au.group_name or \"\"), 15), \"SnacksPickerAuGroup\" }\n  ret[#ret + 1] = { \" \" }\n  if au.command ~= \"\" then\n    Snacks.picker.highlight.format(item, au.command, ret, { lang = \"vim\" })\n  else\n    ret[#ret + 1] = { \"callback\", \"Function\" }\n  end\n  return ret\nend\n\nfunction M.hl(item)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { item.hl_group, item.hl_group }\n  return ret\nend\n\nfunction M.man(item)\n  local a = Snacks.picker.util.align\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { a(item.page, 20), \"SnacksPickerManPage\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { (\"(%s)\"):format(item.section), \"SnacksPickerManSection\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { item.desc, \"SnacksPickerManDesc\" }\n  return ret\nend\n\n-- Pretty keymaps using which-key icons when available\nfunction M.keymap(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ---@type vim.api.keyset.get_keymap\n  local k = item.item\n  local a = Snacks.picker.util.align\n\n  if package.loaded[\"which-key\"] then\n    local Icons = require(\"which-key.icons\")\n    local icon, hl = Icons.get({ keymap = k, desc = k.desc })\n    if icon then\n      ret[#ret + 1] = { a(icon, 3), hl }\n    else\n      ret[#ret + 1] = { \"   \" }\n    end\n  end\n  local lhs = Snacks.util.normkey(k.lhs)\n  ret[#ret + 1] = { k.mode, \"SnacksPickerKeymapMode\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(lhs, 15), \"SnacksPickerKeymapLhs\" }\n  ret[#ret + 1] = { \" \" }\n  local icon_nowait = picker.opts.icons.keymaps.nowait\n\n  if k.nowait == 1 then\n    ret[#ret + 1] = { icon_nowait, \"SnacksPickerKeymapNowait\" }\n  else\n    ret[#ret + 1] = { (\" \"):rep(vim.api.nvim_strwidth(icon_nowait)) }\n  end\n  ret[#ret + 1] = { \" \" }\n\n  if k.buffer and k.buffer > 0 then\n    ret[#ret + 1] = { a(\"buf:\" .. k.buffer, 6), \"SnacksPickerBufNr\" }\n  else\n    ret[#ret + 1] = { a(\"\", 6) }\n  end\n  ret[#ret + 1] = { \" \" }\n\n  local rhs_len = 0\n  if k.rhs and k.rhs ~= \"\" then\n    local rhs = k.rhs or \"\"\n    rhs_len = #rhs\n    local cmd = rhs:lower():find(\"<cmd>\")\n    if cmd then\n      ret[#ret + 1] = { rhs:sub(1, cmd + 4), \"NonText\" }\n      rhs = rhs:sub(cmd + 5)\n      local cr = rhs:lower():find(\"<cr>$\")\n      if cr then\n        rhs = rhs:sub(1, cr - 1)\n      end\n      Snacks.picker.highlight.format(item, rhs, ret, { lang = \"vim\" })\n      if cr then\n        ret[#ret + 1] = { \"<CR>\", \"NonText\" }\n      end\n    elseif rhs:lower():find(\"^<plug>\") then\n      ret[#ret + 1] = { \"<Plug>\", \"NonText\" }\n      local plug = rhs:sub(7):gsub(\"^%(\", \"\"):gsub(\"%)$\", \"\")\n      ret[#ret + 1] = { \"(\", \"SnacksPickerDelim\" }\n      Snacks.picker.highlight.format(item, plug, ret, { lang = \"vim\" })\n      ret[#ret + 1] = { \")\", \"SnacksPickerDelim\" }\n    elseif rhs:find(\"v:lua%.\") then\n      ret[#ret + 1] = { \"v:lua\", \"NonText\" }\n      ret[#ret + 1] = { \".\", \"SnacksPickerDelim\" }\n      Snacks.picker.highlight.format(item, rhs:sub(7), ret, { lang = \"lua\" })\n    else\n      ret[#ret + 1] = { k.rhs, \"SnacksPickerKeymapRhs\" }\n    end\n  else\n    ret[#ret + 1] = { \"callback\", \"Function\" }\n    rhs_len = 8\n  end\n\n  if rhs_len < 15 then\n    ret[#ret + 1] = { (\" \"):rep(15 - rhs_len) }\n  end\n\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(k.desc or \"\", 20) }\n\n  if item.file then\n    ret[#ret + 1] = { \" \" }\n    vim.list_extend(ret, M.filename(item, picker))\n  end\n  return ret\nend\n\nfunction M.git_status(item, picker)\n  local status = item.status\n  if not status and item.block then\n    local block = item.block ---@type snacks.picker.diff.Block\n    status = block.new and \"A\" or block.delete and \"D\" or block.rename and \"R\" or block.copy and \"C\" or \"M\"\n    status = block.unmerged and (status .. status) or item.staged and (status .. \" \") or (\" \" .. status)\n  elseif not status then\n    return M.filename(item, picker)\n  end\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local a = Snacks.picker.util.align\n  local s = vim.trim(status):sub(1, 1)\n  local hls = {\n    [\"A\"] = \"SnacksPickerGitStatusAdded\",\n    [\"M\"] = \"SnacksPickerGitStatusModified\",\n    [\"D\"] = \"SnacksPickerGitStatusDeleted\",\n    [\"R\"] = \"SnacksPickerGitStatusRenamed\",\n    [\"C\"] = \"SnacksPickerGitStatusCopied\",\n    [\"?\"] = \"SnacksPickerGitStatusUntracked\",\n  }\n  local hl = hls[s] or \"SnacksPickerGitStatus\"\n  hl = status:sub(1, 1) == \"M\" and \"SnacksPickerGitStatusStaged\" or hl\n  ret[#ret + 1] = { a(status, 2, { align = \"right\" }), hl }\n  ret[#ret + 1] = { \" \" }\n  if item.rename then\n    local file = item.file\n    item.file = item.rename\n    item._path = nil\n    vim.list_extend(ret, M.filename(item, picker))\n    item.file = file\n    item._path = nil\n    ret[#ret + 1] = { \"-> \", \"SnacksPickerDelim\" }\n    ret[#ret + 1] = { \" \" }\n  end\n  vim.list_extend(ret, M.filename(item, picker))\n  return ret\nend\n\nfunction M.file_git_status(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local status = require(\"snacks.picker.source.git\").git_status(item.status)\n\n  local hl = \"SnacksPickerGitStatus\"\n  if status.unmerged then\n    hl = \"SnacksPickerGitStatusUnmerged\"\n  elseif status.staged then\n    hl = \"SnacksPickerGitStatusStaged\"\n  else\n    hl = \"SnacksPickerGitStatus\" .. status.status:sub(1, 1):upper() .. status.status:sub(2)\n  end\n\n  if picker.opts.formatters.file.git_status_hl then\n    item.filename_hl = hl\n  end\n\n  local icon = status.status:sub(1, 1):upper()\n  icon = status.status == \"untracked\" and \"?\" or status.status == \"ignored\" and \"!\" or icon\n  if picker.opts.icons.git.enabled then\n    icon = picker.opts.icons.git[status.unmerged and \"unmerged\" or status.status] or icon --[[@as string]]\n    if status.staged then\n      icon = picker.opts.icons.git.staged\n    end\n  end\n\n  ret[#ret + 1] = {\n    col = 0,\n    virt_text = { { icon, hl }, { \" \" } },\n    virt_text_pos = \"right_align\",\n    hl_mode = \"combine\",\n  }\n  return ret\nend\n\nfunction M.register(item)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { \"[\", \"SnacksPickerDelim\" }\n  ret[#ret + 1] = { item.reg, \"SnacksPickerRegister\" }\n  ret[#ret + 1] = { \"]\", \"SnacksPickerDelim\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { item.value }\n  return ret\nend\n\nfunction M.buffer(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { Snacks.picker.util.align(tostring(item.buf), 3), \"SnacksPickerBufNr\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { Snacks.picker.util.align(item.flags, 2, { align = \"right\" }), \"SnacksPickerBufFlags\" }\n  ret[#ret + 1] = { \" \" }\n\n  vim.list_extend(ret, M.filename(item, picker))\n\n  if item.buftype ~= \"\" then\n    ret[#ret + 1] = { \" \" }\n    vim.list_extend(ret, {\n      { \"[\", \"SnacksPickerDelim\" },\n      { item.buftype, \"SnacksPickerBufType\" },\n      { \"]\", \"SnacksPickerDelim\" },\n    })\n  end\n\n  if item.name == \"\" and item.filetype ~= \"\" then\n    ret[#ret + 1] = { \" \" }\n    vim.list_extend(ret, {\n      { \"[\", \"SnacksPickerDelim\" },\n      { item.filetype, \"SnacksPickerFileType\" },\n      { \"]\", \"SnacksPickerDelim\" },\n    })\n  end\n\n  return ret\nend\n\nfunction M.selected(item, picker)\n  local a = Snacks.picker.util.align\n  local selected = picker.opts.icons.ui.selected\n  local unselected = picker.opts.icons.ui.unselected\n  local width = math.max(vim.api.nvim_strwidth(selected), vim.api.nvim_strwidth(unselected))\n  local ret = {} ---@type snacks.picker.Highlight[]\n  if picker.list:is_selected(item) then\n    ret[#ret + 1] = { a(selected, width), \"SnacksPickerSelected\", virtual = true }\n  elseif picker.opts.formatters.selected.unselected then\n    ret[#ret + 1] = { a(unselected, width), \"SnacksPickerUnselected\", virtual = true }\n  else\n    ret[#ret + 1] = { a(\"\", width) }\n  end\n  return ret\nend\n\nfunction M.debug(item, picker)\n  local score = item.score\n  if not picker.matcher.sorting then\n    score = picker.matcher.DEFAULT_SCORE\n    if item.score_add then\n      score = score + item.score_add\n    end\n    if item.score_mul then\n      score = score * item.score_mul\n    end\n  end\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { (\"%.2f \"):format(score), \"Number\" }\n  return ret\nend\n\nfunction M.icon(item, picker)\n  local a = Snacks.picker.util.align\n  ---@cast item snacks.picker.Icon\n  local ret = {} ---@type snacks.picker.Highlight[]\n\n  local icon_width = vim.api.nvim_strwidth(item.icon)\n  ret[#ret + 1] = { a(item.icon, icon_width > 3 and 15 or 3), \"SnacksPickerIcon\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(item.source, 10), \"SnacksPickerIconSource\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(item.name, 30), \"SnacksPickerIconName\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(item.category, 8), \"SnacksPickerIconCategory\" }\n  return ret\nend\n\nfunction M.notification(item, picker)\n  local a = Snacks.picker.util.align\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local notif = item.item ---@type snacks.notifier.Notif\n  ret[#ret + 1] = { a(os.date(\"%R\", notif.added), 5), \"SnacksPickerTime\" }\n  ret[#ret + 1] = { \" \" }\n  if item.severity then\n    vim.list_extend(ret, M.severity(item, picker))\n  end\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(notif.title or \"\", 15), \"SnacksNotifierHistoryTitle\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { notif.msg, \"SnacksPickerNotificationMessage\" }\n  Snacks.picker.highlight.markdown(ret)\n  -- ret[#ret + 1] = { \" \" }\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/init.lua",
    "content": "---@class snacks.picker\n---@field actions snacks.picker.actions\n---@field config snacks.picker.config\n---@field format snacks.picker.formatters\n---@field preview snacks.picker.previewers\n---@field sort snacks.picker.sorters\n---@field util snacks.picker.util\n---@field current? snacks.Picker\n---@field highlight snacks.picker.highlight\n---@field resume fun(opts?: snacks.picker.Config):snacks.Picker\n---@field sources snacks.picker.sources.Config\n---@overload fun(opts: snacks.picker.Config): snacks.Picker\n---@overload fun(source: string, opts: snacks.picker.Config): snacks.Picker\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.pick(...)\n  end,\n  ---@param M snacks.picker\n  __index = function(M, k)\n    if type(k) ~= \"string\" then\n      return\n    end\n    local mods = {\n      \"actions\",\n      \"config\",\n      \"format\",\n      \"preview\",\n      \"util\",\n      \"sort\",\n      highlight = \"util.highlight\",\n      sources = \"config.sources\",\n    }\n    for m, mod in pairs(mods) do\n      mod = mod == k and k or m == k and mod or nil\n      if mod then\n        ---@diagnostic disable-next-line: no-unknown\n        M[k] = require(\"snacks.picker.\" .. mod)\n        return rawget(M, k)\n      end\n    end\n    return M.config.wrap(k, { check = true })\n  end,\n})\n\n---@type snacks.meta.Meta\nM.meta = {\n  desc = \"Picker for selecting items\",\n  needs_setup = true,\n  merge = { config = \"config.defaults\", picker = \"core.picker\", \"actions\" },\n}\n\n---@class snacks.picker.resume.Opts\n---@field source? string\n---@field include? string[]\n---@field exclude? string[]\n\n-- create actual picker functions for autocomplete\nvim.defer_fn(function()\n  M.config.setup()\nend, 10)\n\n--- Create a new picker\n---@param source? string\n---@param opts? snacks.picker.Config\n---@overload fun(opts: snacks.picker.Config): snacks.Picker\nfunction M.pick(source, opts)\n  if not opts and type(source) == \"table\" then\n    opts, source = source, nil\n  end\n  opts = opts or {}\n  opts.source = source or opts.source\n  -- Show pickers if no source, items or finder is provided\n  if not (opts.source or opts.items or opts.finder or opts.multi) then\n    opts.source = \"pickers\"\n    return M.pick(opts)\n  end\n  local current = opts.source and M.get({ source = opts.source })[1]\n  if current then\n    current:close()\n    return\n  end\n  return require(\"snacks.picker.core.picker\").new(opts)\nend\n\n--- Implementation for `vim.ui.select`\n---@type snacks.picker.ui_select\nfunction M.select(...)\n  return require(\"snacks.picker.select\").select(...)\nend\n\n---@private\nfunction M.setup()\n  if M.config.get().ui_select then\n    vim.ui.select = M.select\n  end\nend\n\n---@private\nfunction M.health()\n  require(\"snacks.picker.core._health\").health()\nend\n\n--- Get active pickers, optionally filtered by source,\n--- or the current tab\n---@param opts? {source?: string, tab?: boolean} tab defaults to true\nfunction M.get(opts)\n  return require(\"snacks.picker.core.picker\").get(opts)\nend\n\n---@param opts? snacks.picker.resume.Opts\n---@overload fun(source:string):snacks.Picker?\n---@return snacks.Picker?\nfunction M.resume(opts)\n  return require(\"snacks.picker.resume\").resume(opts)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/preview.lua",
    "content": "---@class snacks.picker.previewers\nlocal M = {}\n\nlocal uv = vim.uv or vim.loop\nlocal ns = vim.api.nvim_create_namespace(\"snacks.picker.preview\")\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.directory(ctx)\n  ctx.preview:reset()\n  ctx.preview:minimal()\n  local path = Snacks.picker.util.path(ctx.item)\n  if not path then\n    ctx.preview:notify(\"Item has no `file`\", \"error\")\n    return\n  end\n  local name = vim.fn.fnamemodify(path, \":t\")\n  ctx.preview:set_title(ctx.item.title or name)\n  local ls = {} ---@type {file:string, type:\"file\"|\"directory\"}[]\n  for file, t in vim.fs.dir(path) do\n    t = t or Snacks.util.path_type(path .. \"/\" .. file)\n    ls[#ls + 1] = { file = file, type = t }\n  end\n  ctx.preview:set_lines(vim.split(string.rep(\"\\n\", #ls), \"\\n\"))\n  table.sort(ls, function(a, b)\n    if a.type ~= b.type then\n      return a.type == \"directory\"\n    end\n    return a.file < b.file\n  end)\n  for i, item in ipairs(ls) do\n    local is_dir = item.type == \"directory\"\n    local cat = is_dir and \"directory\" or \"file\"\n    local hl = is_dir and \"Directory\" or nil\n    local icon, icon_hl = Snacks.util.icon(item.file, cat, {\n      fallback = ctx.picker.opts.icons.files,\n    })\n    local line = { { icon .. \" \", icon_hl }, { item.file, hl } }\n    vim.api.nvim_buf_set_extmark(ctx.buf, ns, i - 1, 0, {\n      virt_text = line,\n    })\n  end\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.image(ctx)\n  local buf = ctx.preview:scratch()\n  ctx.preview:set_title(ctx.item.title or vim.fn.fnamemodify(ctx.item.file, \":t\"))\n  Snacks.image.buf.attach(buf, { src = Snacks.picker.util.path(ctx.item) })\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.none(ctx)\n  ctx.preview:reset()\n  ctx.preview:notify(\"no preview available\", \"warn\")\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.preview(ctx)\n  if ctx.item.preview == \"file\" then\n    return M.file(ctx)\n  end\n  assert(type(ctx.item.preview) == \"table\", \"item.preview must be a table\")\n  ctx.preview:reset()\n  local lines = vim.split(ctx.item.preview.text, \"\\n\")\n  ctx.preview:set_lines(lines)\n  if ctx.item.preview.ft then\n    ctx.preview:highlight({ ft = ctx.item.preview.ft })\n  end\n  for _, extmark in ipairs(ctx.item.preview.extmarks or {}) do\n    local e = vim.deepcopy(extmark)\n    e.col, e.row = nil, nil\n    vim.api.nvim_buf_set_extmark(ctx.buf, ns, (extmark.row or 1) - 1, extmark.col, e)\n  end\n  if ctx.item.preview.loc ~= false then\n    ctx.preview:loc()\n  end\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.file(ctx)\n  if ctx.item.buf and not ctx.item.file and not vim.api.nvim_buf_is_valid(ctx.item.buf) then\n    ctx.preview:notify(\"Buffer no longer exists\", \"error\")\n    return\n  end\n  if ctx.item.buf and not vim.api.nvim_buf_is_valid(ctx.item.buf) and (ctx.item.file or \"\"):sub(1, 1) == \"[\" then\n    ctx.preview:notify(\"Buffer no longer exists\", \"error\")\n    return\n  end\n\n  local title = ctx.item.preview_title or ctx.item.title\n\n  -- used by some LSP servers that load buffers with custom URIs\n  if ctx.item.buf and vim.uri_from_bufnr(ctx.item.buf):sub(1, 4) ~= \"file\" then\n    if not vim.api.nvim_buf_is_loaded(ctx.item.buf) then\n      vim.b[ctx.item.buf].snacks_picker_loaded = true\n      vim.fn.bufload(ctx.item.buf)\n    end\n  elseif ctx.item.file and ctx.item.file:find(\"^%w+://\") then\n    ctx.item.buf = vim.fn.bufadd(ctx.item.file)\n    vim.b[ctx.item.buf].snacks_picker_loaded = true\n    vim.fn.bufload(ctx.item.buf)\n  end\n\n  if ctx.item.buf and vim.api.nvim_buf_is_loaded(ctx.item.buf) then\n    if not title then\n      local name = vim.api.nvim_buf_get_name(ctx.item.buf)\n      title = uv.fs_stat(name) and vim.fn.fnamemodify(name, \":t\") or name\n    end\n    ctx.preview:set_title(title)\n    ctx.preview:set_buf(ctx.item.buf)\n  else\n    local path = Snacks.picker.util.path(ctx.item)\n    if not path then\n      ctx.preview:notify(\"Item has no `file`\", \"error\")\n      return\n    end\n\n    if Snacks.image.supports_file(path) and Snacks.image.config.enabled ~= false then\n      return M.image(ctx)\n    end\n\n    -- re-use existing preview when path is the same\n    if path ~= Snacks.picker.util.path(ctx.prev) then\n      ctx.preview:reset()\n      vim.bo[ctx.buf].buftype = \"\"\n\n      title = title or vim.fn.fnamemodify(path, \":t\")\n      ctx.preview:set_title(title)\n\n      local stat = uv.fs_stat(path)\n      if not stat then\n        ctx.preview:notify(\"file not found: \" .. path, \"error\")\n        return false\n      end\n      if stat.type == \"directory\" then\n        return M.directory(ctx)\n      end\n      local max_size = ctx.picker.opts.previewers.file.max_size or (1024 * 1024)\n      if stat.size > max_size then\n        ctx.preview:notify(\"large file > 1MB\", \"warn\")\n        return false\n      end\n      if stat.size == 0 then\n        ctx.preview:notify(\"empty file\", \"warn\")\n        return false\n      end\n\n      local file = assert(io.open(path, \"r\"))\n\n      local is_binary = false\n      local ft = ctx.picker.opts.previewers.file.ft or vim.filetype.match({ filename = path })\n      if ft == \"bigfile\" then\n        ft = nil\n      end\n      local lines = {}\n      for line in file:lines() do\n        ---@cast line string\n        if #line > ctx.picker.opts.previewers.file.max_line_length then\n          line = line:sub(1, ctx.picker.opts.previewers.file.max_line_length) .. \"...\"\n        end\n        -- Check for binary data in the current line\n        if line:find(\"[%z\\1-\\8\\11\\12\\14-\\31]\") then\n          is_binary = true\n          if not ft then\n            ctx.preview:notify(\"binary file\", \"warn\")\n            return\n          end\n        end\n        table.insert(lines, line)\n      end\n\n      file:close()\n\n      if is_binary then\n        ctx.preview:wo({ number = false, relativenumber = false, cursorline = false, signcolumn = \"no\" })\n      end\n      ctx.preview:set_lines(lines)\n      ctx.preview:highlight({ file = path, ft = ctx.picker.opts.previewers.file.ft, buf = ctx.buf })\n    end\n  end\n  ctx.preview:loc()\nend\n\n---@param diff string|string[]|snacks.picker.diff.Block[]\n---@param ft \"diff\"|\"git\"\n---@param ctx snacks.picker.preview.ctx\nlocal function fancy_diff(diff, ft, ctx)\n  local buf = ctx.preview:scratch()\n  ctx.preview.win:map()\n  require(\"snacks.picker.util.diff\").render(buf, ns, diff, {\n    annotations = ctx.item.annotations or ctx.picker.opts.annotations,\n  })\n  Snacks.util.wo(ctx.win, ctx.picker.opts.previewers.diff.wo or {})\nend\n\n---@param cmd string[]\n---@param ctx snacks.picker.preview.ctx\n---@param opts? snacks.job.Opts|{ft?: string}\nfunction M.cmd(cmd, ctx, opts)\n  opts = opts or {}\n  local Job = require(\"snacks.util.job\")\n  local buf = ctx.preview:scratch()\n  vim.bo[buf].buftype = \"nofile\"\n\n  opts = Snacks.config.merge(opts, {\n    debug = ctx.picker.opts.debug.proc,\n    term = opts.term ~= false and not opts.ft and opts.pty ~= false,\n    width = vim.api.nvim_win_get_width(ctx.win),\n    height = vim.api.nvim_win_get_height(ctx.win),\n    cwd = ctx.item.cwd or ctx.picker.opts.cwd,\n    env = {\n      PAGER = \"cat\",\n      DELTA_PAGER = \"cat\",\n    },\n  })\n\n  local style = ctx.picker.opts.previewers.diff.style\n  if style == \"fancy\" and vim.tbl_contains({ \"diff\", \"git\" }, opts.ft) then\n    opts.on_line = function() end or nil -- disable default line handler\n    opts.on_lines = function(_, lines)\n      fancy_diff(lines, opts.ft, ctx)\n    end\n  end\n\n  local job = Job.new(buf, cmd, opts)\n\n  if opts.ft and style ~= \"fancy\" then\n    ctx.preview:highlight({ ft = opts.ft })\n  end\n  return job\nend\n\n---@param ctx snacks.picker.preview.ctx\n---@return string[], boolean terminal\nlocal function git(ctx, ...)\n  local terminal = ctx.picker.opts.previewers.diff.style == \"terminal\"\n  local ret = { \"git\" }\n  vim.list_extend(ret, not terminal and { \"--no-pager\" } or {})\n  vim.list_extend(ret, ctx.picker.opts.previewers.git.args or {})\n  vim.list_extend(ret, { ... })\n  return ret, terminal\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.git_show(ctx)\n  local cmd, terminal = git(ctx, \"show\", ctx.item.commit)\n  local pathspec = ctx.item.files or ctx.item.file\n  pathspec = type(pathspec) == \"table\" and pathspec or { pathspec }\n  if #pathspec > 0 then\n    cmd[#cmd + 1] = \"--\"\n    vim.list_extend(cmd, pathspec)\n  end\n  M.cmd(cmd, ctx, { ft = not terminal and \"git\" or nil })\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.git_log(ctx)\n  local cmd = git(\n    ctx,\n    \"--no-pager\",\n    \"log\",\n    \"--pretty=format:%h %s (%ch) <%an>\",\n    \"--abbrev-commit\",\n    \"--decorate\",\n    \"--date=short\",\n    \"--color=never\",\n    \"--no-show-signature\",\n    \"--no-patch\",\n    ctx.item.commit\n  )\n  M.cmd(cmd, ctx, {\n    ft = \"git\",\n    ---@param text string\n    on_line = function(_, text)\n      local commit, msg, date, author = text:match(\"^(%S+) (.*) %((.*)%) <(.*)>$\")\n      if commit then\n        local hl = Snacks.picker.format.git_log({\n          idx = 1,\n          score = 0,\n          text = \"\",\n          commit = commit,\n          msg = msg,\n          date = date,\n          author = author,\n        }, ctx.picker)\n        Snacks.picker.highlight.render(ctx.buf, ns, { hl }, { append = true })\n        Snacks.util.wo(ctx.win, { breakindent = true, wrap = true, linebreak = true })\n      end\n    end,\n  })\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.diff(ctx)\n  local style = ctx.picker.opts.previewers.diff.style\n  local cmd = vim.deepcopy(ctx.picker.opts.previewers.diff.cmd)\n  style = style == \"terminal\" and vim.fn.executable(cmd[1]) == 0 and \"fancy\" or style\n  if style == \"syntax\" then\n    ctx.item.preview = { text = ctx.item.diff, ft = \"diff\", loc = false }\n    return M.preview(ctx)\n  elseif style ~= \"terminal\" then\n    return fancy_diff(ctx.item.diff, \"diff\", ctx)\n  end\n  if cmd[1] == \"delta\" and not vim.tbl_contains(cmd, \"--dark\") and not vim.tbl_contains(cmd, \"--light\") then\n    table.insert(cmd, 2, \"--\" .. vim.o.background)\n  end\n  M.cmd(cmd, ctx, {\n    input = ctx.item.diff,\n  })\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.git_diff(ctx)\n  local cmd, terminal = git(ctx, \"diff\")\n  if not ctx.item.status then\n    cmd[#cmd + 1] = \"HEAD\" -- generic diff against HEAD\n  elseif ctx.item.status:find(\"[UAD][UAD]\") then\n    cmd[#cmd + 1] = \"--cc\" -- combined diff for conflicts\n  elseif ctx.item.status:sub(1, 1) ~= \" \" then\n    cmd[#cmd + 1] = \"--cached\" -- staged changes\n  end\n  if ctx.item.file then\n    vim.list_extend(cmd, { \"--\", ctx.item.file })\n  end\n  M.cmd(cmd, ctx, {\n    ft = not terminal and \"diff\" or nil,\n  })\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.git_stash(ctx)\n  local cmd, terminal = git(ctx, \"stash\", \"show\", \"--patch\", ctx.item.stash)\n  M.cmd(cmd, ctx, { ft = not terminal and \"diff\" or nil })\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.git_status(ctx)\n  local ss = ctx.item.status\n  if ss:find(\"^[A?]\") then\n    M.file(ctx)\n  else\n    M.git_diff(ctx)\n  end\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.colorscheme(ctx)\n  if not ctx.preview.state.colorscheme then\n    ctx.preview.state.colorscheme = vim.g.colors_name or \"default\"\n    ctx.preview.state.background = vim.o.background\n    ctx.preview.win:on(\"WinClosed\", function()\n      vim.schedule(function()\n        if not ctx.preview.state.colorscheme then\n          return\n        end\n        vim.cmd(\"colorscheme \" .. ctx.preview.state.colorscheme)\n        vim.o.background = ctx.preview.state.background\n      end)\n    end, { win = true })\n  end\n  vim.schedule(function()\n    vim.cmd(\"colorscheme \" .. ctx.item.text)\n  end)\n  Snacks.picker.preview.file(ctx)\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.man(ctx)\n  M.cmd({ \"man\", ctx.item.section, ctx.item.page }, ctx, {\n    ft = \"man\",\n    env = {\n      MANPAGER = ctx.picker.opts.previewers.man_pager or vim.fn.executable(\"col\") == 1 and \"col -bx\" or \"cat\",\n      MANWIDTH = tostring(ctx.preview.win:dim().width),\n      MANPATH = vim.env.MANPATH,\n    },\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/resume.lua",
    "content": "local M = {}\n\nM.state = {} ---@type table<string, snacks.picker.resume.State>\n\n---@param picker snacks.Picker\nfunction M.add(picker)\n  for toggle in pairs(picker.opts.toggles) do\n    picker.init_opts[toggle] = picker.opts[toggle]\n  end\n\n  local source = picker.opts.source or \"custom\"\n\n  ---@class snacks.picker.resume.State\n  local state = {\n    opts = picker.init_opts or {},\n    selected = picker:selected({ fallback = false }),\n    cursor = picker.list.cursor,\n    topline = picker.list.top,\n    filter = picker.input.filter,\n    added = vim.uv.hrtime(),\n    items = source:find(\"^lsp_\") and picker.finder.items or nil,\n  }\n  state.opts.live = picker.opts.live\n  M.state[source] = state\nend\n\n---@param state snacks.picker.resume.State\nfunction M._resume(state)\n  state.opts.pattern = state.filter.pattern\n  state.opts.search = state.filter.search\n  if state.items then\n    state.opts.finder = function()\n      return state.items\n    end\n  end\n  local ret = Snacks.picker.pick(state.opts)\n  ret.list:set_selected(state.selected)\n  ret.list:update()\n  ret.input:update()\n  ret.matcher.task:on(\n    \"done\",\n    vim.schedule_wrap(function()\n      if ret.closed then\n        return\n      end\n      ret.list:view(state.cursor, state.topline)\n    end)\n  )\n  return ret\nend\n\n---@param opts? snacks.picker.resume.Opts\n---@overload fun(source:string):snacks.Picker?\nfunction M.resume(opts)\n  opts = type(opts) == \"string\" and { source = opts } or opts or {}\n  local sources = opts.source and { opts.source } or opts.include or vim.tbl_keys(M.state)\n  local states = {} ---@type snacks.picker.resume.State[]\n\n  for _, source in ipairs(sources) do\n    if M.state[source] and not vim.tbl_contains(opts.exclude or {}, source) then\n      states[#states + 1] = M.state[source]\n    end\n  end\n\n  table.sort(states, function(a, b)\n    return a.added > b.added\n  end)\n\n  local last = states[1]\n\n  if not last then\n    if opts.source then\n      return Snacks.picker.pick(opts.source)\n    end\n    Snacks.notify.error(\"No picker to resume\")\n    Snacks.picker.pickers()\n    return\n  end\n  return M._resume(last)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/select.lua",
    "content": "local M = {}\n\n---@alias vim.ui.select.on_choice fun(item?: any, idx?: number)\n---@alias snacks.picker.ui_select fun(items: any[], opts?: snacks.picker.ui_select.Opts, on_choice: vim.ui.select.on_choice)\n\n---@class snacks.picker.ui_select.Opts: vim.ui.select.Opts\n---@field format_item? fun(item: any, supports_chunks: boolean):(string|snacks.picker.Highlight[])\n---@field snacks? snacks.picker.Config\n\n---@generic T\n---@param items T[] Arbitrary items\n---@param opts? snacks.picker.ui_select.Opts\n---@param on_choice fun(item?: T, idx?: number)\nfunction M.select(items, opts, on_choice)\n  assert(type(on_choice) == \"function\", \"on_choice must be a function\")\n  opts = opts or {}\n\n  local title = opts.prompt or \"Select\"\n  title = title:gsub(\"^%s*\", \"\"):gsub(\"[%s:]*$\", \"\")\n  local completed = false\n\n  ---@type snacks.picker.select.Config\n  local picker_opts = {\n    source = \"select\",\n    finder = function()\n      ---@type snacks.picker.finder.Item[]\n      local ret = {}\n      for idx, item in ipairs(items) do\n        local text = (opts.format_item or tostring)(item)\n        ---@type snacks.picker.finder.Item\n        local it = type(item) == \"table\" and setmetatable({}, { __index = item }) or {}\n        it.text = idx .. \" \" .. text\n        it.item = item\n        it.idx = idx\n        ret[#ret + 1] = it\n      end\n      return ret\n    end,\n    format = Snacks.picker.format.ui_select(opts),\n    title = title,\n    layout = {\n      config = function(layout)\n        -- Fit list height to number of items, up to 10\n        for _, box in ipairs(layout.layout) do\n          if box.win == \"list\" and not box.height then\n            box.height = math.max(math.min(#items, vim.o.lines * 0.8 - 10), 2)\n          end\n        end\n      end,\n    },\n    actions = {\n      confirm = function(picker, item)\n        if completed then\n          return\n        end\n        completed = true\n        picker:close()\n        vim.schedule(function()\n          on_choice(item and item.item, item and item.idx)\n        end)\n      end,\n    },\n    on_close = function()\n      if completed then\n        return\n      end\n      completed = true\n      vim.schedule(on_choice)\n    end,\n  }\n\n  -- merge custom picker options\n  if opts.snacks then\n    picker_opts = Snacks.config.merge({}, vim.deepcopy(picker_opts), opts.snacks)\n  end\n\n  -- get full picker config\n  picker_opts = Snacks.picker.config.get(picker_opts)\n\n  -- merge kind options\n  local kind_opts = picker_opts.kinds and picker_opts.kinds[opts.kind]\n  if kind_opts then\n    picker_opts = Snacks.config.merge({}, picker_opts, kind_opts)\n  end\n\n  return Snacks.picker.pick(picker_opts)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/sort.lua",
    "content": "---@class snacks.picker.sorters\nlocal M = {}\n\n---@alias snacks.picker.sort.Field { name: string, desc: boolean, len?: boolean }\n\n---@class snacks.picker.sort.Config\n---@field fields? (snacks.picker.sort.Field|string)[]\n\n---@param opts? snacks.picker.sort.Config\nfunction M.default(opts)\n  local fields = {} ---@type snacks.picker.sort.Field[]\n  for _, f in ipairs(opts and opts.fields or { { name = \"score\", desc = true }, \"idx\" }) do\n    if type(f) == \"string\" then\n      local desc, len = false, nil\n      if f:sub(1, 1) == \"#\" then\n        f, len = f:sub(2), true\n      end\n      if f:sub(-5) == \":desc\" then\n        f, desc = f:sub(1, -6), true\n      elseif f:sub(-4) == \":asc\" then\n        f = f:sub(1, -5)\n      end\n      table.insert(fields, { name = f, desc = desc, len = len })\n    else\n      table.insert(fields, f)\n    end\n  end\n\n  ---@param a snacks.picker.Item\n  ---@param b snacks.picker.Item\n  return function(a, b)\n    for _, field in ipairs(fields) do\n      local av, bv = a[field.name], b[field.name]\n      if av ~= nil and bv ~= nil then\n        if field.len then\n          av, bv = #av, #bv\n        end\n        if av ~= bv then\n          if type(av) == \"boolean\" then\n            av, bv = av and 0 or 1, bv and 0 or 1\n          end\n          if field.desc then\n            return av > bv\n          else\n            return av < bv\n          end\n        end\n      end\n    end\n    return false\n  end\nend\n\nfunction M.idx()\n  ---@param a snacks.picker.Item\n  ---@param b snacks.picker.Item\n  return function(a, b)\n    return a.idx < b.idx\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/buffers.lua",
    "content": "local M = {}\n\n---@param opts snacks.picker.buffers.Config\n---@type snacks.picker.finder\nfunction M.buffers(opts, ctx)\n  opts = vim.tbl_extend(\"force\", {\n    hidden = false,\n    unloaded = true,\n    current = true,\n    nofile = false,\n    sort_lastused = true,\n  }, opts)\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local current_buf = vim.api.nvim_get_current_buf()\n  local alternate_buf = vim.fn.bufnr(\"#\")\n  for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n    local keep = (opts.hidden or vim.bo[buf].buflisted)\n      and (opts.unloaded or vim.api.nvim_buf_is_loaded(buf))\n      and (opts.current or buf ~= current_buf)\n      and (opts.nofile or vim.bo[buf].buftype ~= \"nofile\")\n      and (not opts.modified or vim.bo[buf].modified)\n    if keep then\n      local name = vim.api.nvim_buf_get_name(buf)\n      if name == \"\" then\n        name = \"[Scratch]\"\n      end\n      local info = vim.fn.getbufinfo(buf)[1]\n      local mark = vim.api.nvim_buf_get_mark(buf, '\"')\n      local flags = {\n        buf == current_buf and \"%\" or (buf == alternate_buf and \"#\" or \"\"),\n        info.hidden == 1 and \"h\" or (#(info.windows or {}) > 0) and \"a\" or \"\",\n        vim.bo[buf].readonly and \"=\" or \"\",\n        info.changed == 1 and \"+\" or \"\",\n      }\n      table.insert(items, {\n        flags = table.concat(flags),\n        buf = buf,\n        name = vim.api.nvim_buf_get_name(buf),\n        buftype = vim.bo[buf].buftype,\n        filetype = vim.bo[buf].filetype,\n        file = name,\n        info = info,\n        pos = mark[1] ~= 0 and mark or { info.lnum, 0 },\n      })\n      items[#items].text = Snacks.picker.util.text(items[#items], { \"buf\", \"name\", \"filetype\", \"buftype\" })\n    end\n  end\n  if opts.sort_lastused then\n    table.sort(items, function(a, b)\n      return a.info.lastused > b.info.lastused\n    end)\n  end\n  return ctx.filter:filter(items)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/diagnostics.lua",
    "content": "local M = {}\nlocal uv = vim.uv or vim.loop\n\n---@param opts snacks.picker.diagnostics.Config\n---@type snacks.picker.finder\nfunction M.diagnostics(opts, ctx)\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local current_buf = vim.api.nvim_get_current_buf()\n  local cwd = svim.fs.normalize(uv.cwd() or \".\")\n  for _, diag in ipairs(vim.diagnostic.get(ctx.filter.buf, { severity = opts.severity })) do\n    local buf = diag.bufnr\n    if buf and vim.api.nvim_buf_is_valid(buf) then\n      local file = svim.fs.normalize(vim.api.nvim_buf_get_name(buf), { _fast = true })\n      local severity = diag.severity\n      severity = type(severity) == \"number\" and vim.diagnostic.severity[severity] or severity\n      ---@cast severity string?\n      items[#items + 1] = {\n        text = table.concat({ severity or \"\", tostring(diag.code or \"\"), file, diag.source or \"\", diag.message }, \" \"),\n        file = file,\n        buf = diag.bufnr,\n        is_current = buf == current_buf and 0 or 1,\n        is_cwd = file:sub(1, #cwd) == cwd and 0 or 1,\n        lnum = diag.lnum,\n        severity = diag.severity,\n        pos = { diag.lnum + 1, diag.col },\n        end_pos = diag.end_lnum and { diag.end_lnum + 1, diag.end_col } or nil,\n        item = diag,\n        comment = diag.message,\n      }\n    end\n  end\n  return ctx.filter:filter(items)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/diff.lua",
    "content": "local M = {}\n\n---@class snacks.picker.diff.Config: snacks.picker.proc.Config\n---@field cmd? string optional since diff can be passed as string\n---@field group? boolean Group hunks by file\n---@field diff? string|number diff string or buffer number\n---@field annotations? snacks.diff.Annotation[]\n\n---@class snacks.picker.diff.hunk.Pos\n---@field line number\n---@field count number\n\n---@class snacks.picker.Diff\n---@field header string[]\n---@field blocks snacks.picker.diff.Block[]\n\n---@class snacks.picker.diff.Hunk\n---@field diff string[]\n---@field line number\n---@field context? string\n---@field left snacks.picker.diff.hunk.Pos old (normal) /ours (merge)\n---@field right snacks.picker.diff.hunk.Pos new (normal) /working (merge)\n---@field parents? snacks.picker.diff.hunk.Pos[] theirs (merge)\n\n---@class snacks.picker.diff.Block\n---@field unmerged? boolean\n---@field file string\n---@field left? string\n---@field right? string\n---@field header string[]\n---@field hunks snacks.picker.diff.Hunk[]\n---@field mode? {from:string, to:string}\n---@field copy? {from:string, to:string}\n---@field rename? {from:string, to:string}\n---@field delete? string (mode of deleted file)\n---@field new? string (mode of new file)\n---@field similarity? number\n---@field dissimilarity? number\n---@field index? {from:string, to:string, mode:string}\n\n---@param opts? snacks.picker.diff.Config\n---@type snacks.picker.finder\nfunction M.diff(opts, ctx)\n  opts = opts or {}\n  local lines = {} ---@type string[]\n  local finder ---@type snacks.picker.finder.result?\n\n  do\n    if opts.cmd then\n      finder = require(\"snacks.picker.source.proc\").proc(opts, ctx)\n    else\n      local diff = opts.diff\n      if not diff and vim.bo.filetype == \"diff\" then\n        diff = 0\n      end\n      if type(diff) == \"number\" then\n        lines = vim.api.nvim_buf_get_lines(diff, 0, -1, false)\n      elseif type(diff) == \"string\" then\n        lines = vim.split(diff, \"\\n\", { plain = true })\n      else\n        Snacks.notify.error(\"snacks.picker.diff: opts.diff must be a string or buffer number\")\n        return {}\n      end\n    end\n  end\n\n  local cwd = opts.cwd or ctx.filter.cwd\n  return function(cb)\n    if finder then\n      finder(function(proc_item)\n        lines[#lines + 1] = proc_item.text\n      end)\n    end\n\n    ---@param file string\n    ---@param line? number\n    ---@param diff string[]\n    ---@param block snacks.picker.diff.Block\n    local function add(file, line, diff, block)\n      line = line or 1\n      cb({\n        text = file .. \":\" .. line,\n        diff = table.concat(diff, \"\\n\"),\n        file = file,\n        cwd = cwd,\n        rename = block.rename and block.rename.from or nil,\n        annotations = opts.annotations,\n        block = block,\n        pos = { line, 0 },\n      })\n    end\n\n    local diff = M.parse(lines)\n    for _, block in ipairs(diff.blocks) do\n      local diffs = {} ---@type string[]\n      for _, h in ipairs(block.hunks) do\n        if opts.group then\n          vim.list_extend(diffs, h.diff)\n        else\n          add(block.file, h.line, vim.list_extend(vim.deepcopy(block.header), h.diff), block)\n        end\n      end\n      if opts.group or #block.hunks == 0 then\n        local line = block.hunks[1] and block.hunks[1].line or 1\n        add(block.file, line, vim.list_extend(vim.deepcopy(block.header), diffs), block)\n      end\n    end\n  end\nend\n\n---@param lines string[]\nfunction M.parse(lines)\n  local hunk ---@type snacks.picker.diff.Hunk?\n  local block ---@type snacks.picker.diff.Block?\n  local ret = {} ---@type snacks.picker.diff.Block[]\n  local header = {} ---@type string[]\n\n  ---@param file? string\n  ---@param strip_prefix? boolean\n  ---@return string?\n  local function norm(file, strip_prefix)\n    if file then\n      file = file:gsub(\"\\t.*$\", \"\") -- remove tab and after\n      file = file:gsub('^\"(.-)\"$', \"%1\") -- remove quotes\n      if file == \"/dev/null\" then -- no file\n        return\n      end\n      if strip_prefix == false then\n        return file\n      end\n      local prefix = { \"a\", \"b\", \"i\", \"w\", \"c\", \"o\", \"old\", \"new\" }\n      for _, s in ipairs(prefix) do -- remove prefixes\n        if file:sub(1, #s + 1) == s .. \"/\" then\n          return file:sub(#s + 2)\n        end\n      end\n      return file\n    end\n  end\n\n  local function emit()\n    if block and hunk then\n      hunk = nil\n    elseif not block then\n      return\n    end\n    for _, line in ipairs(block.header) do\n      if line:find(\"^%-%-%- \") then\n        block.left = norm(line:sub(5))\n      elseif line:find(\"^%+%+%+ \") then\n        block.right = norm(line:sub(5))\n      elseif line:find(\"^rename from\") then\n        block.rename = block.rename or {}\n        block.left = norm(line:match(\"^rename from (.*)\"), false)\n        block.rename.from = block.left\n      elseif line:find(\"^rename to\") then\n        block.rename = block.rename or {}\n        block.right = norm(line:match(\"^rename to (.*)\"), false)\n        block.rename.to = block.right\n      elseif line:find(\"^copy from\") then\n        block.copy = block.copy or {}\n        block.left = norm(line:match(\"^copy from (.*)\"), false)\n        block.copy.from = block.left\n      elseif line:find(\"^copy to\") then\n        block.copy = block.copy or {}\n        block.right = norm(line:match(\"^copy to (.*)\"), false)\n        block.copy.to = block.right\n      elseif line:find(\"^new file mode\") then\n        block.new = line:match(\"^new file mode (.*)\")\n      elseif line:find(\"^deleted file mode\") then\n        block.delete = line:match(\"^deleted file mode (.*)\")\n      elseif line:find(\"^old mode\") then\n        block.mode = block.mode or {}\n        block.mode.from = line:match(\"^old mode (.*)\")\n      elseif line:find(\"^new mode\") then\n        block.mode = block.mode or {}\n        block.mode.to = line:match(\"^new mode (.*)\")\n      elseif line:find(\"^similarity index\") then\n        local sim = line:match(\"^similarity index (%d+)%%\")\n        block.similarity = tonumber(sim) or 0\n      elseif line:find(\"^dissimilarity index\") then\n        local dis = line:match(\"^dissimilarity index (%d+)%%\")\n        block.dissimilarity = tonumber(dis) or 0\n      elseif line:find(\"^index \") then\n        local from, to, mode = line:match(\"^index (%S+)%.%.(%S+)%s*(%d*)$\")\n        block.index = { from = from, to = to, mode = mode ~= \"\" and mode or nil }\n      end\n    end\n    local first = block.header[1] or \"\"\n    if not block.right and not block.left and first:find(\"^diff\") then\n      -- no left/right so for sure no rename.\n      -- this means the diff header is for the same file\n      if first:find(\"^diff %-%-cc\") then\n        block.left = norm(first:match(\"^diff %-%-cc (.+)$\"))\n        block.right = block.left\n      else\n        first = first:gsub(\"^diff \", \"\"):gsub(\"^%s*%-%S+%s*\", \"\") --[[@as string]]\n        local idx = 1\n        while idx <= #first do\n          local s = first:find(\" \", idx, true)\n          if not s then\n            break\n          end\n          idx = s + 1\n          local l = norm(first:sub(1, s - 1))\n          local r = norm(first:sub(s + 1))\n          if l == r then\n            block.left = l\n            block.right = r\n            break\n          end\n        end\n      end\n    end\n    block.file = block.right or block.left or block.file\n    table.sort(block.hunks, function(a, b)\n      return a.line < b.line\n    end)\n    ret[#ret + 1] = block\n    block = nil\n  end\n\n  local with_diff_header = false\n\n  for _, text in ipairs(lines) do\n    if not block and text:find(\"^%s*$\") then\n      -- Ignore empty lines before a diff block\n    elseif text:find(\"^diff\") or (not with_diff_header and text:find(\"^%-%-%- \") and (not block or hunk)) then\n      with_diff_header = with_diff_header or text:find(\"^diff\") == 1\n      emit()\n      block = {\n        file = \"\", --file or \"unknown\",\n        header = { text },\n        hunks = {},\n      }\n    elseif text:find(\"@@\", 1, true) == 1 and block then\n      -- Hunk header\n      hunk = M.parse_hunk_header(text)\n      if hunk then\n        block.unmerged = block.unmerged or (hunk.parents ~= nil) or nil\n        block.hunks[#block.hunks + 1] = hunk\n      else\n        Snacks.notify.error(\"Invalid hunk header: \" .. text, { title = \"Snacks Picker Diff\" })\n      end\n    elseif hunk then\n      -- Hunk body\n      hunk.diff[#hunk.diff + 1] = text\n    elseif block then\n      block.header[#block.header + 1] = text\n    elseif #ret == 0 then\n      header[#header + 1] = text\n    else\n      Snacks.notify.error(\"Unexpected line: \" .. text, { title = \"Snacks Picker Diff\" })\n    end\n  end\n  emit()\n  ---@type snacks.picker.Diff\n  return { blocks = ret, header = header }\nend\n\n---@param line string\nfunction M.parse_hunk_header(line)\n  local count_start, inner, count_end, context = line:match(\"^(@+)%s*(.-)%s*(@+)%s*(.*)$\")\n  if not count_start or not count_end or count_start ~= count_end or #count_start < 2 then\n    return\n  end\n  local ret = {} ---@type {line:number, count:number}[]\n  for _, part in ipairs(vim.split(inner, \"%s+\")) do\n    local l, c = part:match(\"^[%-+](%d+),?(%d*)$\")\n    if not l then\n      return\n    end\n    ret[#ret + 1] = { line = tonumber(l) or 1, count = tonumber(c) or 1 }\n  end\n  if #ret ~= #count_start then\n    return\n  end\n  local right = table.remove(ret)\n  ---@type snacks.picker.diff.Hunk\n  return {\n    diff = { line },\n    line = right and right.line or 1,\n    left = table.remove(ret, 1),\n    right = right,\n    parents = #ret > 0 and ret or nil,\n    context = context ~= \"\" and context or nil,\n  }\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/explorer.lua",
    "content": "---@diagnostic disable: await-in-sync\nlocal Actions = require(\"snacks.explorer.actions\")\nlocal Tree = require(\"snacks.explorer.tree\")\n\nlocal M = {}\n\nM.actions = Actions.actions\n\n---@type table<snacks.Picker, snacks.picker.explorer.State>\nM._state = setmetatable({}, { __mode = \"k\" })\nlocal uv = vim.uv or vim.loop\n\n---@class snacks.picker.explorer.Item: snacks.picker.finder.Item\n---@field file string\n---@field dir? boolean\n---@field parent? snacks.picker.explorer.Item\n---@field open? boolean\n---@field last? boolean\n---@field sort? string\n---@field internal? boolean internal parent directories not part of fd output\n---@field status? string\n\nlocal function norm(path)\n  return svim.fs.normalize(path)\nend\n\n---@class snacks.picker.explorer.State\n---@field on_find? fun()?\nlocal State = {}\nState.__index = State\n---@param picker snacks.Picker\nfunction State.new(picker)\n  local self = setmetatable({}, State)\n\n  local opts = picker.opts --[[@as snacks.picker.explorer.Config]]\n  local r = picker:ref()\n  local function ref()\n    local v = r.value\n    return v and not v.closed and v or nil\n  end\n\n  Tree:refresh(picker:cwd())\n\n  local buf = vim.api.nvim_win_get_buf(picker.main)\n  local buf_file = svim.fs.normalize(vim.api.nvim_buf_get_name(buf))\n  if uv.fs_stat(buf_file) then\n    Tree:open(buf_file)\n  end\n\n  if opts.watch then\n    local on_close = picker.opts.on_close\n    picker.opts.on_close = function(p)\n      vim.schedule(function()\n        require(\"snacks.explorer.watch\").watch()\n      end)\n      if on_close then\n        on_close(p)\n      end\n    end\n  end\n\n  picker.list.win:on(\"BufWritePost\", function(_, ev)\n    local p = ref()\n    if p then\n      Tree:refresh(ev.file)\n      Actions.update(p)\n    end\n  end)\n\n  picker.list.win:on(\"TabEnter\", function(_, ev)\n    local p = ref()\n    if p and p:on_current_tab() then\n      Actions.update(p)\n    end\n  end)\n\n  picker.list.win:on(\"WinEnter\", function(_, ev)\n    local p = ref()\n    if p then\n      p._main:update()\n    end\n  end)\n\n  picker.list.win:on(\"DirChanged\", function(_, ev)\n    local p = ref()\n    if p then\n      p:set_cwd(svim.fs.normalize(ev.file))\n      p:find()\n    end\n  end)\n\n  if opts.diagnostics then\n    local dirty = false\n    local diag_update = Snacks.util.debounce(function()\n      dirty = false\n      local p = ref()\n      if p then\n        if require(\"snacks.explorer.diagnostics\").update(p:cwd()) then\n          p.list:set_target()\n          p:find()\n        end\n      end\n    end, { ms = 200 })\n    picker.list.win:on({ \"InsertLeave\", \"DiagnosticChanged\" }, function(_, ev)\n      dirty = dirty or ev.event == \"DiagnosticChanged\"\n      if vim.fn.mode() == \"n\" and dirty then\n        diag_update()\n      end\n    end)\n  end\n\n  -- schedule initial follow\n  if opts.follow_file then\n    picker.list.win:on({ \"WinEnter\", \"BufEnter\" }, function(_, ev)\n      vim.schedule(function()\n        if ev.buf ~= vim.api.nvim_get_current_buf() then\n          return\n        end\n        local p = ref()\n        if not p or p:is_focused() or not p:on_current_tab() or p.closed then\n          return\n        end\n        local win = vim.api.nvim_get_current_win()\n        if vim.api.nvim_win_get_config(win).relative ~= \"\" then\n          return\n        end\n        local file = vim.api.nvim_buf_get_name(ev.buf)\n        local item = p:current()\n        if item and item.file == norm(file) then\n          return\n        end\n        Actions.update(p, { target = file })\n      end)\n    end)\n    self.on_find = function()\n      local p = ref()\n      if p and buf_file then\n        Actions.update(p, { target = buf_file })\n      end\n    end\n  end\n  return self\nend\n\n---@param ctx snacks.picker.finder.ctx\nfunction State:setup(ctx)\n  local opts = ctx.picker.opts --[[@as snacks.picker.explorer.Config]]\n  if opts.watch then\n    require(\"snacks.explorer.watch\").watch()\n  end\n  return not ctx.filter:is_empty()\nend\n\n---@param opts snacks.picker.explorer.Config\nfunction M.setup(opts)\n  local searching = false\n  return Snacks.config.merge(opts, {\n    actions = {\n      confirm = Actions.actions.confirm,\n    },\n    filter = {\n      --- Trigger finder when pattern toggles between empty / non-empty\n      ---@param picker snacks.Picker\n      ---@param filter snacks.picker.Filter\n      transform = function(picker, filter)\n        local s = not filter:is_empty()\n        if searching ~= s then\n          searching = s\n          filter.meta.searching = searching\n          return true\n        end\n      end,\n    },\n    formatters = {\n      file = {\n        filename_only = opts.tree,\n      },\n    },\n  })\nend\n\n---@param picker snacks.Picker\nfunction M.get_state(picker)\n  if not M._state[picker] then\n    M._state[picker] = State.new(picker)\n  end\n  return M._state[picker]\nend\n\n---@param opts snacks.picker.explorer.Config\n---@type snacks.picker.finder\nfunction M.explorer(opts, ctx)\n  local state = M.get_state(ctx.picker)\n\n  ctx.picker.matcher.opts.keep_parents = false\n  if state:setup(ctx) then\n    ctx.picker.matcher.opts.keep_parents = true\n    return M.search(opts, ctx)\n  end\n\n  -- initial on_find (typically for follow_file), has to be done both for:\n  -- * regular explorer view\n  -- * when git status refreshes the view\n  local on_find = state.on_find\n  state.on_find = nil\n\n  if opts.git_status then\n    require(\"snacks.explorer.git\").update(ctx.filter.cwd, {\n      untracked = opts.git_untracked,\n      on_update = function()\n        if ctx.picker.closed then\n          return\n        end\n        ctx.picker.list:set_target()\n        ctx.picker:find({ on_done = on_find })\n      end,\n    })\n  end\n\n  if opts.diagnostics then\n    require(\"snacks.explorer.diagnostics\").update(ctx.filter.cwd)\n  end\n\n  return function(cb)\n    if on_find then\n      assert(ctx.picker.matcher.task:running())\n      ctx.picker.matcher.task:on(\"done\", vim.schedule_wrap(on_find))\n    end\n    local items = {} ---@type table<string, snacks.picker.explorer.Item>\n    local top = Tree:find(ctx.filter.cwd)\n    local last = {} ---@type table<snacks.picker.explorer.Node, snacks.picker.explorer.Item>\n    Tree:get(ctx.filter.cwd, function(node)\n      local parent = node.parent and items[node.parent.path] or nil\n      local status = node.status\n      if not status and parent and parent.dir_status then\n        status = parent.dir_status\n      end\n      local item = {\n        file = node.path,\n        dir = node.dir,\n        open = node.open,\n        dir_status = node.dir_status or parent and parent.dir_status,\n        text = node.path,\n        parent = parent,\n        hidden = node.hidden,\n        ignored = node.ignored,\n        status = (not node.dir or not node.open or opts.git_status_open) and status or nil,\n        last = true,\n        type = node.type,\n        severity = (not node.dir or not node.open or opts.diagnostics_open) and node.severity or nil,\n      }\n      if last[node.parent] then\n        last[node.parent].last = false\n      end\n      last[node.parent] = item\n      if top == node then\n        item.hidden = false\n        item.ignored = false\n      end\n      items[node.path] = item\n      cb(item)\n    end, { hidden = opts.hidden, ignored = opts.ignored, exclude = opts.exclude, include = opts.include })\n  end\nend\n\n---@param opts snacks.picker.explorer.Config\n---@type snacks.picker.finder\nfunction M.search(opts, ctx)\n  opts = Snacks.picker.util.shallow_copy(opts)\n  opts.cmd = \"fd\"\n  opts.cwd = ctx.filter.cwd\n  opts.notify = false\n  opts.args = {\n    \"--type\",\n    \"d\", -- include directories\n    \"--path-separator\", -- same everywhere\n    \"/\",\n  }\n  opts.dirs = { ctx.filter.cwd }\n  ctx.picker.list:set_target()\n\n  ---@type snacks.picker.explorer.Item\n  local root = {\n    file = opts.cwd,\n    dir = true,\n    open = true,\n    text = \"\",\n    sort = \"\",\n    internal = true,\n  }\n\n  local files = require(\"snacks.picker.source.files\").files(opts, ctx)\n\n  local dirs = {} ---@type table<string, snacks.picker.explorer.Item>\n  local last = {} ---@type table<snacks.picker.finder.Item, snacks.picker.finder.Item>\n\n  ---@async\n  return function(cb)\n    cb(root)\n\n    ---@param item snacks.picker.explorer.Item\n    local function add(item)\n      local dirname, basename = item.file:match(\"(.*)/(.*)\")\n      dirname, basename = dirname or \"\", basename or item.file\n      local parent = dirs[dirname] ~= item and dirs[dirname] or root\n\n      -- hierarchical sorting\n      if item.dir then\n        item.sort = parent.sort .. \"!\" .. basename .. \" \"\n      else\n        item.sort = parent.sort .. \"#\" .. basename .. \" \"\n      end\n      item.hidden = basename:sub(1, 1) == \".\"\n      item.text = item.text:sub(1, #opts.cwd) == opts.cwd and item.text:sub(#opts.cwd + 2) or item.text\n      local node = Tree:node(item.file)\n      if node then\n        item.dir = node.dir\n        item.type = node.type\n        item.status = (not node.dir or opts.git_status_open) and node.status or nil\n      end\n\n      if opts.tree then\n        -- tree\n        item.parent = parent\n        if not last[parent] or last[parent].sort < item.sort then\n          if last[parent] then\n            last[parent].last = false\n          end\n          item.last = true\n          last[parent] = item\n        end\n      end\n      -- add to picker\n      cb(item)\n    end\n\n    -- get files and directories\n    files(function(item)\n      ---@cast item snacks.picker.explorer.Item\n      item.cwd = nil -- we use absolute paths\n\n      -- Directories\n      if item.file:sub(-1) == \"/\" then\n        item.dir = true\n        item.file = item.file:sub(1, -2)\n        if dirs[item.file] then\n          dirs[item.file].internal = false\n          return\n        end\n        item.open = true\n        dirs[item.file] = item\n      end\n\n      -- Add parents when needed\n      for dir in Snacks.picker.util.parents(item.file, opts.cwd) do\n        if dirs[dir] then\n          break\n        else\n          dirs[dir] = {\n            text = dir,\n            file = dir,\n            dir = true,\n            open = true,\n            internal = true,\n          }\n          add(dirs[dir])\n        end\n      end\n\n      add(item)\n    end)\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/files.lua",
    "content": "local M = {}\n\nlocal uv = vim.uv or vim.loop\n\n---@type {cmd:string[], args:string[], enabled?:boolean, available?:boolean|string}[]\nlocal commands = {\n  {\n    cmd = { \"fd\", \"fdfind\" },\n    args = { \"--type\", \"f\", \"--type\", \"l\", \"--color\", \"never\", \"-E\", \".git\" },\n  },\n  {\n    cmd = { \"rg\" },\n    args = { \"--files\", \"--no-messages\", \"--color\", \"never\", \"-g\", \"!.git\" },\n  },\n  {\n    cmd = { \"find\" },\n    args = { \".\", \"-type\", \"f\", \"-not\", \"-path\", \"*/.git/*\" },\n    enabled = vim.fn.has(\"win-32\") == 0,\n  },\n}\n\n---@param cmd? string\n---@return string? cmd, string[]? args\nfunction M.get_cmd(cmd)\n  local checked = {} ---@type string[]\n  for _, command in ipairs(commands) do\n    if command.enabled ~= false and command.available ~= false and (not cmd or vim.tbl_contains(command.cmd, cmd)) then\n      if command.available then\n        assert(type(command.available) == \"string\", \"available must be a string\")\n        return command.available, vim.deepcopy(command.args)\n      end\n      for _, c in ipairs(command.cmd) do\n        table.insert(checked, c)\n        if vim.fn.executable(c) == 1 then\n          command.available = c\n          return c, vim.deepcopy(command.args)\n        end\n      end\n      command.available = false\n    end\n  end\n  checked = #checked == 0 and cmd and { cmd } or checked\n  checked = vim.tbl_map(function(c)\n    return \"`\" .. c .. \"`\"\n  end, checked)\n  Snacks.notify.error(\"No supported finder found:\\n- \" .. table.concat(checked, \"\\n-\"))\nend\n\nfunction M.get_fd()\n  return M.get_cmd(\"fd\")\nend\n\n---@param opts snacks.picker.files.Config\n---@param filter snacks.picker.Filter\nlocal function get_cmd(opts, filter)\n  local cmd, args = M.get_cmd(opts.cmd)\n  if not cmd or not args then\n    return\n  end\n  local is_fd, is_fd_rg, is_find, is_rg = cmd == \"fd\" or cmd == \"fdfind\", cmd ~= \"find\", cmd == \"find\", cmd == \"rg\"\n\n  -- exclude\n  for _, e in ipairs(opts.exclude or {}) do\n    if is_fd then\n      vim.list_extend(args, { \"-E\", e })\n    elseif is_rg then\n      vim.list_extend(args, { \"-g\", \"!\" .. e })\n    elseif is_find then\n      table.insert(args, \"-not\")\n      table.insert(args, \"-path\")\n      table.insert(args, e)\n    end\n  end\n\n  -- extensions\n  local ft = opts.ft or {}\n  ft = type(ft) == \"string\" and { ft } or ft\n  ---@cast ft string[]\n  for _, e in ipairs(ft) do\n    if is_fd then\n      table.insert(args, \"-e\")\n      table.insert(args, e)\n    elseif is_rg then\n      table.insert(args, \"-g\")\n      table.insert(args, \"*.\" .. e)\n    elseif is_find then\n      table.insert(args, \"-name\")\n      table.insert(args, \"*.\" .. e)\n    end\n  end\n\n  -- hidden\n  if opts.hidden and is_fd_rg then\n    table.insert(args, \"--hidden\")\n  elseif not opts.hidden and is_find then\n    vim.list_extend(args, { \"-not\", \"-path\", \"*/.*\" })\n  end\n\n  -- ignored\n  if opts.ignored and is_fd_rg then\n    args[#args + 1] = \"--no-ignore\"\n  end\n\n  -- follow\n  if opts.follow then\n    args[#args + 1] = \"-L\"\n  end\n\n  -- extra args\n  vim.list_extend(args, opts.args or {})\n\n  -- file glob\n  ---@type string?\n  local pattern, pargs = Snacks.picker.util.parse(filter.search)\n  vim.list_extend(args, pargs)\n\n  pattern = pattern ~= \"\" and pattern or nil\n  if pattern then\n    if is_fd then\n      table.insert(args, pattern)\n    elseif is_rg then\n      table.insert(args, \"--glob\")\n      table.insert(args, pattern)\n    elseif is_find then\n      table.insert(args, \"-name\")\n      table.insert(args, pattern)\n    end\n  end\n\n  -- dirs\n  local dirs = opts.dirs or {}\n  if opts.rtp then\n    vim.list_extend(dirs, Snacks.picker.util.rtp())\n  end\n  if #dirs > 0 then\n    dirs = vim.tbl_map(svim.fs.normalize, dirs) ---@type string[]\n    if is_fd and not pattern then\n      args[#args + 1] = \".\"\n    end\n    if is_find then\n      table.remove(args, 1)\n      for _, d in pairs(dirs) do\n        table.insert(args, 1, d)\n      end\n    else\n      vim.list_extend(args, dirs)\n    end\n  end\n\n  return cmd, args\nend\n\n---@param opts snacks.picker.files.Config\n---@type snacks.picker.finder\nfunction M.files(opts, ctx)\n  local cwd = not (opts.rtp or (opts.dirs and #opts.dirs > 0))\n      and svim.fs.normalize(opts and opts.cwd or uv.cwd() or \".\")\n    or nil\n  local cmd, args = get_cmd(opts, ctx.filter)\n  if not cmd then\n    return function() end\n  end\n  if opts.debug.files then\n    Snacks.notify(cmd .. \" \" .. table.concat(args or {}, \" \"))\n  end\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cmd = cmd,\n      args = args,\n      notify = not opts.live,\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        item.cwd = cwd\n        item.file = item.text\n      end,\n    }),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.proc.Config\n---@type snacks.picker.finder\nfunction M.zoxide(opts, ctx)\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cmd = \"zoxide\",\n      args = { \"query\", \"--list\" },\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        item.file = item.text\n        item.dir = true\n      end,\n    }),\n    ctx\n  )\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/gh.lua",
    "content": "local Actions = require(\"snacks.gh.actions\")\nlocal Api = require(\"snacks.gh.api\")\n\nlocal M = {}\n\nM.actions = setmetatable({}, {\n  __index = function(t, k)\n    if type(k) ~= \"string\" then\n      return\n    end\n    if not Actions.actions[k] then\n      return nil\n    end\n    ---@type snacks.picker.Action\n    local action = {\n      desc = Actions.actions[k].desc,\n      action = function(picker, item, action)\n        local items = picker:selected({ fallback = true })\n        if item.gh_item then\n          item = item.gh_item\n          items = { item }\n        end\n        ---@diagnostic disable-next-line: param-type-mismatch\n        return Actions.actions[k].action(item, {\n          picker = picker,\n          items = items,\n          action = action,\n        })\n      end,\n    }\n    rawset(t, k, action)\n    return action\n  end,\n})\n\n---@param opts snacks.picker.gh.list.Config\n---@type snacks.picker.finder\nfunction M.gh(opts, ctx)\n  if ctx.filter.search ~= \"\" then\n    opts.search = ctx.filter.search\n  end\n  ---@async\n  return function(cb)\n    Api.list(opts.type, function(items)\n      for _, item in ipairs(items) do\n        cb(item)\n      end\n    end, opts):wait()\n  end\nend\n\n---@param opts snacks.picker.gh.issue.Config\n---@type snacks.picker.finder\nfunction M.issue(opts, ctx)\n  return M.gh(\n    vim.tbl_extend(\"force\", {\n      type = \"issue\",\n    }, opts),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.gh.pr.Config\n---@type snacks.picker.finder\nfunction M.pr(opts, ctx)\n  return M.gh(\n    vim.tbl_extend(\"force\", {\n      type = \"pr\",\n    }, opts),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.gh.actions.Config\n---@type snacks.picker.finder\nfunction M.get_actions(opts, ctx)\n  opts = opts or {}\n  ---@async\n  return function(cb)\n    local item = opts.item\n    if not opts.item and not opts.number then\n      item = Api.current_pr()\n    end\n\n    if not item then\n      local required = { \"type\", \"repo\", \"number\" }\n      local missing = vim.tbl_filter(function(field)\n        return opts[field] == nil\n      end, required) ---@type string[]\n      if #missing > 0 then\n        Snacks.notify.error({\n          \"Missing required options for `Snacks.picker.gh_actions()`:\",\n          \"- `\" .. table.concat(missing, \", \") .. \"`\",\n          \"\",\n          \"Either provide the fields, or run in a git repo with a **current PR**.\",\n        }, { title = \"Snacks Picker GH Actions\" })\n        return\n      end\n      item = Api.get({ type = opts.type or \"pr\", repo = opts.repo, number = opts.number })\n      if not item then\n        Snacks.notify.error(\"snacks.picker.gh.get_actions: Failed to get item\")\n        return\n      end\n    end\n\n    local actions = ctx.async:schedule(function()\n      return Actions.get_actions(item, {\n        picker = ctx.picker,\n        items = { item },\n      })\n    end)\n    actions.gh_actions = nil -- remove this action\n    actions.gh_perform_action = nil -- remove this action\n    local items = {} ---@type snacks.picker.finder.Item[]\n    for name, action in pairs(actions) do\n      ---@class snacks.picker.gh.Action: snacks.picker.finder.Item\n      items[#items + 1] = {\n        text = Snacks.picker.util.text(action, { \"name\", \"desc\" }),\n        file = item.uri,\n        name = name,\n        item = item,\n        desc = action.desc or name,\n        action = action,\n      }\n    end\n    table.sort(items, function(a, b)\n      local pa = a.action.priority or 0\n      local pb = b.action.priority or 0\n      if pa ~= pb then\n        return pa > pb\n      end\n      return a.desc < b.desc\n    end)\n    for i, it in ipairs(items) do\n      it.text = (\"%d. %s\"):format(i, it.text)\n      cb(it)\n    end\n  end\nend\n\n---@param opts snacks.picker.gh.diff.Config\n---@type snacks.picker.finder\nfunction M.diff(opts, ctx)\n  opts = opts or {}\n  if not opts.pr then\n    Snacks.notify.error(\"snacks.picker.gh.diff: `opts.pr` is required\")\n    return {}\n  end\n  local cwd = ctx:git_root()\n  local args = { \"pr\", \"diff\", tostring(opts.pr) }\n  if opts.repo then\n    vim.list_extend(args, { \"--repo\", opts.repo })\n  end\n\n  opts.previewers.diff.style = \"fancy\" -- only fancy style support inline review comments\n\n  local Render = require(\"snacks.gh.render\")\n  local Diff = require(\"snacks.picker.source.diff\")\n  ---@async\n  return function(cb)\n    local item = Api.get({ type = \"pr\", repo = opts.repo, number = opts.pr })\n\n    -- fetch on the main thread since rendering uses non-fast APIs\n    local annotations = ctx.async:schedule(function()\n      return Render.annotations(item)\n    end)\n\n    Diff.diff(\n      ctx:opts({\n        cmd = \"gh\",\n        args = args,\n        cwd = cwd,\n        annotations = annotations,\n      }),\n      ctx\n    )(function(it)\n      it.gh_item = item\n      cb(it)\n    end)\n  end\nend\n\n---@param opts snacks.picker.gh.reactions.Config\n---@type snacks.picker.finder\nfunction M.reactions(opts, ctx)\n  if not opts.repo then\n    Snacks.notify.error(\"snacks.picker.gh.reactions: `opts.repo` is required\")\n    return {}\n  end\n  if not opts.number then\n    Snacks.notify.error(\"snacks.picker.gh.reactions: `opts.number` is required\")\n    return {}\n  end\n\n  local all = { \"+1\", \"-1\", \"laugh\", \"hooray\", \"confused\", \"heart\", \"rocket\", \"eyes\" }\n  ---@async\n  return function(cb)\n    local items = {} ---@type table<string, snacks.picker.finder.Item>\n    local user = Api.user()\n\n    ---@type {user:snacks.gh.User, content:string}[]\n    local reactions = Api.request_sync({\n      endpoint = (\"/repos/%s/issues/%s/reactions\"):format(opts.repo, opts.number),\n    })\n\n    for _, r in ipairs(reactions) do\n      if r.user.login == user.login then\n        items[r.content] = setmetatable({\n          text = r.content,\n          reaction = r.content,\n          added = true,\n        }, { __index = r })\n      end\n    end\n\n    for _, reaction in ipairs(all) do\n      cb(items[reaction] or {\n        text = reaction,\n        reaction = reaction,\n        added = false,\n      })\n    end\n  end\nend\n\n---@param opts snacks.picker.gh.labels.Config\n---@type snacks.picker.finder\nfunction M.labels(opts, ctx)\n  if not opts.repo then\n    Snacks.notify.error(\"snacks.picker.gh.labels: `opts.repo` is required\")\n    return {}\n  end\n  if not opts.number then\n    Snacks.notify.error(\"snacks.picker.gh.labels: `opts.number` is required\")\n    return {}\n  end\n\n  ---@async\n  return function(cb)\n    ---@type {labels: snacks.gh.Label[]}\n    local repo = Api.fetch_sync({\n      fields = { \"labels\" },\n      args = { \"repo\", \"view\", opts.repo },\n    })\n    local item = Api.get_cached(opts)\n    assert(item, \"Failed to get item for labels\")\n    local added = {} ---@type table<string, boolean>\n    for _, label in ipairs(item.labels or {}) do\n      added[label.name] = true\n    end\n    repo.labels = repo.labels or {}\n    table.sort(repo.labels, function(a, b)\n      if added[a.name] ~= added[b.name] then\n        return added[a.name] == true\n      end\n      return a.name:lower() < b.name:lower()\n    end)\n\n    for _, r in ipairs(repo.labels or {}) do\n      cb({\n        text = r.name,\n        label = r.name,\n        added = added[r.name] == true,\n        item = r,\n      })\n    end\n  end\nend\n\n---@param item snacks.picker.gh.Item\n---@type snacks.picker.format\nfunction M.format(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local a = Snacks.picker.util.align\n\n  local config = require(\"snacks.gh\").config()\n  -- Status Icon\n  local icons = config.icons[item.type]\n  local status = icons[item.status] and item.status or \"other\"\n  if status then\n    local icon = icons[status]\n    local icon_hl = \"SnacksGh\" .. Snacks.picker.util.title(item.type) .. Snacks.picker.util.title(status)\n    ret[#ret + 1] = { a(icon, 2), icon_hl }\n    ret[#ret + 1] = { \" \" }\n  end\n\n  -- Number / Hash\n  if item.hash then\n    ret[#ret + 1] = { a(item.hash, 8), \"SnacksPickerDimmed\" }\n  end\n\n  -- Updated At\n  -- if item.updated then\n  --   ret[#ret + 1] = { a(Snacks.picker.util.reltime(item.updated), 12), \"SnacksPickerGitDate\" }\n  -- end\n\n  -- Title\n  if item.title then\n    item.msg = item.title\n    Snacks.picker.highlight.extend(ret, Snacks.picker.format.commit_message(item, picker))\n  end\n\n  -- Author\n  if item.author and not item.item.author.is_bot then\n    ret[#ret + 1] = { \" \", nil }\n    ret[#ret + 1] = { \"@\" .. item.author, \"SnacksPickerGitAuthor\" }\n  end\n\n  -- Labels\n  for _, label in ipairs(item.item.labels or {}) do\n    ret[#ret + 1] = { \" \", nil }\n    local color = label.color or \"888888\"\n    local badge = Snacks.picker.highlight.badge(label.name, \"#\" .. color)\n    vim.list_extend(ret, badge)\n  end\n\n  return ret\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.preview_diff(ctx)\n  Snacks.picker.preview.diff(ctx)\n  local item = ctx.item.gh_item ---@type snacks.picker.gh.Item?\n  if item then\n    vim.b[ctx.buf].snacks_gh = {\n      repo = item.repo,\n      type = item.type,\n      number = item.number,\n    }\n  end\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.preview(ctx)\n  local config = require(\"snacks.gh\").config()\n  local item = ctx.item\n  item.wo = config.wo\n  item.bo = config.bo\n  item.preview_title = (\"%s %s %s\"):format(\n    config.icons.logo,\n    (item.type == \"issue\" and \"Issue\" or \"PR\"),\n    (item.hash or \"\")\n  )\n  return Snacks.picker.preview.file(ctx)\nend\n\n---@type snacks.picker.format\nfunction M.format_label(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local added = item.added\n  if picker.list:is_selected(item) then\n    added = not added -- reflect the change that will happen on action\n  end\n  ret[#ret + 1] = { added and \"󰱒 \" or \"󰄱 \", \"SnacksPickerDelim\" }\n  ret[#ret + 1] = { \" \" }\n  local color = item.item.color or \"888888\"\n  local badge = Snacks.picker.highlight.badge(item.label, \"#\" .. color)\n  vim.list_extend(ret, badge)\n  return ret\nend\n\n---@param item snacks.picker.gh.Action\n---@type snacks.picker.format\nfunction M.format_action(item, picker)\n  local ret = {} ---@type snacks.picker.Highlight[]\n\n  if item.action.icon then\n    ret[#ret + 1] = { item.action.icon, \"Special\" }\n    ret[#ret + 1] = { \" \" }\n  end\n\n  local count = picker:count()\n  local idx = tostring(item.idx)\n  idx = (\" \"):rep(#tostring(count) - #idx) .. idx\n  ret[#ret + 1] = { idx .. \".\", \"SnacksPickerIdx\" }\n\n  ret[#ret + 1] = { \" \" }\n\n  if item.desc then\n    ret[#ret + 1] = { item.desc or item.name }\n    Snacks.picker.highlight.highlight(ret, {\n      [\"#%d+\"] = \"Number\",\n    })\n  end\n  return ret\nend\n\n---@type snacks.picker.format\nfunction M.format_reaction(item, picker)\n  local config = require(\"snacks.gh\").config()\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local name = item.reaction\n  name = name == \"+1\" and \"thumbs_up\" or name == \"-1\" and \"thumbs_down\" or name\n  local added = item.added\n  if picker.list:is_selected(item) then\n    added = not added -- reflect the change that will happen on action\n  end\n  ret[#ret + 1] = { added and \"󰱒 \" or \"󰄱 \", \"SnacksPickerDelim\" }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { config.icons.reactions[name] or name }\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/git.lua",
    "content": "local M = {}\n\nlocal uv = vim.uv or vim.loop\n\nlocal commit_pat = (\"[a-z0-9]\"):rep(7)\n\n---@class snacks.picker.git.Args\n---@field args? string[] additional arguments to pass to `git`\n---@field cmd_args? string[] additional arguments to pass to the `git <cmd>``\n\n---@param cmd string\n---@param ... string|snacks.picker.git.Args\nfunction M.git(cmd, ...)\n  local args, cmd_args = {}, {} ---@type string[], string[]\n\n  for i = 1, select(\"#\", ...) do\n    local arg = select(i, ...)\n    if type(arg) == \"string\" then\n      cmd_args[#cmd_args + 1] = arg\n    else\n      vim.list_extend(args, arg.args or {})\n      vim.list_extend(cmd_args, arg.cmd_args or {})\n    end\n  end\n\n  local ret = { \"-c\", \"core.quotepath=false\" } ---@type string[]\n  vim.list_extend(ret, args)\n  ret[#ret + 1] = cmd\n  vim.list_extend(ret, cmd_args)\n  return ret\nend\n\n---@param opts snacks.picker.git.files.Config\n---@type snacks.picker.finder\nfunction M.files(opts, ctx)\n  local args = M.git(\"ls-files\", \"--exclude-standard\", \"--cached\", opts)\n  if opts.untracked then\n    table.insert(args, \"--others\")\n  elseif opts.submodules then\n    table.insert(args, \"--recurse-submodules\")\n  end\n  if not opts.cwd then\n    opts.cwd = ctx:git_root()\n    ctx.picker:set_cwd(opts.cwd)\n  end\n  local cwd = svim.fs.normalize(opts.cwd) or nil\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cmd = \"git\",\n      args = args,\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        item.cwd = cwd\n        item.file = item.text\n      end,\n    }),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.git.grep.Config\n---@type snacks.picker.finder\nfunction M.grep(opts, ctx)\n  if opts.need_search ~= false and ctx.filter.search == \"\" then\n    return function() end\n  end\n  local args = M.git(\"grep\", \"--line-number\", \"--column\", \"--no-color\", \"-I\", opts)\n  if opts.untracked then\n    table.insert(args, \"--untracked\")\n  elseif opts.submodules then\n    table.insert(args, \"--recurse-submodules\")\n  end\n  if opts.ignorecase then\n    table.insert(args, \"-i\")\n  end\n\n  local pattern, pargs = Snacks.picker.util.parse(ctx.filter.search)\n  table.insert(args, pattern)\n\n  args[#args + 1] = \"--\"\n  vim.list_extend(args, pargs)\n\n  local pathspec = type(opts.pathspec) == \"table\" and opts.pathspec or { opts.pathspec }\n  ---@cast pathspec string[]\n  vim.list_extend(args, pathspec)\n\n  if not opts.cwd then\n    opts.cwd = ctx:git_root()\n    ctx.picker:set_cwd(opts.cwd)\n  end\n  local cwd = svim.fs.normalize(opts.cwd) or nil\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cmd = \"git\",\n      args = args,\n      notify = false,\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        item.cwd = cwd\n        local file, line, col, text = item.text:match(\"^(.+):(%d+):(%d+):(.*)$\")\n        if not file then\n          if not item.text:match(\"WARNING\") then\n            Snacks.notify.error(\"invalid grep output:\\n\" .. item.text)\n          end\n          return false\n        else\n          item.line = text\n          item.file = file\n          item.pos = { tonumber(line), tonumber(col) - 1 }\n        end\n      end,\n    }),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.git.log.Config\n---@type snacks.picker.finder\nfunction M.log(opts, ctx)\n  local args = M.git(\n    \"log\",\n    \"--pretty=format:%h %s (%ch) <%an>\",\n    \"--abbrev-commit\",\n    \"--decorate\",\n    \"--date=short\",\n    \"--color=never\",\n    \"--no-show-signature\",\n    \"--no-patch\",\n    opts\n  )\n\n  if opts.author then\n    table.insert(args, \"--author=\" .. opts.author)\n  end\n\n  local file ---@type string?\n  if opts.current_line then\n    local cursor = vim.api.nvim_win_get_cursor(ctx.filter.current_win)\n    file = vim.api.nvim_buf_get_name(ctx.filter.current_buf)\n    local line = cursor[1]\n    args[#args + 1] = \"-L\"\n    args[#args + 1] = line .. \",+1:\" .. file\n  elseif opts.current_file then\n    file = vim.api.nvim_buf_get_name(ctx.filter.current_buf)\n    if opts.follow then\n      args[#args + 1] = \"--follow\"\n    end\n    args[#args + 1] = \"--\"\n    args[#args + 1] = file\n  end\n\n  if ctx.filter.search ~= \"\" then\n    vim.list_extend(args, { \"-S\", ctx.filter.search })\n  end\n\n  local Proc = require(\"snacks.picker.source.proc\")\n  file = file and svim.fs.normalize(file) or nil\n\n  local cwd = svim.fs.normalize(file and vim.fn.fnamemodify(file, \":h\") or opts and opts.cwd or uv.cwd() or \".\") or nil\n  cwd = Snacks.git.get_root(cwd) or cwd\n\n  local renames = { file } ---@type string[]\n  return function(cb)\n    if file then\n      -- detect renames\n      local is_rename = false\n      Proc.proc({\n        cmd = \"git\",\n        cwd = cwd,\n        args = M.git(\n          \"log\",\n          \"-z\",\n          \"--follow\",\n          \"--name-status\",\n          \"--pretty=format:''\",\n          \"--diff-filter=R\",\n          \"--\",\n          file,\n          opts\n        ),\n      }, ctx)(function(item)\n        for _, text in ipairs(vim.split(item.text, \"\\0\")) do\n          if text:find(\"^R%d%d%d$\") then\n            is_rename = true\n          elseif is_rename then\n            is_rename = false\n            renames[#renames + 1] = text\n          end\n        end\n      end)\n    end\n\n    Proc.proc(\n      ctx:opts({\n        cwd = cwd,\n        cmd = \"git\",\n        args = args,\n        ---@param item snacks.picker.finder.Item\n        transform = function(item)\n          local commit, msg, date, author = item.text:match(\"^(%S+) (.*) %((.*)%) <(.*)>$\")\n          if not commit then\n            Snacks.notify.error((\"failed to parse log item:\\n%q\"):format(item.text))\n            return false\n          end\n          item.cwd = cwd\n          item.commit = commit\n          item.msg = msg\n          item.date = date\n          item.author = author\n          item.file = file\n          item.files = renames\n        end,\n      }),\n      ctx\n    )(cb)\n  end\nend\n\n---@param opts snacks.picker.git.status.Config\n---@type snacks.picker.finder\nfunction M.status(opts, ctx)\n  local args = M.git(\"status\", \"-uall\", \"--porcelain=v1\", \"-z\", { args = { \"--no-pager\" } }, opts)\n  if opts.ignored then\n    table.insert(args, \"--ignored=matching\")\n  end\n\n  local cwd = ctx:git_root()\n  ctx.picker:set_cwd(cwd)\n\n  local prev ---@type snacks.picker.finder.Item?\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      sep = \"\\0\",\n      cwd = cwd,\n      cmd = \"git\",\n      args = args,\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        local status, file = item.text:match(\"^(..) (.+)$\")\n        if status then\n          item.cwd = cwd\n          item.status = status\n          item.file = file\n          prev = item\n        elseif prev and prev.status:find(\"R\") then\n          prev.rename = item.text\n          return false\n        else\n          return false\n        end\n      end,\n    }),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.git.diff.Config\n---@type snacks.picker.finder\nfunction M.diff(opts, ctx)\n  opts = opts or {}\n  local args = M.git(\"diff\", \"--no-color\", \"--no-ext-diff\", \"--diff-filter=u\", { args = { \"--no-pager\" } }, opts)\n  if opts.base then\n    vim.list_extend(args, { \"--merge-base\", opts.base })\n  end\n  if opts.staged then\n    table.insert(args, \"--cached\")\n  end\n\n  local cwd = ctx:git_root()\n  ctx.picker:set_cwd(cwd)\n\n  local Diff = require(\"snacks.picker.source.diff\")\n  local finders = {} ---@type snacks.picker.finder.result[]\n  finders[#finders + 1] = Diff.diff(\n    ctx:opts({\n      cmd = \"git\",\n      args = args,\n      cwd = cwd,\n    }),\n    ctx\n  )\n  if opts.staged == nil and opts.base == nil then\n    finders[#finders + 1] = Diff.diff(\n      ctx:opts({\n        cmd = \"git\",\n        args = vim.list_extend(vim.deepcopy(args), { \"--cached\" }),\n        cwd = cwd,\n      }),\n      ctx\n    )\n  end\n  return function(cb)\n    local items = {} ---@type snacks.picker.finder.Item[]\n    for f, finder in ipairs(finders) do\n      finder(function(item)\n        if not opts.base then\n          item.staged = opts.staged or f == 2\n        end\n        items[#items + 1] = item\n      end)\n    end\n    table.sort(items, function(a, b)\n      if a.file ~= b.file then\n        return a.file < b.file\n      end\n      return a.pos[1] < b.pos[1]\n    end)\n    for _, item in ipairs(items) do\n      cb(item)\n    end\n  end\nend\n\n---@param opts snacks.picker.git.branches.Config\n---@type snacks.picker.finder\nfunction M.branches(opts, ctx)\n  local args = M.git(\"branch\", \"--no-color\", \"-vvl\", { args = { \"--no-pager\" } }, opts)\n  if opts.all then\n    table.insert(args, \"--all\")\n  end\n  local cwd = ctx:git_root()\n\n  local patterns = {\n    -- stylua: ignore start\n    --- e.g. \"* (HEAD detached at f65a2c8) f65a2c8 chore(build): auto-generate docs\"\n    \"^(.)%s(%b())%s+(\" .. commit_pat .. \")%s*(.*)$\",\n    --- e.g. \"  main                       d2b2b7b [origin/main: behind 276] chore(build): auto-generate docs\"\n    \"^(.)%s(%S+)%s+(\".. commit_pat .. \")%s*(.*)$\",\n    -- stylua: ignore end\n  } ---@type string[]\n\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cwd = cwd,\n      cmd = \"git\",\n      args = args,\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        item.cwd = cwd\n        if item.text:find(\"HEAD.*%->\") then\n          return false\n        end\n        for p, pattern in ipairs(patterns) do\n          local status, branch, commit, msg = item.text:match(pattern)\n          if status then\n            local detached = p == 1\n            item.current = status == \"*\"\n            item.branch = not detached and branch or nil\n            item.commit = commit\n            item.msg = msg\n            item.detached = detached\n            return\n          end\n        end\n        Snacks.notify.warn(\"failed to parse branch: \" .. item.text)\n        return false -- skip items we could not parse\n      end,\n    }),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.git.Config\n---@type snacks.picker.finder\nfunction M.stash(opts, ctx)\n  local args = M.git(\"stash\", \"list\", { args = { \"--no-pager\" } }, opts)\n  local cwd = ctx:git_root()\n\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cwd = cwd,\n      cmd = \"git\",\n      args = args,\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        if item.text:find(\"autostash\", 1, true) then\n          return false\n        end\n        local stash, branch, msg = item.text:gsub(\": On (%S+):\", \": WIP on %1:\"):match(\"^(%S+): WIP on (%S+): (.*)$\")\n        if stash then\n          local commit, m = msg:match(\"^(\" .. commit_pat .. \") (.*)\")\n          item.cwd = cwd\n          item.stash = stash\n          item.branch = branch\n          item.commit = commit\n          item.msg = m or msg\n          return\n        end\n        Snacks.notify.warn(\"failed to parse stash:\\n```git\\n\" .. item.text .. \"\\n```\")\n        return false -- skip items we could not parse\n      end,\n    }),\n    ctx\n  )\nend\n\n---@class snacks.picker.git.Status\n---@field xy string\n---@field status \"modified\" | \"deleted\" | \"added\" | \"untracked\" | \"renamed\" | \"copied\" | \"ignored\"\n---@field unmerged? boolean\n---@field staged? boolean\n---@field priority? number\n\n---@param xy string\n---@return snacks.picker.git.Status\nfunction M.git_status(xy)\n  local ss = {\n    A = \"added\",\n    D = \"deleted\",\n    M = \"modified\",\n    R = \"renamed\",\n    C = \"copied\",\n    [\"?\"] = \"untracked\",\n    [\"!\"] = \"ignored\",\n  }\n  local prios = \"!?CRDAM\"\n\n  ---@param status string\n  ---@param unmerged? boolean\n  ---@param staged? boolean\n  local function s(status, unmerged, staged)\n    local prio = (prios:find(status, 1, true) or 0) + (unmerged and 20 or 0)\n    if not staged and not status:find(\"[!]\") then\n      prio = prio + 10\n    end\n    return {\n      xy = xy,\n      status = ss[status],\n      unmerged = unmerged,\n      staged = staged,\n      priority = prio,\n    }\n  end\n  ---@param c string\n  local function f(c)\n    return xy:gsub(\"T\", \"M\"):match(c) --[[@as string?]]\n  end\n\n  if f(\"%?%?\") then\n    return s(\"?\")\n  elseif f(\"!!\") then\n    return s(\"!\")\n  elseif f(\"UU\") then\n    return s(\"M\", true)\n  elseif f(\"DD\") then\n    return s(\"D\", true)\n  elseif f(\"AA\") then\n    return s(\"A\", true)\n  elseif f(\"U\") then\n    return s(f(\"A\") and \"A\" or \"D\", true)\n  end\n\n  local m = f(\"^([MADRC])\")\n  if m then\n    return s(m, nil, true)\n  end\n  m = f(\"([MADRC])$\")\n  if m then\n    return s(m)\n  end\n  error(\"unknown status: \" .. xy)\nend\n\n---@param a string\n---@param b string\nfunction M.merge_status(a, b)\n  if a == b then\n    return a\n  end\n  local as = M.git_status(a)\n  local bs = M.git_status(b)\n  if as.unmerged or bs.unmerged then\n    return as.priority > bs.priority and as.xy or bs.xy\n  end\n  if not as.staged or not bs.staged then\n    if as.status == bs.status then\n      return as.staged and b or a\n    end\n    return \" M\"\n  end\n  return \"M \"\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/grep.lua",
    "content": "local M = {}\n\nlocal uv = vim.uv or vim.loop\n\nlocal MATCH_SEP = \"󰄊󱥳󱥰\"\n\n---@param opts snacks.picker.grep.Config\n---@param filter snacks.picker.Filter\nlocal function get_cmd(opts, filter)\n  local cmd = \"rg\"\n  local args = {\n    \"--color=never\",\n    \"--no-heading\",\n    \"--with-filename\",\n    \"--line-number\",\n    \"--replace\",\n    (\"%s${0}%s\"):format(MATCH_SEP, MATCH_SEP),\n    \"--column\",\n    \"--smart-case\",\n    \"--max-columns=500\",\n    \"--max-columns-preview\",\n    \"--glob=!.bare\",\n    \"--glob=!.git\",\n    \"-0\",\n  }\n\n  args = vim.deepcopy(args)\n\n  -- exclude\n  for _, e in ipairs(opts.exclude or {}) do\n    vim.list_extend(args, { \"-g\", \"!\" .. e })\n  end\n\n  -- hidden\n  if opts.hidden then\n    table.insert(args, \"--hidden\")\n  else\n    table.insert(args, \"--no-hidden\")\n  end\n\n  -- ignored\n  if opts.ignored then\n    args[#args + 1] = \"--no-ignore\"\n  end\n\n  -- follow\n  if opts.follow then\n    args[#args + 1] = \"-L\"\n  end\n\n  local types = type(opts.ft) == \"table\" and opts.ft or { opts.ft }\n  ---@cast types string[]\n  for _, t in ipairs(types) do\n    args[#args + 1] = \"-t\"\n    args[#args + 1] = t\n  end\n\n  if opts.regex == false then\n    args[#args + 1] = \"--fixed-strings\"\n  end\n\n  local glob = type(opts.glob) == \"table\" and opts.glob or { opts.glob }\n  ---@cast glob string[]\n  for _, g in ipairs(glob) do\n    args[#args + 1] = \"-g\"\n    args[#args + 1] = g\n  end\n\n  -- extra args\n  vim.list_extend(args, opts.args or {})\n\n  -- search pattern\n  local pattern, pargs = Snacks.picker.util.parse(filter.search)\n  vim.list_extend(args, pargs)\n\n  args[#args + 1] = \"--\"\n  table.insert(args, pattern)\n\n  local paths = {} ---@type string[]\n\n  if opts.buffers then\n    for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n      local name = vim.api.nvim_buf_get_name(buf)\n      if name ~= \"\" and vim.bo[buf].buflisted and uv.fs_stat(name) then\n        paths[#paths + 1] = name\n      end\n    end\n  end\n  vim.list_extend(paths, opts.dirs or {})\n  if opts.rtp then\n    vim.list_extend(paths, Snacks.picker.util.rtp())\n  end\n\n  -- dirs\n  if #paths > 0 then\n    paths = vim.tbl_map(svim.fs.normalize, paths) ---@type string[]\n    vim.list_extend(args, paths)\n  end\n\n  return cmd, args\nend\n\n---@param opts snacks.picker.grep.Config\n---@type snacks.picker.finder\nfunction M.grep(opts, ctx)\n  if opts.need_search ~= false and ctx.filter.search == \"\" then\n    return function() end\n  end\n  local absolute = (opts.dirs and #opts.dirs > 0) or opts.buffers or opts.rtp\n  local cwd = not absolute and svim.fs.normalize(opts and opts.cwd or uv.cwd() or \".\") or nil\n  local cmd, args = get_cmd(opts, ctx.filter)\n  if opts.debug.grep then\n    Snacks.notify.info(\"grep: \" .. cmd .. \" \" .. table.concat(args, \" \"))\n  end\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      notify = false, -- never notify on grep errors, since it's impossible to know if the error is due to the search pattern\n      cmd = cmd,\n      args = args,\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        item.cwd = cwd\n        -- Split on NUL byte (which comes from rg's -0 flag)\n        local file_sep = item.text:find(\"\\0\")\n        if not file_sep then\n          if not item.text:match(\"WARNING\") then\n            Snacks.notify.error(\"invalid grep output:\\n\" .. item.text)\n          end\n          return false\n        end\n        local file = item.text:sub(1, file_sep - 1)\n        local rest = item.text:sub(file_sep + 1)\n        ---@type string?, string?, string?\n        local line, col, text = rest:match(\"^(%d+):(%d+):(.*)$\")\n        if not (line and col and text) then\n          if not item.text:match(\"WARNING\") then\n            Snacks.notify.error(\"invalid grep output:\\n\" .. item.text:gsub(\"%z\", \" \"))\n          end\n          return false\n        end\n        item.text = file .. \":\" .. rest:gsub(MATCH_SEP, \"\")\n\n        -- indices of matches\n        local from = tonumber(col)\n        item.pos = { tonumber(line), from - 1 }\n\n        item.resolve = function()\n          local positions = {} ---@type number[]\n          local offset = 0\n          local in_match = false\n          while from < #text do\n            local idx = text:find(MATCH_SEP, from, true)\n            if not idx then\n              break\n            end\n            if in_match then\n              for i = from, idx - 1 do\n                positions[#positions + 1] = i - offset\n              end\n              item.end_pos = item.end_pos or { item.pos[1], idx - offset - 1 }\n            end\n            in_match = not in_match\n            offset = offset + #MATCH_SEP\n            from = idx + #MATCH_SEP\n          end\n          item.positions = #positions > 0 and positions or nil\n          item.line = text:gsub(MATCH_SEP, \"\")\n        end\n\n        item.file = file\n      end,\n    }),\n    ctx\n  )\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/help.lua",
    "content": "local M = {}\n\n---@param opts snacks.picker.help.Config\n---@type snacks.picker.finder\nfunction M.help(opts, ctx)\n  local langs = opts.lang or vim.opt.helplang:get() ---@type string[]\n  local rtp = vim.o.runtimepath\n  if package.loaded.lazy then\n    rtp = rtp .. \",\" .. table.concat(require(\"lazy.core.util\").get_unloaded_rtp(\"\"), \",\")\n  end\n  local files = vim.fn.globpath(rtp, \"doc/*\", true, true) ---@type string[]\n\n  if not vim.tbl_contains(langs, \"en\") then\n    langs[#langs + 1] = \"en\"\n  end\n\n  local tag_files = {} ---@type string[]\n\n  for _, file in ipairs(files) do\n    local name = vim.fn.fnamemodify(file, \":t\")\n    local lang = \"en\"\n    if name == \"tags\" or name:sub(1, 5) == \"tags-\" then\n      lang = name:match(\"^tags%-(..)$\") or lang\n      if vim.tbl_contains(langs, lang) then\n        tag_files[#tag_files + 1] = file\n      end\n    end\n  end\n\n  ---@async\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    local done = {} ---@type table<string, boolean>\n\n    for _, file in ipairs(tag_files) do\n      local dir = vim.fs.dirname(file)\n      for line in io.lines(file) do\n        local fields = vim.split(line, string.char(9), { plain = true })\n        local tag = fields[1]\n        if not line:match(\"^!_TAG_\") and #fields >= 3 and not done[tag] then\n          done[tag] = true\n          ---@type snacks.picker.finder.Item\n          local item = {\n            text = tag,\n            tag = tag,\n            file = dir .. \"/\" .. fields[2],\n            search = \"/\\\\V\" .. fields[3]:sub(2),\n          }\n          if tag:find(\"^[vbg]?:\") or tag:find(\"^/\") then\n            item.ft = \"vim\"\n          elseif tag:find(\"%(%)$\") then\n            item.ft = \"lua\"\n          elseif tag:find(\"^'.*'$\") then\n            item.text_hl = \"String\"\n          elseif tag:find(\"^E%d+$\") then\n            item.text_hl = \"Error\"\n          elseif tag:find(\"^hl%-\") then\n            item.text_hl = tag:sub(4)\n          end\n          if item.file then\n            cb(item)\n          end\n        end\n      end\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/icons.lua",
    "content": "local M = {}\n\n---@class snacks.picker.icons.Source\n---@field url string\n---@field v? number\n---@field priority? number\n---@field build fun(data:table):snacks.picker.Icon[]\n\n---@alias snacks.picker.icons.source.Item {[1]:string, [2]:string}|{icon:string, name:string, category:string}\n\nlocal NERDFONTS_SETS = {\n  cod = \"Codicons\",\n  dev = \"Devicons\",\n  fa = \"Font Awesome\",\n  fae = \"Font Awesome Extension\",\n  iec = \"IEC Power Symbols\",\n  linux = \"Font Logos\",\n  logos = \"Font Logos\",\n  oct = \"Octicons\",\n  ple = \"Powerline Extra\",\n  pom = \"Pomicons\",\n  seti = \"Seti-UI\",\n  weather = \"Weather Icons\",\n  md = \"Material Design Icons\",\n}\n\n---@param source string\nlocal function custom_source(source, url)\n  ---@type snacks.picker.icons.Source\n  return {\n    v = 3,\n    url = url,\n    build = function(data)\n      ---@cast data snacks.picker.icons.source.Item[]\n      local ret = {} ---@type snacks.picker.Icon[]\n      for _, info in ipairs(data) do\n        table.insert(ret, {\n          name = vim.trim(info.name or info[2] or \"\"),\n          icon = vim.trim(info.icon or info[1] or \"\"),\n          category = info.category,\n          source = source,\n        })\n      end\n      return ret\n    end,\n  }\nend\n\n---@type table<string, snacks.picker.icons.Source>\nM.sources = {\n  nerd_fonts = {\n    priority = 10,\n    url = \"https://github.com/ryanoasis/nerd-fonts/raw/refs/heads/master/glyphnames.json\",\n    v = 4,\n    build = function(data)\n      ---@cast data table<string, {char:string, code:string}>\n      local ret = {} ---@type snacks.picker.Icon[]\n      for name, info in pairs(data) do\n        if name ~= \"METADATA\" then\n          local font, icon = name:match(\"^([%w_]+)%-(.*)$\")\n          if not font then\n            error(\"Invalid icon name: \" .. name)\n          end\n          table.insert(ret, {\n            name = icon,\n            icon = info.char,\n            source = \"nerd fonts\",\n            category = NERDFONTS_SETS[font] or font,\n          })\n        end\n      end\n      return ret\n    end,\n  },\n  emoji = {\n    url = \"https://raw.githubusercontent.com/muan/unicode-emoji-json/refs/heads/main/data-by-emoji.json\",\n    priority = 20,\n    v = 4,\n    build = function(data)\n      ---@cast data table<string, {name:string, slug:string, group:string}>\n      local ret = {} ---@type snacks.picker.Icon[]\n      for icon, info in pairs(data) do\n        table.insert(ret, {\n          name = info.name,\n          icon = icon,\n          source = \"emoji\",\n          category = info.group,\n        })\n      end\n      return ret\n    end,\n  },\n}\n\n---@class snacks.picker.Icon: snacks.picker.finder.Item\n---@field icon string\n---@field name string\n---@field source string\n---@field category string\n---@field desc? string\n\n---@param source_name string\nlocal function load(source_name)\n  local source = M.sources[source_name]\n  if not source then\n    Snacks.notify.error(\"Unknown icon source: \" .. source_name)\n    return {}\n  end\n\n  -- Load from local file if not a URL\n  if not source.url:find(\"^https?://\") then\n    local fd = assert(io.open(source.url, \"r\"))\n    local data = fd:read(\"*a\")\n    fd:close()\n    return source.build(vim.json.decode(data))\n  end\n\n  local parts = { source_name, \"v\" .. (source.v or 1), \"-\", vim.fn.sha256(source.url):sub(1, 8), \".json\" }\n  local file = vim.fn.stdpath(\"cache\") .. \"/snacks/picker/icons/\" .. table.concat(parts, \"\")\n  vim.fn.mkdir(vim.fn.fnamemodify(file, \":h\"), \"p\")\n  if vim.fn.filereadable(file) == 1 then\n    local fd = assert(io.open(file, \"r\"))\n    local data = fd:read(\"*a\")\n    fd:close()\n    return vim.json.decode(data) ---@type snacks.picker.Icon[]\n  end\n\n  Snacks.notify(\"Fetching `\" .. source_name .. \"` icons\")\n  if vim.fn.executable(\"curl\") == 0 then\n    Snacks.notify.error(\"`curl` is required to fetch icons\")\n    return {}\n  end\n  local out = vim.fn.system({ \"curl\", \"-s\", \"-L\", source.url })\n  if vim.v.shell_error ~= 0 then\n    Snacks.notify.error(out, { title = \"Icons Picker\" })\n    return {}\n  end\n  local icons = source.build(vim.json.decode(out))\n  local fd = assert(io.open(file, \"w\"))\n  fd:write(vim.json.encode(icons))\n  fd:close()\n  return icons\nend\n\n---@param opts snacks.picker.icons.Config\n---@type snacks.picker.finder\nfunction M.icons(opts)\n  local ret = {} ---@type snacks.picker.Icon[]\n\n  for source, url in pairs(opts.custom_sources or {}) do\n    M.sources[source] = custom_source(source, url)\n  end\n\n  local sources = opts.icon_sources or vim.tbl_keys(M.sources)\n  table.sort(sources, function(a, b)\n    local sa = M.sources[a] and M.sources[a].priority or 0\n    local sb = M.sources[b] and M.sources[b].priority or 0\n    return sa > sb\n  end)\n\n  for _, source in ipairs(sources) do\n    vim.list_extend(ret, load(source))\n  end\n  for _, icon in ipairs(ret) do\n    icon.text = Snacks.picker.util.text(icon, { \"source\", \"category\", \"name\" })\n    icon.data = icon.icon\n  end\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/lazy.lua",
    "content": "local M = {}\n\n---@type snacks.picker.finder\nfunction M.spec(opts, ctx)\n  local spec = require(\"lazy.core.config\").spec\n  local Util = require(\"lazy.core.util\")\n  local paths = {} ---@type string[]\n  for _, import in ipairs(spec.modules) do\n    Util.lsmod(import, function(_, modpath)\n      paths[#paths + 1] = modpath\n    end)\n  end\n  local names = {} ---@type string[]\n  for _, frag in pairs(spec.meta.fragments.fragments) do\n    local name = frag.spec[1] or frag.name\n    if not vim.tbl_contains(names, name) then\n      names[#names + 1] = name\n    end\n  end\n  local regex = \"\\\\M\\\\['\\\"]\\\\(\" .. table.concat(names, \"\\\\|\") .. \"\\\\)\\\\['\\\"]\"\n  local re = vim.regex(regex)\n  local ret = {} ---@type snacks.picker.finder.Item[]\n  for _, path in ipairs(paths) do\n    local lines = Snacks.picker.util.lines(path)\n    for l, line in ipairs(lines) do\n      local from, to = re:match_str(line)\n      if from then\n        ret[#ret + 1] = {\n          file = path,\n          line = line,\n          text = line,\n          pos = { l, from },\n          end_pos = { l, to },\n        }\n      end\n    end\n  end\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/lines.lua",
    "content": "local M = {}\n\n---@param opts snacks.picker.lines.Config\n---@type snacks.picker.finder\nfunction M.lines(opts, ctx)\n  local buf = opts.buf or ctx.filter.current_buf\n  buf = buf == 0 and vim.api.nvim_get_current_buf() or buf\n  local extmarks = require(\"snacks.picker.util.highlight\").get_highlights({ buf = buf, extmarks = true })\n  local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)\n  local items = {} ---@type snacks.picker.finder.Item[]\n  for l, line in ipairs(lines) do\n    ---@type snacks.picker.finder.Item\n    local item = {\n      buf = buf,\n      text = line,\n      pos = { l, (line:find(\"%S\") or 1) - 1 },\n      highlights = extmarks[l],\n    }\n    items[#items + 1] = item\n  end\n  return items\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/lsp/config.lua",
    "content": "---@diagnostic disable: await-in-sync\nlocal M = {}\n\nlocal has_11 = vim.fn.has(\"nvim-0.11\") == 1\n\n---@class snacks.picker.lsp.config.Item: snacks.picker.finder.Item\n---@field name string\n---@field config? vim.lsp.ClientConfig\n---@field docs? string\n---@field buffers table<number, boolean>\n---@field attached? boolean\n---@field attached_buf? boolean\n---@field enabled? boolean\n---@field installed? boolean\n---@field deprecated? boolean\n---@field cmd? string[]\n---@field bin? string\n\n---@class snacks.picker.lsp.config.Config\n---@field config vim.lsp.Config\n---@field enabled? boolean\n---@field docs? string\n---@field deprecated? boolean\n\n---@param name string\nlocal function is_enabled(name)\n  if has_11 then\n    return vim.lsp.is_enabled(name)\n  end\n  local lspconfig = require(\"lspconfig.configs\")\n  return lspconfig[name] and lspconfig[name].manager ~= nil\nend\n\n---@param name string\nlocal function get_config(name)\n  local modpath = vim.api.nvim_get_runtime_file(\"lsp/\" .. name .. \".lua\", false)[1]\n  local ret = { config = {} } ---@type snacks.picker.lsp.config.Config\n  local deprecate = vim.deprecate\n  vim.deprecate = function()\n    ret.deprecated = true\n  end\n  local ok, config = pcall(function()\n    return has_11 and vim.lsp.config[name] or loadfile(modpath)() or {}\n  end)\n  vim.deprecate = deprecate\n  ret.config = ok and config or {}\n  ret.enabled = is_enabled(name)\n  local lines = modpath and Snacks.picker.util.lines(modpath) or {}\n  local header = {} ---@type string[]\n  for _, line in ipairs(lines) do\n    if line:match(\"^%s*%-%-\") then\n      if not line:match(\"@brief\") then\n        header[#header + 1] = line:gsub(\"^%s*%-%-+%s?\", \"\")\n      end\n    elseif not line:match(\"^%s*$\") then\n      break\n    end\n  end\n  ret.docs = vim.trim(table.concat(header, \"\\n\"))\n  return ret\nend\n\n---@param opts snacks.picker.lsp.config.Config\n---@type snacks.picker.finder\nfunction M.find(opts, ctx)\n  local all = vim.api.nvim_get_runtime_file(\"lsp/*.lua\", true)\n  local available = {} ---@type table<string, string>\n  for _, f in ipairs(all) do\n    local name = f:match(\"([^/\\\\]+)%.lua$\")\n    if name then\n      available[name] = name\n    end\n  end\n\n  for name in pairs(has_11 and vim.lsp.config._configs or {}) do\n    available[name] = name\n  end\n\n  if vim.tbl_count(available) == 0 then\n    Snacks.notify.warn(\"No LSP configurations found?\")\n    return {}\n  end\n  local main_buf = vim.api.nvim_win_get_buf(ctx.picker.main)\n\n  ---@param item snacks.picker.lsp.config.Item\n  local function resolve(item)\n    local mod = get_config(item.name)\n    item.docs = item.docs or mod.docs\n    item.config = item.config or mod.config\n    item.cmd = item.cmd or mod.config.cmd\n    item.enabled = item.enabled or mod.enabled\n    item.deprecated = mod.deprecated\n  end\n\n  local items = {} ---@type table<string, snacks.picker.lsp.config.Item>\n  for _, client in ipairs(vim.lsp.get_clients()) do\n    items[client.name] = items[client.name]\n      or {\n        name = client.name,\n        buffers = {},\n        config = client.config,\n        attached = true,\n        enabled = true,\n        cmd = client.config.cmd,\n      }\n    for buf in pairs(client.attached_buffers) do\n      items[client.name].buffers[buf] = true\n    end\n    items[client.name].attached_buf = items[client.name].buffers[main_buf]\n  end\n\n  for name in pairs(available) do\n    items[name] = items[name] or {\n      name = name,\n      buffers = {},\n    }\n    items[name].resolve = resolve\n  end\n\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    local bins = Snacks.picker.util.get_bins()\n    for name, item in pairs(items) do\n      Snacks.picker.util.resolve(item)\n      local config = item.config or {}\n      local cmd = item.cmd or type(config.cmd) == \"table\" and config.cmd or nil\n      local bin ---@type string?\n      local installed = false\n      if type(cmd) == \"table\" and #cmd > 0 then\n        ---@type string[]\n        cmd = vim.deepcopy(cmd)\n        cmd[1] = svim.fs.normalize(cmd[1])\n        if cmd[1]:find(\"/\") then\n          installed = vim.fn.filereadable(cmd[1]) == 1\n          bin = cmd[1]\n        else\n          bin = bins[cmd[1]] or cmd[1]\n          installed = bins[cmd[1]] ~= nil\n        end\n        cmd[1] = vim.fs.basename(cmd[1])\n      end\n      local want = (not opts.installed or installed) and (not opts.configured or item.enabled)\n      if opts.attached == true and not item.attached then\n        want = false\n      elseif type(opts.attached) == \"number\" then\n        local buf = opts.attached == 0 and main_buf or opts.attached\n        if not item.buffers[buf] then\n          want = false\n        end\n      end\n      want = want and not item.deprecated\n      if want then\n        cb({\n          name = name,\n          cmd = cmd,\n          bin = bin,\n          installed = installed,\n          enabled = item.enabled or false,\n          buffers = item.buffers,\n          attached = item.attached or false,\n          attached_buf = item.attached_buf or false,\n          text = name .. \" \" .. table.concat(config.filetypes or {}, \" \"),\n          docs = item.docs,\n          config = config,\n        })\n      end\n    end\n  end\nend\n\n---@param item snacks.picker.Item\n---@param picker snacks.Picker\nfunction M.format(item, picker)\n  local a = Snacks.picker.util.align\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local config = item.config ---@type vim.lsp.ClientConfig\n  local icons = picker.opts.icons.lsp\n  if item.attached_buf then\n    ret[#ret + 1] = { a(icons.attached, 2), \"SnacksPickerLspAttachedBuf\" }\n  elseif item.attached then\n    ret[#ret + 1] = { a(icons.attached, 2), \"SnacksPickerLspAttached\" }\n  elseif item.enabled then\n    ret[#ret + 1] = { a(icons.enabled, 2), \"SnacksPickerLspEnabled\" }\n  elseif item.installed then\n    ret[#ret + 1] = { a(icons.disabled, 2), \"SnacksPickerLspDisabled\" }\n  else\n    ret[#ret + 1] = { a(icons.unavailable, 2), \"SnacksPickerLspUnavailable\" }\n  end\n  ret[#ret + 1] = { a(item.name, 20) }\n  for _, ft in ipairs(config.filetypes or {}) do\n    ret[#ret + 1] = { \" \" }\n    local icon, hl = Snacks.util.icon(ft, \"filetype\")\n    ret[#ret + 1] = { a(icon, 2), hl }\n    ret[#ret + 1] = { ft, \"SnacksPickerDimmed\" }\n  end\n\n  return ret\nend\n\n---@param ctx snacks.picker.preview.ctx\nfunction M.preview(ctx)\n  local config = ctx.item.config ---@type vim.lsp.ClientConfig\n  local item = ctx.item --[[@as snacks.picker.lsp.config.Item]]\n  local lines = {} ---@type string[]\n  lines[#lines + 1] = \"# `\" .. item.name .. \"`\"\n  lines[#lines + 1] = \"\"\n\n  ---@param path string\n  local function norm(path)\n    return vim.fn.fnamemodify(path, \":p:~\"):gsub(\"[\\\\/]$\", \"\")\n  end\n\n  local function list(values)\n    return table.concat(\n      vim.tbl_map(function(v)\n        return \"`\" .. vim.inspect(v) .. \"`\"\n      end, values),\n      \", \"\n    )\n  end\n\n  if item.cmd then\n    local cmd = type(item.cmd) == \"function\" and \"<function>\" or table.concat(item.cmd, \" \")\n    lines[#lines + 1] = \"- **cmd**: `\" .. cmd .. \"`\"\n  end\n\n  if item.installed then\n    lines[#lines + 1] = \"- **installed**: `\" .. norm(item.bin) .. \"`\"\n    lines[#lines + 1] = \"- **enabled**: \" .. (item.enabled and \"yes\" or \"no\")\n  else\n    lines[#lines + 1] = \"- **installed**: \" .. (item.bin and \"`\" .. item.bin .. \"` \" or \"\") .. \"not installed\"\n  end\n  local ft = config.filetypes or {}\n  if #ft > 0 then\n    lines[#lines + 1] = \"- **filetypes**: \" .. list(ft)\n  end\n\n  -- root markers\n  local markers = config.root_markers or {}\n  if #markers > 0 then\n    lines[#lines + 1] = \"- **root markers**: \" .. list(markers)\n  end\n\n  local clients = vim.lsp.get_clients({ name = item.name })\n  if #clients > 0 then\n    for _, client in ipairs(clients) do\n      lines[#lines + 1] = \"\"\n      lines[#lines + 1] = \"## Client [id=\" .. client.id .. \"]\"\n      lines[#lines + 1] = \"\"\n\n      -- server info\n      for k, v in pairs(client.server_info or {}) do\n        lines[#lines + 1] = (\"- **%s**: `%s`\"):format(k, v)\n      end\n\n      -- workspaces\n      local roots = {} ---@type string[]\n      for _, ws in ipairs(client.workspace_folders or {}) do\n        roots[#roots + 1] = vim.uri_to_fname(ws.uri)\n      end\n      roots = #roots == 0 and { client.root_dir } or roots\n      if #roots > 0 then\n        if #roots > 1 then\n          lines[#lines + 1] = \"- **workspace**:\"\n          for _, root in ipairs(roots) do\n            lines[#lines + 1] = \"    - `\" .. norm(root) .. \"`\"\n          end\n        else\n          lines[#lines + 1] = \"- **workspace**: `\" .. norm(roots[1]) .. \"`\"\n        end\n      end\n\n      -- buffers\n      lines[#lines + 1] = \"- **buffers**: \" .. list(vim.tbl_keys(client.attached_buffers))\n\n      local function format_cap(method, value)\n        if not value then\n          return\n        end\n        value = type(value) == \"table\" and value or {}\n        ---@cast value table\n        local details = {} ---@type string[]\n\n        local checks = {\n          [\"workspace/executeCommand\"] = \"commands\",\n          [\"textDocument/codeAction\"] = \"codeActionKinds\",\n        }\n        for m, k in pairs(checks) do\n          if method == m and type(value[k]) == \"table\" then\n            details = value[k] --[[@as string[] ]]\n            break\n          end\n        end\n\n        lines[#lines + 1] = (\"  *  **%s**:%s\"):format(method, #details > 0 and \"\" or \" `true`\")\n        if #details > 0 then\n          for _, detail in ipairs(details) do\n            lines[#lines + 1] = \"    - `\" .. detail .. \"`\"\n          end\n        end\n      end\n\n      -- server capabilities\n      local methods = vim.tbl_keys(vim.lsp.protocol._request_name_to_server_capability or {}) --[[@as string[] ]]\n      table.sort(methods)\n      if #methods > 0 then\n        lines[#lines + 1] = \"- **server capabilities**:\"\n        for _, method in ipairs(methods) do\n          local cap = vim.lsp.protocol._request_name_to_server_capability[method]\n          local value = vim.tbl_get(client.server_capabilities, unpack(cap))\n          format_cap(method, value)\n        end\n      end\n\n      -- dynamic capabilities\n      methods = vim.tbl_keys(vim.tbl_get(client, \"dynamic_capabilities\", \"capabilities\") or {}) --[[@as string[] ]]\n      table.sort(methods)\n      if #methods > 0 then\n        lines[#lines + 1] = \"- **dynamic capabilities**:\"\n        for _, cap in ipairs(methods) do\n          local method = table.concat(vim.lsp.protocol._provider_to_client_registration[cap] or {}, \"/\")\n          local regs = client.dynamic_capabilities.capabilities[cap]\n          for _, reg in ipairs(regs or {}) do\n            format_cap(method, reg.registerOptions or {})\n          end\n        end\n      end\n\n      -- settings\n      local settings = vim.inspect(client.settings)\n      if not vim.tbl_isempty(client.settings) then\n        lines[#lines + 1] = \"- **settings**:\"\n        lines[#lines + 1] = \"```lua\\n\" .. settings .. \"\\n```\"\n      end\n\n      -- init options\n      if not vim.tbl_isempty(client.config.init_options or {}) then\n        local init_options = vim.inspect(client.config.init_options)\n        lines[#lines + 1] = \"- **init options**:\"\n        lines[#lines + 1] = \"```lua\\n\" .. init_options .. \"\\n```\"\n      end\n    end\n  end\n\n  if item.docs then\n    lines[#lines + 1] = \"\"\n    lines[#lines + 1] = \"## Docs\"\n    lines[#lines + 1] = \"\"\n    lines[#lines + 1] = item.docs\n  end\n  ctx.preview:set_lines(lines)\n  ctx.preview:highlight({ ft = \"markdown\" })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/lsp/init.lua",
    "content": "---@diagnostic disable: await-in-sync\nlocal Async = require(\"snacks.picker.util.async\")\n\n---@module 'uv'\n\nlocal M = {}\n\n---@alias lsp.Symbol lsp.SymbolInformation|lsp.DocumentSymbol\n---@alias lsp.Loc lsp.LocationLink|lsp.Location\n\n---@class snacks.picker.lsp.Loc: lsp.Location\n---@field encoding string\n---@field resolved? boolean\n\nlocal kinds = nil ---@type table<lsp.SymbolKind, string>\n\n--- Gets the original symbol kind name from its number.\n--- Some plugins override the symbol kind names, so this function is needed to get the original name.\n---@param kind lsp.SymbolKind\n---@return string\nfunction M.symbol_kind(kind)\n  if not kinds then\n    kinds = {}\n    for k, v in pairs(vim.lsp.protocol.SymbolKind) do\n      if type(v) == \"number\" then\n        kinds[v] = k\n      end\n    end\n  end\n  return kinds[kind] or \"Unknown\"\nend\n\n--- Neovim 0.11 uses a lua class for clients, while older versions use a table.\n--- Wraps older style clients to be compatible with the new style.\n---@param client vim.lsp.Client\n---@return vim.lsp.Client\nlocal function wrap(client)\n  local meta = getmetatable(client)\n  if meta and meta.request then\n    return client\n  end\n  ---@diagnostic disable-next-line: undefined-field\n  if client.wrapped then\n    return client\n  end\n  local methods = { \"request\", \"supports_method\", \"cancel_request\", \"notify\" }\n  -- old style\n  return setmetatable({ wrapped = true }, {\n    __index = function(_, k)\n      if k == \"supports_method\" then\n        -- supports_method doesn't support the bufnr argument\n        return function(_, method)\n          return client[k](method)\n        end\n      end\n      if vim.tbl_contains(methods, k) then\n        return function(_, ...)\n          return client[k](...)\n        end\n      end\n      return client[k]\n    end,\n  })\nend\n\n---@param item snacks.picker.finder.Item\n---@param result lsp.Loc\n---@param client vim.lsp.Client\nfunction M.add_loc(item, result, client)\n  ---@type snacks.picker.lsp.Loc\n  local loc = {\n    uri = result.uri or result.targetUri,\n    range = result.range or result.targetSelectionRange,\n    encoding = client.offset_encoding,\n  }\n  item.loc = loc\n  item.pos = { loc.range.start.line + 1, loc.range.start.character }\n  item.end_pos = { loc.range[\"end\"].line + 1, loc.range[\"end\"].character }\n  item.file = vim.uri_to_fname(loc.uri)\n  return item\nend\n\n---@param buf number\n---@param method string\n---@return vim.lsp.Client[]\nfunction M.get_clients(buf, method)\n  ---@param client vim.lsp.Client\n  local clients = vim.tbl_map(function(client)\n    return wrap(client)\n    ---@diagnostic disable-next-line: deprecated\n  end, (vim.lsp.get_clients or vim.lsp.get_active_clients)({ bufnr = buf }))\n  ---@param client vim.lsp.Client\n  return vim.tbl_filter(function(client)\n    return client:supports_method(method, buf)\n    ---@diagnostic disable-next-line: deprecated\n  end, clients)\nend\n\n---@class snacks.picker.lsp.Requester\n---@field async snacks.picker.Async\n---@field requests table<string, {client_id:number, request_id:number, done:boolean}>\n---@field pending integer\n---@field autocmd_id? number\nlocal R = {}\nR.__index = R\nR._id = 0\n\nfunction R.new()\n  local self = setmetatable({}, R)\n  self.async = Async.running()\n  self.requests = {}\n  self.pending = 0\n  R._id = R._id + 1\n\n  self.async\n    :on(\n      \"abort\",\n      vim.schedule_wrap(function()\n        self:cancel()\n      end)\n    )\n    :on(\n      \"done\",\n      vim.schedule_wrap(function()\n        pcall(vim.api.nvim_del_autocmd, self.autocmd_id)\n      end)\n    )\n  return self\nend\n\n---@param clients vim.lsp.Client[]\n---@param ctx lsp.HandlerContext\nfunction R:debug(clients, ctx)\n  Snacks.debug.inspect({\n    error = \"LSP request callback yielded after done.\",\n    method = ctx.method,\n    requests = vim.deepcopy(self.requests),\n    pending = self.pending,\n    client_id = ctx.client_id,\n    ---@param c vim.lsp.Client\n    clients = vim.tbl_map(function(c)\n      return { id = c.id, name = c.name }\n    end, clients),\n  })\nend\n\n---@param client_id number\n---@param request_id number\n---@param completed? boolean\nfunction R:track(client_id, request_id, completed)\n  local key = (\"%d:%d\"):format(client_id, request_id)\n  if completed and self.requests[key] and not self.requests[key].done then\n    self.requests[key].done = true\n    self.pending = self.pending - 1\n    self.async:resume()\n    return\n  elseif not completed then\n    self.requests[key] = { client_id = client_id, request_id = request_id, done = false }\n    self.pending = self.pending + 1\n  end\nend\n\nfunction R:cancel()\n  while #self.requests > 0 do\n    local req = table.remove(self.requests)\n    local client = vim.lsp.get_client_by_id(req.client_id)\n    if client then\n      client:cancel_request(req.request_id)\n    end\n  end\nend\n\nfunction R:track_cancel()\n  if self.autocmd_id then\n    return\n  end\n  self.autocmd_id = vim.api.nvim_create_autocmd(\"LspRequest\", {\n    group = vim.api.nvim_create_augroup(\"snacks.picker.lsp.cancel.\" .. R._id, { clear = true }),\n    callback = function(ev)\n      if ev.data.request.type == \"cancel\" then\n        self:track(ev.data.client_id, ev.data.request_id, true)\n      end\n    end,\n  })\nend\n\n---@param buf number|vim.lsp.Client\n---@param method string\n---@param params fun(client:vim.lsp.Client):table\n---@param cb fun(client:vim.lsp.Client, result:table, params:table)\n---@async\nfunction R:request(buf, method, params, cb)\n  self.pending = self.pending + 1\n  vim.schedule(function()\n    self:track_cancel() -- setup autocmd here, since this must be called in the main loop\n\n    ---@diagnostic disable-next-line: param-type-mismatch\n    local clients = type(buf) == \"number\" and M.get_clients(buf, method) or { wrap(buf) }\n\n    self.pending = self.pending + #clients\n    for _, client in ipairs(clients) do\n      local done = false\n      local status, request_id ---@type boolean, number?\n      status, request_id = client:request(method, params(client), function(err, result, ctx)\n        done = true\n        if not err and result and not self.async:aborted() then\n          if not self.async:running() or self.pending <= 0 then\n            self:debug(clients, ctx)\n          end\n          cb(client, result, ctx.params)\n        end\n        if request_id then\n          self:track(client.id, request_id, true)\n        end\n      end)\n      -- skip tracking if the request failed\n      -- or is already done (in-process syncronous response)\n      if status and request_id and not done then\n        self:track(client.id, request_id)\n      end\n    end\n    self.pending = self.pending - 1 - #clients\n    self.async:resume()\n  end)\n  return self\nend\n\nfunction R:wait()\n  while self.pending > 0 do\n    self.async:suspend()\n  end\nend\n\n---@param buf number\n---@param method string\n---@param params fun(client:vim.lsp.Client):table\n---@param cb fun(client:vim.lsp.Client, result:table, params:table)\n---@async\nfunction M.request(buf, method, params, cb)\n  R.new():request(buf, method, params, cb):wait()\nend\n\n-- Support for older versions of neovim\n---@param locs vim.quickfix.entry[]\nfunction M.fix_locs(locs)\n  for _, loc in ipairs(locs) do\n    local range = loc.user_data and loc.user_data.range or nil ---@type lsp.Range?\n    if range then\n      if not loc.end_lnum then\n        if range.start.line == range[\"end\"].line then\n          loc.end_lnum = loc.lnum\n          loc.end_col = loc.col + range[\"end\"].character - range.start.character\n        end\n      end\n    end\n  end\nend\n\nfunction M.bufmap()\n  local bufmap = {} ---@type table<string,number>\n  for _, b in ipairs(vim.api.nvim_list_bufs()) do\n    if vim.bo[b].buflisted and vim.bo[b].buftype == \"\" and vim.api.nvim_buf_is_loaded(b) then\n      local name = vim.api.nvim_buf_get_name(b)\n      if name ~= \"\" then\n        bufmap[name] = b\n      end\n    end\n  end\n  return bufmap\nend\n\n---@param method string\n---@param opts snacks.picker.lsp.Config|{context?:lsp.ReferenceContext}\n---@param filter snacks.picker.Filter\nfunction M.get_locations(method, opts, filter)\n  local win = filter.current_win\n  local buf = filter.current_buf\n  local fname = vim.api.nvim_buf_get_name(buf)\n  fname = svim.fs.normalize(fname)\n  local cursor = vim.api.nvim_win_get_cursor(win)\n  local bufmap = M.bufmap()\n\n  ---@async\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    M.request(buf, method, function(client)\n      local params = vim.lsp.util.make_position_params(win, client.offset_encoding)\n      ---@diagnostic disable-next-line: inject-field\n      params.context = opts.context\n      return params\n    end, function(client, result)\n      result = result or {}\n      -- Result can be a single item or a list of items\n      result = vim.tbl_isempty(result) and {} or svim.islist(result) and result or { result }\n\n      local items = vim.lsp.util.locations_to_items(result or {}, client.offset_encoding)\n      M.fix_locs(items)\n\n      if not opts.include_current then\n        ---@param item vim.quickfix.entry\n        items = vim.tbl_filter(function(item)\n          if svim.fs.normalize(item.filename) ~= fname then\n            return true\n          end\n          if not item.lnum then\n            return true\n          end\n          if item.lnum == cursor[1] then\n            return false\n          end\n          if not item.end_lnum then\n            return true\n          end\n          return not (item.lnum <= cursor[1] and item.end_lnum >= cursor[1])\n        end, items)\n      end\n\n      local done = {} ---@type table<string, boolean>\n      for _, loc in ipairs(items) do\n        ---@type snacks.picker.finder.Item\n        local item = {\n          text = loc.filename .. \" \" .. loc.text,\n          buf = bufmap[loc.filename],\n          file = loc.filename,\n          pos = { loc.lnum, loc.col - 1 },\n          end_pos = loc.end_lnum and loc.end_col and { loc.end_lnum, loc.end_col - 1 } or nil,\n          line = loc.text,\n        }\n        local loc_key = loc.filename .. \":\" .. loc.lnum\n        if filter:match(item) and not (done[loc_key] and opts.unique_lines) then\n          ---@diagnostic disable-next-line: await-in-sync\n          cb(item)\n          done[loc_key] = true\n        end\n      end\n    end)\n  end\nend\n\n---@alias lsp.ResultItem lsp.Symbol|lsp.CallHierarchyItem|{text?:string}\n---@param client vim.lsp.Client\n---@param results lsp.ResultItem[]\n---@param opts? {default_uri?:string, filter?:(fun(result:lsp.ResultItem):boolean), text_with_file?:boolean}\nfunction M.results_to_items(client, results, opts)\n  opts = opts or {}\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local last = {} ---@type table<snacks.picker.finder.Item, snacks.picker.finder.Item>\n\n  ---@param result lsp.ResultItem\n  ---@param parent snacks.picker.finder.Item\n  local function add(result, parent)\n    ---@type snacks.picker.finder.Item\n    local item = {\n      kind = M.symbol_kind(result.kind),\n      parent = parent,\n      detail = result.detail,\n      name = result.name,\n      text = \"\",\n      range = result.range or result.selectionRange,\n      item = result,\n    }\n    local uri = result.location and result.location.uri or result.uri or opts.default_uri\n    local loc = result.location or { range = result.selectionRange or result.range, uri = uri }\n    loc.uri = loc.uri or uri\n    M.add_loc(item, loc, client)\n    local text = table.concat({ M.symbol_kind(result.kind), result.name }, \" \")\n    if opts.text_with_file and item.file then\n      text = text .. \" \" .. item.file\n    end\n    item.text = text\n\n    if not opts.filter or opts.filter(result) then\n      items[#items + 1] = item\n      last[parent] = item\n      parent = item\n    end\n\n    for _, child in ipairs(result.children or {}) do\n      add(child, parent)\n    end\n    result.children = nil\n  end\n\n  local root = { text = \"\", root = true } ---@type snacks.picker.finder.Item\n  ---@type snacks.picker.finder.Item\n  for _, result in ipairs(results) do\n    add(result, root)\n  end\n  for _, item in pairs(last) do\n    item.last = true\n  end\n\n  return items\nend\n\n---@param opts snacks.picker.lsp.symbols.Config\n---@type snacks.picker.finder\nfunction M.symbols(opts, ctx)\n  if opts.keep_parents then\n    ctx.picker.matcher.opts.keep_parents = true\n    ctx.picker.matcher.opts.sort = false\n  end\n  local buf = ctx.filter.current_buf\n  -- For unloaded buffers, load the buffer and\n  -- refresh the picker on every LspAttach event\n  -- for 10 seconds. Also defer to ensure the file is loaded by the LSP.\n  if not vim.api.nvim_buf_is_loaded(buf) then\n    local id = vim.api.nvim_create_autocmd(\"LspAttach\", {\n      buffer = buf,\n      callback = vim.schedule_wrap(function()\n        if ctx.picker:count() > 0 then\n          return true\n        end\n        ctx.picker:find()\n        vim.defer_fn(function()\n          if ctx.picker:count() == 0 then\n            ctx.picker:find()\n          end\n        end, 1000)\n      end),\n    })\n    pcall(vim.fn.bufload, buf)\n    vim.defer_fn(function()\n      vim.api.nvim_del_autocmd(id)\n    end, 10000)\n    return function()\n      ctx.async:sleep(2000)\n    end\n  end\n\n  local bufmap = M.bufmap()\n  local filter = opts.filter[vim.bo[buf].filetype]\n  if filter == nil then\n    filter = opts.filter.default\n  end\n  ---@param kind string?\n  local function want(kind)\n    kind = kind or \"Unknown\"\n    return type(filter) == \"boolean\" or vim.tbl_contains(filter, kind)\n  end\n\n  local method = opts.workspace and \"workspace/symbol\" or \"textDocument/documentSymbol\"\n  local p = opts.workspace and { query = ctx.filter.search }\n    or { textDocument = vim.lsp.util.make_text_document_params(buf) }\n\n  ---@async\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    M.request(buf, method, function()\n      return p\n    end, function(client, result, params)\n      local items = M.results_to_items(client, result, {\n        default_uri = params.textDocument and params.textDocument.uri or nil,\n        text_with_file = opts.workspace,\n        filter = function(item)\n          return want(M.symbol_kind(item.kind))\n        end,\n      })\n\n      -- Fix sorting\n      if not opts.workspace then\n        table.sort(items, function(a, b)\n          if a.pos[1] == b.pos[1] then\n            return a.pos[2] < b.pos[2]\n          end\n          return a.pos[1] < b.pos[1]\n        end)\n      end\n\n      -- fix last\n      local last = {} ---@type table<snacks.picker.finder.Item, snacks.picker.finder.Item>\n      for _, item in ipairs(items) do\n        item.last = nil\n        local parent = item.parent\n        if parent then\n          if last[parent] then\n            last[parent].last = nil\n          end\n          last[parent] = item\n          item.last = true\n        end\n      end\n\n      for _, item in ipairs(items) do\n        item.tree = opts.tree\n        item.buf = bufmap[item.file]\n        ---@diagnostic disable-next-line: await-in-sync\n        cb(item)\n      end\n    end)\n  end\nend\n\n---@param opts snacks.picker.lsp.Config\n---@param filter snacks.picker.Filter\n---@param incoming? boolean\nfunction M.call_hierarchy(opts, filter, incoming)\n  local method = (\"callHierarchy/%sCalls\"):format(incoming and \"incoming\" or \"outgoing\")\n  local buf = filter.current_buf\n  local win = filter.current_win\n\n  ---@async\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    local requester = R.new()\n    requester:request(buf, \"textDocument/prepareCallHierarchy\", function(client)\n      return vim.lsp.util.make_position_params(win, client.offset_encoding)\n    end, function(client, result)\n      ---@cast result lsp.CallHierarchyItem[]\n      for _, res in ipairs(result or {}) do\n        requester:request(client, method, function()\n          return { item = res }\n        end, function(_, calls)\n          ---@cast calls (lsp.CallHierarchyIncomingCall|lsp.CallHierarchyOutgoingCall)[]\n\n          local call_items = {} ---@type lsp.CallHierarchyItem[]\n          ---@param call lsp.CallHierarchyIncomingCall|lsp.CallHierarchyOutgoingCall\n          for _, call in ipairs(calls) do\n            if incoming then\n              for _, range in ipairs(call.fromRanges or {}) do\n                local from = vim.deepcopy(call.from)\n                from.selectionRange = range or from.selectionRange\n                table.insert(call_items, from)\n              end\n            else\n              table.insert(call_items, call.to)\n            end\n          end\n\n          local items = M.results_to_items(client, call_items, { default_uri = res.uri })\n          vim.tbl_map(cb, items)\n        end)\n      end\n    end)\n    requester:wait()\n  end\nend\n\n---@param opts snacks.picker.lsp.references.Config\n---@type snacks.picker.finder\nfunction M.references(opts, ctx)\n  opts = opts or {}\n  return M.get_locations(\n    \"textDocument/references\",\n    vim.tbl_deep_extend(\"force\", opts, {\n      context = { includeDeclaration = opts.include_declaration },\n    }),\n    ctx.filter\n  )\nend\n\n---@param opts snacks.picker.lsp.Config\n---@type snacks.picker.finder\nfunction M.incoming_calls(opts, ctx)\n  return M.call_hierarchy(opts, ctx.filter, true)\nend\n\n---@param opts snacks.picker.lsp.Config\n---@type snacks.picker.finder\nfunction M.outgoing_calls(opts, ctx)\n  return M.call_hierarchy(opts, ctx.filter, false)\nend\n\n---@param opts snacks.picker.lsp.Config\n---@type snacks.picker.finder\nfunction M.definitions(opts, ctx)\n  return M.get_locations(\"textDocument/definition\", opts, ctx.filter)\nend\n\n---@param opts snacks.picker.lsp.Config\n---@type snacks.picker.finder\nfunction M.type_definitions(opts, ctx)\n  return M.get_locations(\"textDocument/typeDefinition\", opts, ctx.filter)\nend\n\n---@param opts snacks.picker.lsp.Config\n---@type snacks.picker.finder\nfunction M.implementations(opts, ctx)\n  return M.get_locations(\"textDocument/implementation\", opts, ctx.filter)\nend\n\n---@param opts snacks.picker.lsp.Config\n---@type snacks.picker.finder\nfunction M.declarations(opts, ctx)\n  return M.get_locations(\"textDocument/declaration\", opts, ctx.filter)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/meta.lua",
    "content": "local M = {}\n\n---@param file string\n---@param t table<string,unknown>\nfunction M.table(file, t)\n  file = Snacks.meta.file(file)\n  local values = vim.tbl_keys(t)\n  table.sort(values)\n  ---@param value string\n  return vim.tbl_map(function(value)\n    return {\n      file = file,\n      text = value,\n      search = (\"/^M\\\\.%s = \\\\|function M\\\\.%s(\"):format(value, value),\n    }\n  end, values)\nend\n\n---@param opts snacks.picker.Config\n---@type snacks.picker.finder\nfunction M.pickers(opts)\n  return M.table(\"picker/config/sources.lua\", opts.sources or {})\nend\n\n---@param opts snacks.picker.Config\n---@type snacks.picker.finder\nfunction M.layouts(opts)\n  return M.table(\"picker/config/layouts.lua\", opts.layouts or {})\nend\n\nfunction M.actions()\n  return M.table(\"picker/actions.lua\", require(\"snacks.picker.actions\"))\nend\n\nfunction M.preview()\n  return M.table(\"picker/preview.lua\", require(\"snacks.picker.preview\"))\nend\n\nfunction M.format()\n  return M.table(\"picker/format.lua\", require(\"snacks.picker.format\"))\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/proc.lua",
    "content": "---@diagnostic disable: await-in-sync\nlocal Async = require(\"snacks.picker.util.async\")\n\nlocal M = {}\n\nlocal uv = vim.uv or vim.loop\nM.USE_QUEUE = true\n\n---@class snacks.picker.proc.Config: snacks.picker.Config\n---@field cmd string\n---@field sep? string\n---@field args? string[]\n---@field env? table<string, string>\n---@field cwd? string\n---@field notify? boolean Notify on failure\n---@field transform? snacks.picker.transform\n---@field raw? boolean Return raw output without processing\n\n---@param opts snacks.picker.proc.Config\n---@type snacks.picker.finder\nfunction M.proc(opts, ctx)\n  ---@cast opts snacks.picker.proc.Config\n  assert(opts.cmd, \"`opts.cmd` is required\")\n\n  ---@async\n  return function(cb)\n    if opts.transform then\n      local _cb = cb\n      cb = function(item)\n        local t = opts.transform(item, ctx)\n        item = type(t) == \"table\" and t or item\n        if t ~= false then\n          _cb(item)\n        end\n      end\n    end\n\n    if ctx.picker.opts.debug.proc then\n      vim.schedule(function()\n        ---@diagnostic disable-next-line: param-type-mismatch\n        Snacks.debug.cmd(ctx:opts({ group = true }))\n      end)\n    end\n\n    local sep = opts.sep or \"\\n\"\n    local aborted = false\n    local stdout = assert(uv.new_pipe())\n\n    local self = Async.running()\n\n    local spawn_opts = {\n      args = opts.args,\n      stdio = { nil, stdout, nil },\n      cwd = opts.cwd and svim.fs.normalize(opts.cwd) or nil,\n      env = opts.env,\n      hide = true,\n    }\n\n    local handle ---@type uv.uv_process_t\n    ---@diagnostic disable-next-line: missing-fields\n    handle = uv.spawn(opts.cmd, spawn_opts, function(code, _signal)\n      if not aborted and code ~= 0 and opts.notify ~= false then\n        local full = { opts.cmd or \"\" }\n        vim.list_extend(full, opts.args or {})\n        Snacks.notify.error((\"Command failed:\\n- cmd: `%s`\"):format(table.concat(full, \" \")))\n      end\n      handle:close()\n      self:resume()\n    end)\n    if not handle then\n      return Snacks.notify.error(\"Failed to spawn \" .. opts.cmd)\n    end\n\n    local prev ---@type string?\n    local queue = require(\"snacks.picker.util.queue\").new()\n\n    self:on(\"abort\", function()\n      stdout:read_stop()\n      if not stdout:is_closing() then\n        stdout:close()\n      end\n      aborted = true\n      queue:clear()\n      cb = function() end\n      if not handle:is_closing() then\n        handle:kill(\"sigterm\")\n        vim.defer_fn(function()\n          if not handle:is_closing() then\n            handle:kill(\"sigkill\")\n          end\n        end, 200)\n      end\n    end)\n\n    ---@param data? string\n    local function process(data)\n      if aborted then\n        return\n      end\n      if not data then\n        return prev and cb({ text = prev })\n      end\n      if opts.raw then\n        return cb({ text = data })\n      end\n      local from = 1\n      while from <= #data do\n        local nl = data:find(sep, from, true)\n        if nl then\n          local cr = data:byte(nl - 1, nl - 1) == 13 -- \\r\n          local line = data:sub(from, nl - (cr and 2 or 1))\n          if prev then\n            line, prev = prev .. line, nil\n          end\n          cb({ text = line })\n          from = nl + 1\n        elseif prev then\n          prev = prev .. data:sub(from)\n          break\n        else\n          prev = data:sub(from)\n          break\n        end\n      end\n    end\n\n    stdout:read_start(function(err, data)\n      assert(not err, err)\n      if aborted or not data then\n        stdout:close()\n        self:resume()\n        return\n      end\n      if M.USE_QUEUE then\n        queue:push(data)\n        self:resume()\n      else\n        process(data)\n      end\n    end)\n\n    while not (stdout:is_closing() and queue:empty()) do\n      if queue:empty() then\n        self:suspend()\n      else\n        process(queue:pop())\n      end\n    end\n    -- process the last line\n    if prev then\n      cb({ text = prev })\n    end\n  end\nend\n\n---@param opts snacks.picker.proc.Config|{[1]: snacks.picker.Config, [2]: snacks.picker.proc.Config}\n---@type snacks.picker.finder\nfunction M.json(opts, ctx)\n  opts = get_opts(opts) --[[@as snacks.picker.proc.Config]]\n  opts.raw = true\n  local transform = opts.transform\n  opts.transform = nil\n  return function(cb)\n    local Buffer = require(\"string.buffer\")\n    local data = Buffer.new()\n    M.proc(opts, ctx)(function(item)\n      data:put(item.text)\n    end)\n    local json = vim.json.decode(data:get())\n    assert(svim.islist(json), \"Expected JSON array\")\n    ---@cast json snacks.picker.finder.Item[]\n    for _, item in ipairs(json) do\n      item = setmetatable({ item = item }, { __index = item })\n      item.text = item.text or \"\"\n      local t = transform and transform(item, ctx) or nil\n      item = type(t) == \"table\" and t or item\n      if t ~= false then\n        cb(item)\n      end\n    end\n  end\nend\n\n---@param opts {cmd: string, args?: string[], cwd?: string}\nfunction M.debug(opts)\n  vim.schedule(function()\n    local lines = { opts.cmd } ---@type string[]\n    for _, arg in ipairs(opts.args or {}) do\n      arg = arg:find(\"[$%s]\") and vim.fn.shellescape(arg) or arg\n      if #arg + #lines[#lines] > 40 then\n        lines[#lines] = lines[#lines] .. \" \\\\\"\n        table.insert(lines, \"  \" .. arg)\n      else\n        lines[#lines] = lines[#lines] .. \" \" .. arg\n      end\n    end\n    local id = opts.cmd\n    for _, a in ipairs(opts.args or {}) do\n      if a:find(\"^-\") then\n        id = id .. \" \" .. a\n      end\n    end\n    Snacks.notify.info(\n      (\"- **cwd**: `%s`\\n```sh\\n%s\\n```\"):format(\n        vim.fn.fnamemodify(svim.fs.normalize(opts.cwd or uv.cwd() or \".\"), \":~\"),\n        table.concat(lines, \"\\n\")\n      ),\n      { id = \"snacks.picker.proc.\" .. id, title = \"Snacks Proc\" }\n    )\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/qf.lua",
    "content": "local M = {}\n\n---Represents an item in a Neovim quickfix/loclist.\n---@class qf.item\n---@field bufnr? number The buffer number where the item originates.\n---@field filename? string\n---@field lnum number The start line number for the item.\n---@field end_lnum? number The end line number for the item.\n---@field pattern string A pattern related to the item. It can be a search pattern or any relevant string.\n---@field col? number The column number where the item starts.\n---@field end_col? number The column number where the item ends.\n---@field module? string Module information (if any) associated with the item.\n---@field nr? number A unique number or ID for the item.\n---@field text? string A description or message related to the item.\n---@field type? string The type of the item. E.g., \"W\" might stand for \"Warning\".\n---@field valid number A flag indicating if the item is valid (1) or not (0).\n---@field user_data? any Any user data associated with the item.\n---@field vcol? number Visual column number. Indicates if the column number is a visual column number (when set to 1) or a byte index (when set to 0).\n\n---@class snacks.picker.qf.Config\n---@field qf_win? number\n---@field filter? snacks.picker.filter.Config\n\nlocal severities = {\n  E = vim.diagnostic.severity.ERROR,\n  W = vim.diagnostic.severity.WARN,\n  I = vim.diagnostic.severity.INFO,\n  H = vim.diagnostic.severity.HINT,\n  N = vim.diagnostic.severity.HINT,\n}\n\n---@param opts snacks.picker.qf.Config\n---@type snacks.picker.finder\nfunction M.qf(opts, ctx)\n  local win = opts.qf_win\n  win = win == 0 and vim.api.nvim_get_current_win() or win\n\n  local list = win and vim.fn.getloclist(win, { all = true }) or vim.fn.getqflist({ all = true })\n  ---@cast list { items?: qf.item[] }?\n\n  local ret = {} ---@type snacks.picker.finder.Item[]\n\n  for _, item in pairs(list and list.items or {}) do\n    local row = item.lnum == 0 and 1 or item.lnum\n    local col = (item.col == 0 and 1 or item.col) - 1\n    local end_row = item.end_lnum == 0 and row or item.end_lnum\n    local end_col = item.end_col == 0 and col or (item.end_col - 1)\n\n    if item.valid == 1 then\n      local file = item.filename or item.bufnr and vim.api.nvim_buf_get_name(item.bufnr) or nil\n      local text = item.text or \"\"\n      ret[#ret + 1] = {\n        pos = { row, col },\n        end_pos = item.end_lnum ~= 0 and { end_row, end_col } or nil,\n        text = file .. \" \" .. text,\n        line = text,\n        file = file,\n        severity = severities[item.type] or 0,\n        buf = item.bufnr,\n        item = item,\n      }\n    elseif #ret > 0 and ret[#ret].text and item.text then\n      ret[#ret].text = ret[#ret].text .. \"\\n\" .. item.text\n      ret[#ret].line = ret[#ret].line .. \"\\n\" .. item.text\n    end\n  end\n  return ctx.filter:filter(ret)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/recent.lua",
    "content": "local M = {}\n\nlocal uv = vim.uv or vim.loop\n\n---@param filter snacks.picker.Filter\n---@param extra? string[]\nlocal function oldfiles(filter, extra)\n  local done = {} ---@type table<string, boolean>\n  local files = {} ---@type string[]\n  vim.list_extend(files, extra or {})\n  vim.list_extend(files, vim.v.oldfiles)\n  local i = 0\n  return function()\n    for f = i + 1, #files do\n      i = f\n      local file = files[f]\n      file = vim.fn.fnamemodify(file, \":p\")\n      file = svim.fs.normalize(file, { _fast = true, expand_env = false })\n      local want = not done[file] and filter:match({ file = file, text = \"\" })\n      done[file] = true\n      if want and uv.fs_stat(file) then\n        return file\n      end\n    end\n  end\nend\n\n--- Get the most recent files, optionally filtered by the\n--- current working directory or a custom directory.\n---@param opts snacks.picker.recent.Config\n---@type snacks.picker.finder\nfunction M.files(opts, ctx)\n  local current_file = svim.fs.normalize(vim.api.nvim_buf_get_name(0), { _fast = true })\n  ---@type number[]\n  local bufs = vim.tbl_filter(function(b)\n    return vim.api.nvim_buf_get_name(b) ~= \"\" and vim.bo[b].buftype == \"\"\n  end, vim.api.nvim_list_bufs())\n  table.sort(bufs, function(a, b)\n    return vim.fn.getbufinfo(a)[1].lastused > vim.fn.getbufinfo(b)[1].lastused\n  end)\n  local extra = vim.tbl_map(function(b)\n    return vim.api.nvim_buf_get_name(b)\n  end, bufs)\n  ---@async\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    for file in oldfiles(ctx.filter, extra) do\n      if file ~= current_file then\n        cb({ file = file, text = file, recent = true })\n      end\n    end\n  end\nend\n\nM.recent = M.files\n\n--- Get the most recent projects based on git roots of recent files.\n--- The default action will change the directory to the project root,\n--- try to restore the session and open the picker if the session is not restored.\n--- You can customize the behavior by providing a custom action.\n---@param opts snacks.picker.projects.Config\n---@type snacks.picker.finder\nfunction M.projects(opts, ctx)\n  local args = {\n    \"-H\",\n    \"-t\",\n    \"f\",\n    \"-t\",\n    \"s\",\n    \"-t\",\n    \"d\",\n    \"--max-depth\",\n    tostring(opts.max_depth or 2),\n    \"--follow\",\n    \"--absolute-path\",\n  }\n  vim.list_extend(args, { \"-g\", \"{\" .. table.concat(opts.patterns or {}, \",\") .. \"}\" })\n  local dev = type(opts.dev) == \"string\" and { opts.dev } or opts.dev or {}\n  ---@cast dev string[]\n  vim.list_extend(args, vim.tbl_map(svim.fs.normalize, dev))\n  local fd = require(\"snacks.picker.source.files\").get_fd()\n  if not fd then\n    Snacks.notify.warn(\"`fd` or `fdfind` is required for projects\")\n  end\n  local proc = fd and require(\"snacks.picker.source.proc\").proc({ cmd = fd, args = args, notify = false }, ctx)\n  ---@async\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    local dirs = {} ---@type table<string, boolean>\n    ---@async\n    local function add(dir)\n      if dir and not dirs[dir] then\n        dirs[dir] = true\n        cb({ file = dir, text = dir, dir = true })\n      end\n    end\n\n    if opts.recent then\n      for file in oldfiles(ctx.filter) do\n        local dir = Snacks.git.get_root(file)\n        add(dir)\n      end\n    end\n\n    vim.tbl_map(add, opts.projects or {})\n\n    if not proc then\n      return\n    end\n\n    ---@async\n    proc(function(item)\n      local path = svim.fs.normalize(item.text)\n      path = path:sub(-1) == \"/\" and path:sub(1, -2) or path\n      path = vim.fs.dirname(path)\n      if ctx.filter:match({ file = path, text = path }) then\n        add(path)\n      end\n    end)\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/scratch.lua",
    "content": "local M = {}\n\n---@class snacks.scratch.actions\n---@field [string] snacks.picker.Action.spec\nM.actions = {\n  scratch_open = function(picker, item)\n    picker:close()\n    if not item then\n      return\n    end\n    Snacks.scratch.open({ icon = item.item.icon, file = item.item.file, name = item.item.name, ft = item.item.ft })\n  end,\n  scratch_delete = function(picker, item)\n    local current = item.file\n    os.remove(current)\n    os.remove(current .. \".meta\")\n    picker:refresh()\n  end,\n  scratch_new = function(picker)\n    picker:close()\n    Snacks.scratch.open()\n  end,\n}\n\n---@param opts snacks.picker.proc.Config\n---@type snacks.picker.finder\nfunction M.scratch(opts)\n  local list = Snacks.scratch.list()\n  local items = {} ---@type snacks.picker.finder.Item[]\n  for _, item in ipairs(list) do\n    items[#items + 1] = {\n      file = item.file,\n      item = item,\n      title = item.name,\n      text = Snacks.picker.util.text(item, { \"name\", \"branch\", \"ft\" }),\n      branch = item.branch and (\"branch:%s\"):format(item.branch) or \"\",\n    }\n  end\n  return items\nend\n\n---@type snacks.picker.format\nfunction M.format(item, picker)\n  local file = item.item\n  local ret = {} ---@type snacks.picker.Highlight[]\n  local a = Snacks.picker.util.align\n  local icon, icon_hl = file.icon, nil\n  if not icon then\n    icon, icon_hl = Snacks.util.icon(file.ft, \"filetype\")\n  end\n  ret[#ret + 1] = { a(icon, 3), icon_hl }\n  ret[#ret + 1] = { a(file.name, 20, { truncate = true }) }\n  ret[#ret + 1] = { \" \" }\n  ret[#ret + 1] = { a(item.branch, 20, { truncate = true }), \"Number\" }\n  ret[#ret + 1] = { \" \" }\n  ---@diagnostic disable-next-line: missing-fields\n  vim.list_extend(ret, Snacks.picker.format.filename({ text = \"\", dir = true, file = file.cwd }, picker))\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/snacks.lua",
    "content": "local M = {}\n\n---@param opts snacks.picker.notifications.Config\nfunction M.notifier(opts)\n  local notifs = Snacks.notifier.get_history({ filter = opts.filter, reverse = true })\n  local items = {} ---@type snacks.picker.finder.Item[]\n\n  for _, notif in ipairs(notifs) do\n    items[#items + 1] = {\n      text = Snacks.picker.util.text(notif, { \"level\", \"title\", \"msg\" }),\n      item = notif,\n      severity = notif.level,\n      preview = {\n        text = notif.msg,\n        ft = \"markdown\",\n      },\n    }\n  end\n\n  return items\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/system.lua",
    "content": "local M = {}\n\n---@param opts snacks.picker.proc.Config\n---@type snacks.picker.finder\nfunction M.cliphist(opts, ctx)\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cmd = \"cliphist\",\n      args = { \"list\" },\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        local id, content = item.text:match(\"^(%d+)%s+(.+)$\")\n        if id and content and not content:find(\"^%[%[%s+binary data\") then\n          item.text = content\n          setmetatable(item, {\n            __index = function(_, k)\n              if k == \"data\" then\n                local data = vim.fn.system({ \"cliphist\", \"decode\", id })\n                rawset(item, \"data\", data)\n                if vim.v.shell_error ~= 0 then\n                  error(data)\n                end\n                return data\n              elseif k == \"preview\" then\n                return {\n                  text = item.data,\n                  ft = \"text\",\n                }\n              end\n            end,\n          })\n        else\n          return false\n        end\n      end,\n    }),\n    ctx\n  )\nend\n\n---@param opts snacks.picker.proc.Config\n---@type snacks.picker.finder\nfunction M.man(opts, ctx)\n  return require(\"snacks.picker.source.proc\").proc(\n    ctx:opts({\n      cmd = \"man\",\n      args = { \"-k\", \".\" },\n      ---@param item snacks.picker.finder.Item\n      transform = function(item)\n        local page, section, desc = item.text:match(\"^(%S+)%s*%((%S-)%)%s+-%s+(.+)$\")\n        if page and section and desc then\n          item.section = section\n          item.desc = desc\n          item.page = page\n          item.section = section\n          item.ref = (\"%s(%s)\"):format(item.page, item.section or 1)\n        else\n          return false\n        end\n      end,\n    }),\n    ctx\n  )\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/treesitter.lua",
    "content": "local M = {}\n\n---@class snacks.picker.treesitter.Match\n---@field id string\n---@field name string\n---@field node TSNode\n---@field text string\n---@field meta table<string, any>\n---@field pos {[1]: number, [2]: number}\n---@field end_pos {[1]: number, [2]: number}\n---@field kind? string\n---@field scope? \"parent\" | \"local\" | \"global\"\n---@field children? snacks.picker.treesitter.Match[]\n\n-- stylua: ignore\nlocal kind_mapping = {\n  constant      = \"Constant\",\n  type          = \"Class\",\n  enum          = \"Enum\",\n  field         = \"Field\",\n  [\"function\"]  = \"Function\",\n  macro         = \"Function\",\n  method        = \"Method\",\n  namespace     = \"Namespace\",\n  import        = \"Module\",\n  var           = \"Variable\",\n  -- associated = \"Reference\",\n  -- parameter  = \"Parameter\",\n}\n\nlocal function sort(nodes)\n  table.sort(nodes, function(a, b)\n    if a.pos[1] ~= b.pos[1] then\n      return a.pos[1] < b.pos[1]\n    end\n    if a.pos[2] ~= b.pos[2] then\n      return a.pos[2] < b.pos[2]\n    end\n    if a.end_pos[1] ~= b.end_pos[1] then\n      return a.end_pos[1] < b.end_pos[1]\n    end\n    return a.end_pos[2] < b.end_pos[2]\n  end)\nend\n\nfunction M.get_locals(buf)\n  local ok, parser = pcall(vim.treesitter.get_parser, buf)\n  if not ok or not parser then\n    return {}\n  end\n  parser:parse(true)\n  local query = vim.treesitter.query.get(parser:lang(), \"locals\")\n  if not query then\n    return {}\n  end\n\n  local defs = {} ---@type snacks.picker.treesitter.Match[]\n  local scopes = {} ---@type table<string,snacks.picker.treesitter.Match>\n  for _, tree in ipairs(parser:trees()) do\n    for id, node, meta in query:iter_captures(tree:root(), buf) do\n      local name = query.captures[id]\n      local range = { node:range() }\n      ---@type snacks.picker.treesitter.Match\n      local match = {\n        id = node:id(),\n        node = node,\n        name = name,\n        meta = meta,\n        text = vim.treesitter.get_node_text(node, buf),\n        pos = { range[1] + 1, range[2] },\n        end_pos = { range[3] + 1, range[4] },\n      }\n      local kind = name:match(\"^local%.definition%.(.*)$\")\n      if kind then\n        match.kind = kind\n        match.scope = meta[\"definition.method.scope\"] or \"local\"\n        defs[#defs + 1] = match\n      elseif name == \"local.scope\" then\n        match.kind = \"scope\"\n        scopes[match.id] = match\n      end\n    end\n  end\n\n  ---@param node TSNode\n  local function find_scope(node)\n    local n = node:parent() ---@type TSNode?\n    while n do\n      if scopes[n:id()] then\n        return scopes[n:id()]\n      end\n      n = n:parent()\n    end\n  end\n\n  -- put defs in their scope nodes\n  for _, def in ipairs(defs) do\n    local scope = find_scope(def.node)\n    if scope then\n      scope.children = scope.children or {}\n      table.insert(scope.children, def)\n    end\n  end\n\n  -- put scopes in their parents\n  local ret = {} ---@type snacks.picker.treesitter.Match[]\n  for _, scope in pairs(scopes) do\n    local parent = find_scope(scope.node)\n    if parent then\n      parent.children = parent.children or {}\n      table.insert(parent.children, scope)\n    else\n      ret[#ret + 1] = scope\n    end\n  end\n\n  return ret\nend\n\n---@param opts snacks.picker.treesitter.Config\n---@type snacks.picker.finder\nfunction M.symbols(opts, ctx)\n  local buf = ctx.filter.current_buf\n  local tree = M.get_locals(buf)\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local last = {} ---@type table<snacks.picker.finder.Item,snacks.picker.finder.Item>\n\n  local filter = opts.filter[vim.bo[buf].filetype]\n  if filter == nil then\n    filter = opts.filter.default\n  end\n\n  ---@param kind string?\n  local function want(kind)\n    kind = kind or \"Unknown\"\n    return type(filter) == \"boolean\" or vim.tbl_contains(filter, kind)\n  end\n\n  ---@type snacks.picker.finder.Item\n  local root = { text = \"root\" }\n\n  ---@param match snacks.picker.treesitter.Match\n  ---@param parent snacks.picker.finder.Item?\n  ---@return snacks.picker.finder.Item?\n  local function add(match, parent, depth)\n    local item ---@type snacks.picker.finder.Item?\n    local kind = match.kind and kind_mapping[match.kind]\n    if want(kind) then\n      item = {\n        text = match.text,\n        depth = depth or 0,\n        tree = opts.tree,\n        buf = buf,\n        name = match.text,\n        kind = kind_mapping[match.kind] or \"Unknown\",\n        ts_kind = match.kind,\n        pos = match.pos,\n        end_pos = match.end_pos,\n        last = true,\n        parent = parent,\n      }\n      if parent then\n        if last[parent] then\n          last[parent].last = false\n        end\n        last[parent] = item\n      end\n      items[#items + 1] = item\n    end\n    local children = match.children or {}\n    sort(children)\n    for _, child in ipairs(children) do\n      local c = add(child, item or parent, depth + 1)\n      -- first item in a scope is the scope itself\n      if match.kind == \"scope\" and c and c.depth == depth + 1 then\n        item = item or c\n      end\n    end\n    return item\n  end\n\n  sort(tree)\n\n  for _, scope in ipairs(tree) do\n    add(scope, root, 0)\n  end\n\n  return items\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/source/vim.lua",
    "content": "local M = {}\n\n---@class snacks.picker.history.Config: snacks.picker.Config\n---@field name string\n\nfunction M.commands()\n  local commands = vim.api.nvim_get_commands({})\n  for k, v in pairs(vim.api.nvim_buf_get_commands(0, {})) do\n    if type(k) == \"string\" then -- fixes vim.empty_dict() bug\n      commands[k] = v\n    end\n  end\n  for _, c in ipairs(vim.fn.getcompletion(\"\", \"command\")) do\n    if not commands[c] and c:find(\"^[a-z]\") then\n      commands[c] = { definition = \"completion\" }\n    end\n  end\n  ---@async\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  return function(cb)\n    ---@type string[]\n    local names = vim.tbl_keys(commands)\n    table.sort(names)\n    for _, name in pairs(names) do\n      local def = commands[name]\n      cb({\n        text = name,\n        desc = def.script_id and def.script_id < 0 and def.definition or nil,\n        command = def,\n        cmd = name,\n        preview = {\n          text = vim.inspect(def),\n          ft = \"lua\",\n        },\n      })\n    end\n  end\nend\n\n---@param opts snacks.picker.history.Config\nfunction M.history(opts)\n  local count = vim.fn.histnr(opts.name)\n  local items = {}\n  for i = count, 1, -1 do\n    local line = vim.fn.histget(opts.name, i)\n    if not line:find(\"^%s*$\") then\n      table.insert(items, {\n        text = line,\n        cmd = line,\n        preview = {\n          text = line,\n          ft = \"text\",\n        },\n      })\n    end\n  end\n  return items\nend\n\n---@param opts snacks.picker.marks.Config\n---@type snacks.picker.finder\nfunction M.marks(opts, ctx)\n  local marks = {} ---@type vim.fn.getmarklist.ret.item[]\n  local current_buf = ctx.filter.current_buf\n  if opts.global then\n    vim.list_extend(marks, vim.fn.getmarklist())\n  end\n  if opts[\"local\"] then\n    vim.list_extend(marks, vim.fn.getmarklist(current_buf))\n  end\n\n  ---@type snacks.picker.finder.Item[]\n  local items = {}\n  local bufname = vim.api.nvim_buf_get_name(current_buf)\n  for _, mark in ipairs(marks) do\n    local file = mark.file or bufname\n    local buf = mark.pos[1] and mark.pos[1] > 0 and mark.pos[1] or nil\n    local line ---@type string?\n    if buf and mark.pos[2] > 0 and vim.api.nvim_buf_is_valid(mark.pos[1]) then\n      line = vim.api.nvim_buf_get_lines(buf, mark.pos[2] - 1, mark.pos[2], false)[1]\n    end\n    local label = mark.mark:sub(2, 2)\n    items[#items + 1] = {\n      text = table.concat({ label, file, line }, \" \"),\n      label = label,\n      line = line,\n      buf = buf,\n      file = file,\n      pos = mark.pos[2] > 0 and { mark.pos[2], mark.pos[3] },\n    }\n  end\n  table.sort(items, function(a, b)\n    return a.label < b.label\n  end)\n  return items\nend\n\nfunction M.jumps()\n  local jumps = vim.fn.getjumplist()[1]\n  local items = {} ---@type snacks.picker.finder.Item[]\n  for _, jump in ipairs(jumps) do\n    local buf = jump.bufnr and vim.api.nvim_buf_is_valid(jump.bufnr) and jump.bufnr or 0\n    local file = jump.filename or buf and vim.api.nvim_buf_get_name(buf) or nil\n    if buf or file then\n      local line ---@type string?\n      if buf then\n        line = vim.api.nvim_buf_get_lines(buf, jump.lnum - 1, jump.lnum, false)[1]\n      end\n      local label = tostring(#jumps - #items)\n      table.insert(items, 1, {\n        label = Snacks.picker.util.align(label, #tostring(#jumps), { align = \"right\" }),\n        buf = buf,\n        line = line,\n        text = table.concat({ file, line }, \" \"),\n        file = file,\n        pos = jump.lnum and jump.lnum > 0 and { jump.lnum, jump.col } or nil,\n      })\n    end\n  end\n  return items\nend\n\nfunction M.autocmds()\n  local autocmds = vim.api.nvim_get_autocmds({})\n  local items = {} ---@type snacks.picker.finder.Item[]\n  for _, au in ipairs(autocmds) do\n    local item = au --[[@as snacks.picker.finder.Item]]\n    item.text = Snacks.picker.util.text(item, { \"event\", \"group_name\", \"pattern\", \"command\" })\n    item.preview = {\n      text = vim.inspect(au),\n      ft = \"lua\",\n    }\n    item.item = au\n    if au.callback then\n      local info = debug.getinfo(au.callback, \"S\")\n      if info.what == \"Lua\" then\n        item.file = info.source:sub(2)\n        item.pos = { info.linedefined, 0 }\n        item.preview = \"file\"\n      end\n    end\n    items[#items + 1] = item\n  end\n  return items\nend\n\nfunction M.highlights()\n  local hls = vim.api.nvim_get_hl(0, {}) --[[@as table<string,vim.api.keyset.get_hl_info> ]]\n  local items = {} ---@type snacks.picker.finder.Item[]\n  for group, hl in pairs(hls) do\n    local defs = {} ---@type {group:string, hl:vim.api.keyset.get_hl_info}[]\n    defs[#defs + 1] = { group = group, hl = hl }\n    local link = hl.link\n    local done = { [group] = true } ---@type table<string, boolean>\n    while link and not done[link] do\n      done[link] = true\n      local hl_link = hls[link]\n      if hl_link then\n        defs[#defs + 1] = { group = link, hl = hl_link }\n        link = hl_link.link\n      else\n        break\n      end\n    end\n    local code = {} ---@type string[]\n    local extmarks = {} ---@type snacks.picker.Extmark[]\n    local row = 1\n    for _, def in ipairs(defs) do\n      for _, prop in ipairs({ \"fg\", \"bg\", \"sp\" }) do\n        local v = def.hl[prop]\n        if type(v) == \"number\" then\n          def.hl[prop] = (\"#%06X\"):format(v)\n        end\n      end\n      code[#code + 1] = (\"%s = %s\"):format(def.group, vim.inspect(def.hl))\n      extmarks[#extmarks + 1] = { row = row, col = 0, hl_group = def.group, end_col = #def.group }\n      row = row + #vim.split(code[#code], \"\\n\") + 1\n    end\n    items[#items + 1] = {\n      text = vim.inspect(defs):gsub(\"\\n\", \" \"),\n      hl_group = group,\n      preview = {\n        text = table.concat(code, \"\\n\\n\"),\n        ft = \"lua\",\n        extmarks = extmarks,\n      },\n    }\n  end\n  table.sort(items, function(a, b)\n    return a.hl_group < b.hl_group\n  end)\n  return items\nend\n\nfunction M.colorschemes()\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local rtp = vim.o.runtimepath\n  if package.loaded.lazy then\n    rtp = rtp .. \",\" .. table.concat(require(\"lazy.core.util\").get_unloaded_rtp(\"\"), \",\")\n  end\n  local files = vim.fn.globpath(rtp, \"colors/*\", false, true) ---@type string[]\n  for _, file in ipairs(files) do\n    local name = vim.fn.fnamemodify(file, \":t:r\")\n    local ext = vim.fn.fnamemodify(file, \":e\")\n    if ext == \"vim\" or ext == \"lua\" then\n      items[#items + 1] = {\n        text = name,\n        file = file,\n      }\n    end\n  end\n  return items\nend\n\n---@param opts snacks.picker.keymaps.Config\nfunction M.keymaps(opts)\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local maps = {} ---@type vim.api.keyset.get_keymap[]\n  for _, mode in ipairs(opts.modes) do\n    if opts.global then\n      vim.list_extend(maps, vim.api.nvim_get_keymap(mode))\n    end\n    if opts[\"local\"] then\n      vim.list_extend(maps, vim.api.nvim_buf_get_keymap(0, mode))\n    end\n  end\n  local done = {} ---@type table<string, boolean>\n  for _, km in ipairs(maps) do\n    local key = Snacks.picker.util.text(km, { \"mode\", \"lhs\", \"buffer\" })\n    local keep = true\n    if opts.plugs == false and km.lhs:match(\"^<Plug>\") then\n      keep = false\n    end\n    if keep and not done[key] then\n      done[key] = true\n      local item = {\n        mode = km.mode,\n        item = km,\n        key = km.lhs,\n        preview = {\n          text = vim.inspect(km),\n          ft = \"lua\",\n        },\n      }\n      if km.callback then\n        local info = debug.getinfo(km.callback, \"S\")\n        item.info = info\n        if info.what == \"Lua\" then\n          local source = info.source:sub(2)\n          item.file = source:gsub(\"^vim/\", vim.env.VIMRUNTIME .. \"/lua/vim/\")\n          if source:find(\"^vim/\") and info.linedefined == 0 then\n            item.search = \"/vim\\\\.keymap\\\\.set.*['\\\"]\" .. km.lhs\n          else\n            item.pos = { info.linedefined, 0 }\n          end\n          item.preview = \"file\"\n        end\n      end\n      item.text = Snacks.util.normkey(km.lhs)\n        .. \" \"\n        .. Snacks.picker.util.text(km, { \"mode\", \"lhs\", \"rhs\", \"desc\" })\n        .. (item.file or \"\")\n      items[#items + 1] = item\n    end\n  end\n  return items\nend\n\nfunction M.registers()\n  local registers = '*+\"-:.%/#=_abcdefghijklmnopqrstuvwxyz0123456789'\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local is_osc52 = vim.g.clipboard and vim.g.clipboard.name == \"OSC 52\"\n  local has_clipboard = vim.g.loaded_clipboard_provider == 2\n  for i = 1, #registers, 1 do\n    local reg = registers:sub(i, i)\n    local value = \"\"\n    if is_osc52 and reg:match(\"[%+%*]\") then\n      value = \"OSC 52 detected, register not checked to maintain compatibility\"\n    elseif has_clipboard or not reg:match(\"[%+%*]\") then\n      local ok, reg_value = pcall(vim.fn.getreg, reg, 1)\n      value = (ok and reg_value or \"\") --[[@as string]]\n    end\n    if value ~= \"\" then\n      table.insert(items, {\n        text = (\"%s: %s\"):format(reg, value:gsub(\"\\n\", \"\\\\n\"):gsub(\"\\r\", \"\\\\r\")),\n        reg = reg,\n        label = reg,\n        data = value,\n        value = value:gsub(\"\\n\", \"\\\\n\"):gsub(\"\\r\", \"\\\\r\"),\n        preview = {\n          text = value,\n          ft = \"text\",\n        },\n      })\n    end\n  end\n  return items\nend\n\nfunction M.spelling()\n  local buf = vim.api.nvim_get_current_buf()\n  local win = vim.api.nvim_get_current_win()\n  local cursor = vim.api.nvim_win_get_cursor(0)\n  local line = vim.api.nvim_buf_get_lines(buf, cursor[1] - 1, cursor[1], false)[1]\n\n  -- get a misspelled word from under the cursor, if not found, then use the cursor_word instead\n  local bad = vim.fn.spellbadword() ---@type string[]\n  local word = bad[1] == \"\" and vim.fn.expand(\"<cword>\") or bad[1]\n  local suggestions = vim.fn.spellsuggest(word, 25, bad[2] == \"caps\")\n\n  local items = {} ---@type snacks.picker.finder.Item[]\n\n  for _, label in ipairs(suggestions) do\n    table.insert(items, {\n      text = label,\n      action = function()\n        -- skip whitespace\n        local col = cursor[2] + 1\n        while line:sub(col, col):match(\"%s\") and col < #line do\n          col = col + 1\n          vim.api.nvim_win_set_cursor(win, { cursor[1], col - 1 })\n        end\n        vim.cmd('normal! \"_ciw' .. label)\n      end,\n    })\n  end\n  return items\nend\n\n---@class snacks.picker.tags.Tag\n---@field name string\n---@field filename string\n---@field cmd string\n---@field kind? string\n---@field static? string\n\n---@param opts snacks.picker.tags.Config\n---@type snacks.picker.finder\nfunction M.tags(opts, ctx)\n  local buf = ctx.filter.current_buf\n  ---@type snacks.picker.tags.Tag[]\n  local tags = vim.fn.taglist(ctx.filter.search == \"\" and \".*\" or ctx.filter.search, vim.api.nvim_buf_get_name(buf))\n  local ret = {} ---@type snacks.picker.finder.Item[]\n\n  local lsp_kinds = {\n    c = \"Class\",\n    d = \"Constant\",\n    e = \"Enum\",\n    f = \"Function\",\n    g = \"EnumMember\",\n    l = \"Variable\",\n    m = \"Method\",\n    s = \"Struct\",\n    t = \"TypeParameter\",\n    v = \"Variable\",\n    F = \"Field\",\n    M = \"Module\",\n    n = \"Namespace\",\n    P = \"Property\",\n    S = \"Struct\",\n    T = \"TypeParameter\",\n  }\n\n  for _, tag in ipairs(tags) do\n    ---@type snacks.picker.finder.Item\n    local item = {\n      text = tag.name,\n      name = tag.name,\n      file = tag.filename,\n      search = tag.cmd,\n      kind = tag.kind,\n      lsp_kind = lsp_kinds[tag.kind] or \"Text\",\n    }\n    ret[#ret + 1] = item\n  end\n\n  return ret\nend\n\n---@param opts snacks.picker.undo.Config\n---@type snacks.picker.finder\nfunction M.undo(opts, ctx)\n  local tree = vim.fn.undotree()\n  local buf = vim.api.nvim_get_current_buf()\n  local file = vim.api.nvim_buf_get_name(buf)\n  local items = {} ---@type snacks.picker.finder.Item[]\n  local diff_fn = vim.text and vim.text.diff or vim.diff\n\n  -- Copy the current buffer to a temporary file and load the undo history.\n  -- This is done to prevent the current buffer from being modified,\n  -- and is way better for performance, since LSP change tracking won't be triggered\n  local tmp_file = vim.fn.stdpath(\"cache\") .. \"/snacks-undo\"\n  local tmp_undo = tmp_file .. \".undo\"\n  local tmpbuf = vim.fn.bufadd(tmp_file)\n  vim.bo[tmpbuf].swapfile = false\n  vim.fn.writefile(vim.api.nvim_buf_get_lines(buf, 0, -1, false), tmp_file)\n  vim.fn.bufload(tmpbuf)\n  vim.api.nvim_buf_call(buf, function()\n    vim.cmd(\"silent wundo! \" .. tmp_undo)\n  end)\n  vim.api.nvim_buf_call(tmpbuf, function()\n    pcall(vim.cmd, \"silent rundo \" .. tmp_undo)\n  end)\n\n  ---@param item snacks.picker.finder.Item\n  local function resolve(item)\n    local entry = item.item ---@type vim.fn.undotree.entry\n    ---@type string[], string[]\n    local before, after = {}, {}\n\n    local ei = vim.o.eventignore\n    vim.o.eventignore = \"all\"\n    vim.api.nvim_buf_call(tmpbuf, function()\n      -- state after the undo\n      vim.cmd(\"noautocmd silent undo \" .. entry.seq)\n      after = vim.api.nvim_buf_get_lines(tmpbuf, 0, -1, false)\n      -- state before the undo\n      vim.cmd(\"noautocmd silent undo\")\n      before = vim.api.nvim_buf_get_lines(tmpbuf, 0, -1, false)\n    end)\n    vim.o.eventignore = ei\n\n    local diff = diff_fn(table.concat(before, \"\\n\") .. \"\\n\", table.concat(after, \"\\n\") .. \"\\n\", opts.diff) --[[@as string]]\n    local changes = {} ---@type string[]\n    local added_lines = {} ---@type string[]\n    local removed_lines = {} ---@type string[]\n\n    for _, line in ipairs(vim.split(diff, \"\\n\")) do\n      if line:sub(1, 1) == \"+\" then\n        changes[#changes + 1] = line:sub(2)\n        added_lines[#added_lines + 1] = line:sub(2)\n      elseif line:sub(1, 1) == \"-\" then\n        changes[#changes + 1] = line:sub(2)\n        removed_lines[#removed_lines + 1] = line:sub(2)\n      end\n    end\n    diff = Snacks.picker.util.tpl(\n      \"diff --git a/{file} b/{file}\\n--- {file}\\n+++ {file}\\n{diff}\",\n      { file = vim.fn.fnamemodify(file, \":.\"), diff = diff }\n    )\n    item.text = table.concat(changes, \" \")\n    item.data = table.concat(added_lines, \"\\n\")\n    item.added_lines = table.concat(added_lines, \"\\n\")\n    item.removed_lines = table.concat(removed_lines, \"\\n\")\n    item.added = #added_lines\n    item.removed = #removed_lines\n    item.diff = diff\n  end\n\n  ---@param entries? vim.fn.undotree.entry[]\n  ---@param parent? snacks.picker.finder.Item\n  local function add(entries, parent)\n    entries = entries or {}\n    table.sort(entries, function(a, b)\n      return a.seq < b.seq\n    end)\n    local last ---@type snacks.picker.finder.Item?\n    for e, entry in ipairs(entries) do\n      add(entry.alt, last or parent)\n      local item = {\n        seq = entry.seq,\n        buf = buf,\n        resolve = resolve,\n        file = file,\n        item = entry,\n        current = entry.seq == tree.seq_cur,\n        parent = parent,\n        last = e == #entries,\n        action = function()\n          vim.api.nvim_buf_call(buf, function()\n            vim.cmd(\"undo \" .. entry.seq)\n          end)\n        end,\n      }\n      items[#items + 1] = item\n      last = item\n    end\n  end\n  add(tree.entries)\n\n  -- Resolve the items in batches to prevent blocking the UI\n  ---@param cb async fun(item: snacks.picker.finder.Item)\n  ---@async\n  return function(cb)\n    ctx.async:on(\"done\", function()\n      vim.schedule(function()\n        -- Clean up the temporary files\n        vim.api.nvim_buf_delete(tmpbuf, { force = true })\n        vim.fn.delete(tmp_file)\n        vim.fn.delete(tmp_undo)\n      end)\n    end)\n    for i = #items, 1, -1 do\n      local item = items[i]\n      cb(item)\n      if item.current then\n        ctx.picker.list:set_target(#items - i + 1)\n      end\n    end\n\n    while #items > 0 do\n      vim.schedule(function()\n        local count = 0\n        while #items > 0 and count < 5 do\n          count = count + 1\n          local item = table.remove(items, 1)\n          Snacks.picker.util.resolve(item)\n        end\n        ctx.async:resume()\n      end)\n      ctx.async:suspend()\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/transform.lua",
    "content": "---@class snacks.picker.transformers\n---@field [string] snacks.picker.transform\nlocal M = {}\n\nfunction M.unique_file(item, ctx)\n  ctx.meta.done = ctx.meta.done or {} ---@type table<string, boolean>\n  local path = Snacks.picker.util.path(item)\n  if not path or ctx.meta.done[path] then\n    return false\n  end\n  ctx.meta.done[path] = true\nend\n\nfunction M.text_to_file(item, ctx)\n  item.file = item.file or item.text\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/types.lua",
    "content": "---@meta _\n\n---@class snacks.picker\n---@field autocmds fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field buffers fun(opts?: snacks.picker.buffers.Config|{}): snacks.Picker\n---@field cliphist fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field colorschemes fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field command_history fun(opts?: snacks.picker.history.Config|{}): snacks.Picker\n---@field commands fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field diagnostics fun(opts?: snacks.picker.diagnostics.Config|{}): snacks.Picker\n---@field diagnostics_buffer fun(opts?: snacks.picker.diagnostics.Config|{}): snacks.Picker\n---@field explorer fun(opts?: snacks.picker.explorer.Config|{}): snacks.Picker\n---@field files fun(opts?: snacks.picker.files.Config|{}): snacks.Picker\n---@field gh_actions fun(opts?: snacks.picker.gh.actions.Config|{}): snacks.Picker\n---@field gh_diff fun(opts?: snacks.picker.gh.diff.Config|{}): snacks.Picker\n---@field gh_issue fun(opts?: snacks.picker.gh.issue.Config|{}): snacks.Picker\n---@field gh_labels fun(opts?: snacks.picker.gh.labels.Config|{}): snacks.Picker\n---@field gh_pr fun(opts?: snacks.picker.gh.pr.Config|{}): snacks.Picker\n---@field gh_reactions fun(opts?: snacks.picker.gh.reactions.Config|{}): snacks.Picker\n---@field git_branches fun(opts?: snacks.picker.git.branches.Config|{}): snacks.Picker\n---@field git_diff fun(opts?: snacks.picker.git.diff.Config|{}): snacks.Picker\n---@field git_files fun(opts?: snacks.picker.git.files.Config|{}): snacks.Picker\n---@field git_grep fun(opts?: snacks.picker.git.grep.Config|{}): snacks.Picker\n---@field git_log fun(opts?: snacks.picker.git.log.Config|{}): snacks.Picker\n---@field git_log_file fun(opts?: snacks.picker.git.log.Config|{}): snacks.Picker\n---@field git_log_line fun(opts?: snacks.picker.git.log.Config|{}): snacks.Picker\n---@field git_stash fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field git_status fun(opts?: snacks.picker.git.status.Config|{}): snacks.Picker\n---@field grep fun(opts?: snacks.picker.grep.Config|{}): snacks.Picker\n---@field grep_buffers fun(opts?: snacks.picker.grep.Config|{}): snacks.Picker\n---@field grep_word fun(opts?: snacks.picker.grep.Config|{}): snacks.Picker\n---@field help fun(opts?: snacks.picker.help.Config|{}): snacks.Picker\n---@field highlights fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field icons fun(opts?: snacks.picker.icons.Config|{}): snacks.Picker\n---@field jumps fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field keymaps fun(opts?: snacks.picker.keymaps.Config|{}): snacks.Picker\n---@field lazy fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field lines fun(opts?: snacks.picker.lines.Config|{}): snacks.Picker\n---@field loclist fun(opts?: snacks.picker.qf.Config|{}): snacks.Picker\n---@field lsp_config fun(opts?: snacks.picker.lsp.config.Config|{}): snacks.Picker\n---@field lsp_declarations fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker\n---@field lsp_definitions fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker\n---@field lsp_implementations fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker\n---@field lsp_incoming_calls fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker\n---@field lsp_outgoing_calls fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker\n---@field lsp_references fun(opts?: snacks.picker.lsp.references.Config|{}): snacks.Picker\n---@field lsp_symbols fun(opts?: snacks.picker.lsp.symbols.Config|{}): snacks.Picker\n---@field lsp_type_definitions fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker\n---@field lsp_workspace_symbols fun(opts?: snacks.picker.lsp.symbols.Config|{}): snacks.Picker\n---@field man fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field marks fun(opts?: snacks.picker.marks.Config|{}): snacks.Picker\n---@field notifications fun(opts?: snacks.picker.notifications.Config|{}): snacks.Picker\n---@field picker_actions fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field picker_format fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field picker_layouts fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field picker_preview fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field pickers fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field projects fun(opts?: snacks.picker.projects.Config|{}): snacks.Picker\n---@field qflist fun(opts?: snacks.picker.qf.Config|{}): snacks.Picker\n---@field recent fun(opts?: snacks.picker.recent.Config|{}): snacks.Picker\n---@field registers fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field resume fun(): snacks.Picker\n---@field scratch fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field search_history fun(opts?: snacks.picker.history.Config|{}): snacks.Picker\n---@field smart fun(opts?: snacks.picker.smart.Config|{}): snacks.Picker\n---@field spelling fun(opts?: snacks.picker.Config|{}): snacks.Picker\n---@field tags fun(opts?: snacks.picker.tags.Config|{}): snacks.Picker\n---@field treesitter fun(opts?: snacks.picker.treesitter.Config|{}): snacks.Picker\n---@field undo fun(opts?: snacks.picker.undo.Config|{}): snacks.Picker\n---@field zoxide fun(opts?: snacks.picker.Config|{}): snacks.Picker\n"
  },
  {
    "path": "lua/snacks/picker/util/async.lua",
    "content": "---@class snacks.picker.async\nlocal M = {}\n\n---@type snacks.picker.Async[]\nM._active = {}\n---@type snacks.picker.Async[]\nM._suspended = {}\nM._executor = assert((vim.uv or vim.loop).new_check())\n\nM.BUDGET = 10\n\n---@type table<thread, snacks.picker.Async>\nM._threads = setmetatable({}, { __mode = \"k\" })\n\nlocal uv = (vim.uv or vim.loop)\n\nfunction M.exiting()\n  return vim.v.exiting ~= vim.NIL\nend\n\n---@alias snacks.picker.AsyncEvent \"done\" | \"error\" | \"yield\" | \"ok\" | \"abort\"\n\n---@class snacks.picker.Waitable\n---@field wait async fun()\n\n---@class snacks.picker.Async\n---@field _co? thread\n---@field _fn fun()\n---@field _suspended? boolean\n---@field _aborted? boolean\n---@field _start number\n---@field _on table<snacks.picker.AsyncEvent, fun(res:any, async:snacks.picker.Async)[]>\nlocal Async = {}\nAsync.__index = Async\n\n---@param fn async fun()\n---\nfunction Async.new(fn)\n  local self = setmetatable({}, Async)\n  return self:init(fn)\nend\n\n---@param fn async fun()\n---@return snacks.picker.Async\nfunction Async:init(fn)\n  self._fn = fn\n  self._on = {}\n  self._start = uv.hrtime()\n  self._co = coroutine.create(function()\n    local ok, err = xpcall(self._fn, function(err)\n      return debug.traceback(err, 2)\n    end)\n    if not ok then\n      if self._aborted then\n        self:_emit(\"abort\")\n      else\n        self:_error(err)\n      end\n    end\n    self:_done()\n  end)\n  M._threads[self._co] = self\n  return M.add(self)\nend\n\nfunction Async:aborted()\n  return self._aborted\nend\n\nfunction Async:_done()\n  if self._co == nil then\n    return\n  end\n  self:_emit(\"done\")\n  self._fn = nil\n  M._threads[self._co] = nil\n  self._co = nil\n  self._on = {}\nend\n\nfunction Async:delta()\n  return (uv.hrtime() - self._start) / 1e6\nend\n\n---@param event snacks.picker.AsyncEvent\n---@param cb async fun(res:any, async:snacks.picker.Async)\nfunction Async:on(event, cb)\n  if event == \"done\" and not self:running() then\n    cb(nil, self)\n    return self\n  end\n  self._on[event] = self._on[event] or {}\n  table.insert(self._on[event], cb)\n  return self\nend\n\n---@private\n---@param event snacks.picker.AsyncEvent\n---@param res any\nfunction Async:_emit(event, res)\n  for _, cb in ipairs(self._on[event] or {}) do\n    cb(res, self)\n  end\nend\n\nfunction Async:_error(err)\n  if vim.tbl_isempty(self._on.error or {}) then\n    Snacks.notify.error(\"Unhandled async error:\\n\" .. err)\n  end\n  self:_emit(\"error\", err)\nend\n\nfunction Async:running()\n  return self._co and coroutine.status(self._co) ~= \"dead\" and not self._aborted\nend\n\n---@async\nfunction Async:sleep(ms)\n  self:defer(ms, function() end)\nend\n\n--- Suspends the current async context.\n--- Runs `fn` on the main thread and resumes the async context,\n--- returning the result of `fn` or raising an error if `fn` errors.\n---@generic T: any?\n---@param fn fun(): T?\n---@async\n---@return T\nfunction Async:schedule(fn)\n  self:assert()\n  local ret ---@type {[1]: boolean, [number]:any}\n  vim.schedule(function()\n    ret = { pcall(fn) }\n    self:resume()\n  end)\n  self:suspend()\n  if not ret[1] then\n    error(ret[2])\n  end\n  return select(2, unpack(ret))\nend\n\nfunction Async:assert()\n  assert(coroutine.running() == self._co, \"Not in an async context\")\nend\n\n--- Same as schedule, but defers the execution by `ms` milliseconds.\n---@generic T: any\n---@param fn fun(): T?\n---@param ms number\n---@async\n---@return T\nfunction Async:defer(ms, fn)\n  self:assert()\n  local ret ---@type {[1]: boolean, [number]:any}\n  vim.defer_fn(function()\n    ret = { pcall(fn) }\n    self:resume()\n  end, ms)\n  self:suspend()\n  if not ret[1] then\n    error(ret[2])\n  end\n  return select(2, unpack(ret))\nend\n\n---@async\n---@param yield? boolean\nfunction Async:suspend(yield)\n  self._suspended = true\n  if coroutine.running() == self._co and yield ~= false then\n    M.yield()\n  end\nend\n\nfunction Async:resume()\n  if not self._suspended then\n    return\n  end\n  self._suspended = false\n  M._run()\nend\n\n---@async\n---@param yield? boolean\nfunction Async:wake(yield)\n  local async = M.running()\n  assert(async, \"Not in an async context\")\n  self:on(\"done\", function()\n    async:resume()\n  end)\n  async:suspend(yield)\nend\n\n---@async\nfunction Async:wait()\n  if not self:running() then\n    return self\n  end\n  if coroutine.running() == self._co then\n    error(\"Cannot wait on self\")\n  end\n\n  local async = M.running()\n  if async then\n    self:wake()\n  else\n    while self:running() do\n      vim.wait(10)\n    end\n  end\n  return self\nend\n\nfunction Async:step()\n  if self._suspended then\n    return true\n  end\n  if not self._co then\n    return false\n  end\n  local status = coroutine.status(self._co)\n  if status == \"suspended\" then\n    local ok, res = coroutine.resume(self._co, self._aborted and \"abort\" or nil)\n    if not ok then\n      error(res)\n    elseif res then\n      self:_emit(\"yield\", res)\n    end\n  end\n  return self:running()\nend\n\nfunction Async:abort()\n  if not self:running() then\n    return\n  end\n  self._aborted = true\n  if self._co and coroutine.running() == self._co then\n    error(\"aborted\", 2)\n  end\n  self:resume()\nend\n\nfunction M.abort()\n  for _, async in ipairs(M._active) do\n    async:abort()\n  end\nend\n\n---@async\nfunction M.yield()\n  if coroutine.yield() == \"abort\" then\n    error(\"aborted\", 2)\n  end\nend\n\nfunction M.step()\n  local start = uv.hrtime()\n  for _ = 1, #M._active do\n    if M.exiting() or uv.hrtime() - start > M.BUDGET * 1e6 then\n      break\n    end\n\n    local state = table.remove(M._active, 1) ---@type snacks.picker.Async\n    if state:step() then\n      if state._suspended then\n        table.insert(M._suspended, state)\n      else\n        table.insert(M._active, state)\n      end\n    end\n  end\n  for _ = 1, #M._suspended do\n    local state = table.remove(M._suspended, 1)\n    table.insert(state._suspended and M._suspended or M._active, state)\n  end\n\n  -- M.debug()\n  if #M._active == 0 or M.exiting() then\n    return M._executor:stop()\n  end\nend\n\nfunction M.debug()\n  local lines = {\n    \"- active: \" .. #M._active,\n    \"- suspended: \" .. #M._suspended,\n  }\n  for _, async in ipairs(M._active) do\n    local info = debug.getinfo(async._fn)\n    local file = vim.fn.fnamemodify(info.short_src:sub(1), \":~:.\")\n    table.insert(lines, (\"%s:%d\"):format(file, info.linedefined))\n    if #lines > 10 then\n      break\n    end\n  end\n  local msg = table.concat(lines, \"\\n\")\n  M._notif = vim.notify(msg, nil, { replace = M._notif })\nend\n\n---@param async snacks.picker.Async\nfunction M.add(async)\n  table.insert(M._active, async)\n  M._run()\n  return async\nend\n\n---@async\nfunction M.suspend()\n  local async = assert(M.running(), \"Not in an async context\")\n  async:suspend()\nend\n\nfunction M._run()\n  if not M.exiting() and not M._executor:is_active() then\n    -- M._executor:start(vim.schedule_wrap(M.step))\n    M._executor:start(M.step)\n  end\nend\n\nfunction M.running()\n  local co = coroutine.running()\n  if co then\n    return M._threads[co]\n  end\nend\n\n---@async\n---@param ms number\nfunction M.sleep(ms)\n  local async = M.running()\n  assert(async, \"Not in an async context\")\n  async:sleep(ms)\nend\n\n---@param ms? number\nfunction M.yielder(ms)\n  if not coroutine.running() then\n    return function() end\n  end\n  local ns, count, start = (ms or 5) * 1e6, 0, uv.hrtime()\n  ---@async\n  return function()\n    count = count + 1\n    if count % 100 == 0 then\n      if uv.hrtime() - start > ns then\n        M.yield()\n        start = uv.hrtime()\n      end\n    end\n  end\nend\n\nlocal nop ---@type snacks.picker.Async\n--- Returns a no-op async function\nfunction M.nop()\n  if not nop then\n    nop = Async.new(function() end)\n    nop:step()\n    M._active = vim.tbl_filter(function(a)\n      return a ~= nop\n    end, M._active)\n  end\n  return nop\nend\n\nM.Async = Async\nM.new = Async.new\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/db.lua",
    "content": "local ffi = require(\"ffi\")\n\nffi.cdef([[\n  typedef struct sqlite3 sqlite3;\n  typedef struct sqlite3_stmt sqlite3_stmt;\n\n  int sqlite3_open(const char *filename, sqlite3 **ppDb);\n  int sqlite3_close(sqlite3*);\n  int sqlite3_exec(\n    sqlite3*, const char *sql, int (*callback)(void*,int,char**,char**), void*, char **errmsg);\n  int sqlite3_prepare_v2(\n    sqlite3*, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail);\n  int sqlite3_reset(sqlite3_stmt*);\n  int sqlite3_step(sqlite3_stmt*);\n  int sqlite3_finalize(sqlite3_stmt*);\n  int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*));\n  int sqlite3_bind_int64(sqlite3_stmt*, int, long long);\n  const unsigned char *sqlite3_column_text(sqlite3_stmt*, int);\n  long long sqlite3_column_int64(sqlite3_stmt*, int);\n]])\n\nlocal function sqlite3_lib()\n  local opts = Snacks.picker.config.get()\n  if opts.db.sqlite3_path then\n    return opts.db.sqlite3_path\n  end\n  if jit.os ~= \"Windows\" then\n    return \"sqlite3\"\n  end\n  local sqlite_path = vim.fn.stdpath(\"cache\") .. \"\\\\sqlite3.dll\"\n  if vim.fn.filereadable(sqlite_path) == 0 then\n    Snacks.notify(\"Downloading `sqlite3.dll`\")\n    local url = (\"https://www.sqlite.org/2025/sqlite-dll-win-%s-3480000.zip\"):format(jit.arch)\n    local out = vim.fn.system({\n      \"powershell\",\n      \"-Command\",\n      [[\n        $url = \"]] .. url .. [[\";\n        $zipPath = \"$env:TEMP\\sqlite.zip\";\n        $extractPath = \"$env:TEMP\\sqlite\";\n        Invoke-WebRequest -Uri $url -OutFile $zipPath;\n        Add-Type -AssemblyName System.IO.Compression.FileSystem;\n        [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractPath);\n\n        $dllPath = \"$extractPath\\sqlite3.dll\";\n        if (Test-Path $dllPath) {\n            Move-Item -Path $dllPath -Destination \"]] .. sqlite_path .. [[\" -Force;\n        } else {\n            Write-Host \"sqlite3.dll not found at $dllPath\";\n        }\n      ]],\n    })\n    if vim.v.shell_error ~= 0 then\n      Snacks.notify.error(\"Failed to download `sqlite3.dll`:\\n\" .. out)\n    else\n      Snacks.notify(\"Downloaded `sqlite3.dll`\")\n    end\n  end\n  return sqlite_path\nend\n\nlocal sqlite = ffi.load(sqlite3_lib())\n\n---@alias sqlite3* ffi.cdata*\n---@alias sqlite3_stmt* ffi.cdata*\n\n---@class snacks.picker.db\n---@field type type\n---@field db sqlite3*\n---@field handle ffi.cdata*\n---@field insert snacks.picker.db.Query\n---@field select snacks.picker.db.Query\nlocal M = {}\nM.__index = M\n\n---@param stmt ffi.cdata*\n---@param idx number\n---@param value any\n---@param value_type? type\nlocal function bind(stmt, idx, value, value_type)\n  value_type = value_type or type(value)\n  if value_type == \"string\" then\n    return sqlite.sqlite3_bind_text(stmt, idx, value, #value, nil)\n  elseif value_type == \"number\" then\n    return sqlite.sqlite3_bind_int64(stmt, idx, value)\n  elseif value_type == \"boolean\" then\n    return sqlite.sqlite3_bind_int64(stmt, idx, value and 1 or 0)\n  else\n    error(\"Unsupported value type: \" .. type(value) .. \" (\" .. tostring(value) .. \")\")\n  end\nend\n\n---@class snacks.picker.db.Query\n---@field stmt sqlite3_stmt*\n---@field handle ffi.cdata*\nlocal Query = {}\nQuery.__index = Query\n\nfunction Query.new(db, query)\n  local self = setmetatable({}, Query)\n  local stmt = ffi.new(\"sqlite3_stmt*[1]\")\n  local code = sqlite.sqlite3_prepare_v2(db.db, query, #query, stmt, nil) --[[@as number]]\n  if code ~= 0 then\n    error(\"Failed to prepare statement: \" .. code)\n  end\n  self.handle = stmt\n  ffi.gc(stmt, function()\n    self:close()\n  end)\n  self.stmt = stmt[0]\n  return self\nend\n\nfunction Query:reset()\n  return sqlite.sqlite3_reset(self.stmt)\nend\n\n---@param binds? any[]\nfunction Query:exec(binds)\n  self:reset()\n  for i, value in ipairs(binds or {}) do\n    if bind(self.stmt, i, value) ~= 0 then\n      error((\"Failed to bind %d=%s\"):format(i, value))\n    end\n  end\n  return self:step()\nend\n\n---@return number\nfunction Query:step()\n  return sqlite.sqlite3_step(self.stmt)\nend\n\nfunction Query:close()\n  if self.stmt then\n    sqlite.sqlite3_finalize(self.stmt)\n    self.stmt = nil\n  end\nend\n\nfunction Query:bind(idx, value)\n  return bind(self.stmt, idx, value)\nend\n\n---@param idx? number\n---@param value_type type\nfunction Query:col(value_type, idx)\n  idx = idx or 0\n  local ret = ffi.string(sqlite.sqlite3_column_text(self.stmt, idx))\n  if value_type == \"string\" then\n    return ret\n  elseif value_type == \"number\" then\n    return tonumber(ret)\n  elseif value_type == \"boolean\" then\n    return ret == \"1\"\n  end\n  error(\"Unsupported value type: \" .. value_type)\nend\n\nfunction M.new(path, value_type)\n  local self = setmetatable({}, M)\n  local handle = ffi.new(\"sqlite3*[1]\")\n  if sqlite.sqlite3_open(path, handle) ~= 0 then\n    error(\"Failed to open database: \" .. path)\n  end\n\n  self.handle = handle\n  self.db = handle[0]\n  self.type = value_type or \"number\"\n  self:exec(\"PRAGMA journal_mode=WAL\")\n\n  -- Create the table if it doesn't exist\n  self:exec(([[\n      CREATE TABLE IF NOT EXISTS data (\n        key TEXT PRIMARY KEY,\n        value %s NOT NULL\n      );\n    ]]):format(({\n    number = \"INTEGER\",\n    string = \"TEXT\",\n    boolean = \"INTEGER\",\n  })[self.type]))\n\n  self.insert = self:prepare(\"INSERT OR REPLACE INTO data (key, value) VALUES (?, ?);\")\n  self.select = self:prepare(\"SELECT value FROM data WHERE key = ?;\")\n\n  ffi.gc(handle, function()\n    self:close()\n  end)\n\n  return self\nend\n\n---@param query string\nfunction M:prepare(query)\n  return Query.new(self, query)\nend\n\nfunction M:close()\n  if self.db then\n    sqlite.sqlite3_close(self.db)\n    self.db = nil\n    self.handle = nil\n  end\nend\n\nfunction M:set(key, value)\n  if self.insert:exec({ key, value }) ~= 101 then -- 101 == SQLITE_DONE\n    error(\"Failed to execute insert statement\")\n  end\nend\n\n---@param query string\nfunction M:exec(query)\n  query = query:sub(-1) ~= \";\" and query .. \";\" or query\n  local errmsg = ffi.new(\"char*[1]\")\n  if sqlite.sqlite3_exec(self.db, query, nil, nil, errmsg) ~= 0 then\n    error(ffi.string(errmsg[0]))\n  end\nend\n\nfunction M:begin()\n  self:exec(\"BEGIN\")\nend\n\nfunction M:commit()\n  self:exec(\"COMMIT\")\nend\n\nfunction M:rollback()\n  self:exec(\"ROLLBACK\")\nend\n\n---@param key string\nfunction M:get(key)\n  if self.select:exec({ key }) == 100 then -- 100 == SQLITE_ROW\n    return self.select:col(self.type)\n  end\nend\n\nfunction M:count()\n  local query = self:prepare(\"SELECT COUNT(*) FROM data;\")\n  if query:exec() == 100 then\n    return query:col(\"number\")\n  end\nend\n\nfunction M:get_all()\n  local query = self:prepare(\"SELECT key, value FROM data;\")\n  local ret = {} ---@type table<string, any>\n  local code = query:exec()\n  while code == 100 do -- 100 == SQLITE_ROW\n    local k = query:col(\"string\", 0) -- key is always a string\n    local v = query:col(self.type, 1) -- value type is whatever you set\n    ret[k] = v\n    code = query:step()\n  end\n  query:close()\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/diff.lua",
    "content": "local M = {}\n\n---@class snacks.diff.Config\n---@field max_hunk_lines? number only show last N lines of each hunk (used by GitHub PRs)\n---@field hunk_header? boolean whether to show hunk header (default: true)\n---@field annotations? snacks.diff.Annotation[]\n\n---@class snacks.diff.Annotation\n---@field file string\n---@field side \"left\" | \"right\"\n---@field left? number\n---@field right? number\n---@field line number\n---@field text snacks.picker.Highlight[][]\n\n---@class snacks.diff.Meta\n---@field side \"left\" | \"right\"\n---@field file string\n---@field line number\n---@field code string\n\n---@class snacks.diff.ctx\n---@field diff snacks.picker.Diff\n---@field opts snacks.diff.Config\n---@field block? snacks.picker.diff.Block\n---@field hunk? snacks.picker.diff.Hunk\nlocal C = {}\nC.__index = C\n\n---@param ctx snacks.diff.ctx|{}\n---@return snacks.diff.ctx\nfunction C:extend(ctx)\n  return setmetatable(ctx, { __index = self })\nend\n\n---@param ... string\nlocal function diff_linenr(...)\n  local fg = Snacks.util.color(vim.list_extend({ ... }, { \"NormalFloat\", \"Normal\" }))\n  local bg = Snacks.util.color(vim.list_extend({ ... }, { \"NormalFloat\", \"Normal\" }), \"bg\")\n  bg = bg or vim.o.background == \"dark\" and \"#1e1e1e\" or \"#f5f5f5\"\n  fg = fg or vim.o.background == \"dark\" and \"#f5f5f5\" or \"#1e1e1e\"\n  return {\n    fg = fg,\n    bg = Snacks.util.blend(fg, bg, 0.1),\n  }\nend\n\nlocal CONFLICT_MARKERS = { \"<<<<<<<\", \"=======\", \">>>>>>>\", \"|||||||\" }\nrequire(\"snacks.picker\") -- ensure picker hl groups are available\n\nSnacks.util.set_hl({\n  DiffHeader = \"DiagnosticVirtualTextInfo\",\n  DiffAdd = \"DiffAdd\",\n  DiffDelete = \"DiffDelete\",\n  HunkHeader = \"Normal\",\n  DiffContext = \"DiffChange\",\n  DiffConflict = \"DiagnosticVirtualTextWarn\",\n  DiffAddLineNr = diff_linenr(\"DiffAdd\"),\n  DiffLabel = \"@property\",\n  DiffDeleteLineNr = diff_linenr(\"DiffDelete\"),\n  DiffContextLineNr = diff_linenr(\"DiffChange\"),\n  DiffConflictLineNr = diff_linenr(\"DiagnosticVirtualTextWarn\"),\n}, { default = true, prefix = \"Snacks\" })\n\nlocal H = Snacks.picker.highlight\nlocal U = Snacks.picker.util\n\n---@param diff string|string[]|snacks.picker.Diff\nfunction M.get_diff(diff)\n  if type(diff) == \"string\" then\n    diff = vim.split(diff, \"\\n\", { plain = true })\n  end\n  ---@cast diff snacks.picker.Diff|string[]\n  if type(diff[1]) == \"string\" then\n    diff = require(\"snacks.picker.source.diff\").parse(diff)\n  end\n  ---@cast diff snacks.picker.Diff\n  return diff\nend\n\n---@param buf number\n---@param ns number\n---@param diff string|string[]|snacks.picker.Diff\n---@param opts? snacks.diff.Config\nfunction M.render(buf, ns, diff, opts)\n  diff = M.get_diff(diff)\n  local ret = M.format(diff, opts)\n  return H.render(buf, ns, ret)\nend\n\n---@param diff string|string[]|snacks.picker.Diff\n---@param opts? snacks.diff.Config\nfunction M.format(diff, opts)\n  local ctx = C:extend({\n    diff = M.get_diff(diff),\n    opts = opts or {},\n  })\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  vim.list_extend(ret, M.format_header(ctx))\n  for _, block in ipairs(ctx.diff.blocks) do\n    vim.list_extend(ret, M.format_block(ctx:extend({ block = block })))\n  end\n  return ret\nend\n\n---@param ctx snacks.diff.ctx\nfunction M.format_header(ctx)\n  if #(ctx.diff.header or {}) == 0 then\n    return {}\n  end\n  local popts = Snacks.picker.config.get({})\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  local msg = {} ---@type string[]\n  for _, line in ipairs(ctx.diff.header or {}) do\n    local hash = line:match(\"^commit%s+(%S+)$\")\n    if hash then\n      ret[#ret + 1] = {\n        { \"Commit\", \"SnacksDiffLabel\" },\n        { \": \", \"SnacksPickerDelim\" },\n        { popts.icons.git.commit, \"SnacksPickerGitCommit\" },\n        { hash:sub(1, 8), \"SnacksPickerGitCommit\" },\n      }\n    else\n      local label, value = line:match(\"^(%S+):%s*(.-)%s*$\")\n      if label and value then\n        ret[#ret + 1] = {\n          { label, \"SnacksDiffLabel\" },\n          { \": \", \"SnacksPickerDelim\" },\n          { value, \"SnacksPickerGit\" .. label },\n        }\n      elseif line:match(\"^    \") then\n        msg[#msg + 1] = line:match(\"^    (.-)%s*$\")\n      else\n        ret[#ret + 1] = { { line } }\n      end\n    end\n  end\n  local subject = table.remove(msg, 1) or \"\"\n  if subject then\n    ret[#ret + 1] = {}\n    ---@diagnostic disable-next-line: missing-fields\n    ret[#ret + 1] = Snacks.picker.format.commit_message({ msg = subject }, {})\n  end\n  if #msg > 0 then\n    ret[#ret + 1] = H.rule()\n    local virt_lines = H.get_virtual_lines(table.concat(msg, \"\\n\"), { ft = \"markdown\" })\n    for _, vl in ipairs(virt_lines) do\n      ret[#ret + 1] = vl\n    end\n  end\n  ret[#ret + 1] = H.rule()\n  return ret\nend\n\n---@param ctx snacks.diff.ctx\nfunction M.format_block(ctx)\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  vim.list_extend(ret, M.format_block_header(ctx))\n  for _, hunk in ipairs(ctx.block.hunks) do\n    local hunk_lines = M.format_hunk(ctx:extend({ hunk = hunk }))\n    if ctx.opts and ctx.opts.max_hunk_lines and #hunk_lines > ctx.opts.max_hunk_lines then\n      hunk_lines = vim.list_slice(hunk_lines, #hunk_lines - ctx.opts.max_hunk_lines + 1)\n    end\n    vim.list_extend(ret, hunk_lines)\n  end\n  return ret\nend\n\n---@param ctx snacks.diff.ctx\nfunction M.format_block_header(ctx)\n  local block = assert(ctx.block)\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  ret[#ret + 1] = H.add_eol({}, \"SnacksDiffHeader\")\n\n  local icon, icon_hl = Snacks.util.icon(block.file)\n  local file = {} ---@type snacks.picker.Highlight[]\n  file[#file + 1] = { \"  \" }\n  -- needed to play nice with markview / markdown-renderer\n  file[#file + 1] = { col = 0, virt_text = { { \"  \", \"SnacksDiffHeader\" } }, virt_text_pos = \"overlay\" }\n  file[#file + 1] = { icon, icon_hl, inline = true }\n  file[#file + 1] = { \"  \" }\n\n  if block.rename then\n    file[#file + 1] = { block.rename.from }\n    file[#file + 1] = { \" -> \", \"SnacksPickerDelim\" }\n    file[#file + 1] = { block.rename.to }\n  else\n    file[#file + 1] = { block.file }\n  end\n  H.insert_hl(file, \"SnacksDiffHeader\")\n  H.add_eol(file, \"SnacksDiffHeader\")\n  ret[#ret + 1] = file\n\n  ret[#ret + 1] = H.add_eol({}, \"SnacksDiffHeader\")\n  return ret\nend\n\n---@param ctx snacks.diff.ctx\nfunction M.parse_hunk(ctx)\n  local block = assert(ctx.block)\n  local hunk = assert(ctx.hunk)\n  local diff = vim.deepcopy(hunk.diff)\n  local versions = {} ---@type snacks.picker.diff.hunk.Pos[]\n  local unmerged = #versions > 2\n  local lines, prefixes, conflict_markers = {}, {}, {} ---@type string[], string[], table<number, string>\n\n  -- build versions\n  versions[#versions + 1] = hunk.left\n  vim.list_extend(versions, hunk.parents or {})\n  versions[#versions + 1] = hunk.right\n  while #versions < 2 do -- normally should not happen, but just in case\n    versions[#versions + 1] = { line = hunk.line, count = 0 }\n  end\n\n  -- setup diff lines\n  table.remove(diff, 1) -- remove hunk header line\n  while #diff > 0 and diff[#diff]:match(\"^%s*$\") do\n    table.remove(diff) -- remove trailing empty lines\n  end\n\n  -- parse diff lines\n  for l, line in ipairs(diff) do\n    prefixes[#prefixes + 1] = line:sub(1, #versions - 1)\n    local code_line = line:sub(#versions)\n    if unmerged and vim.tbl_contains(CONFLICT_MARKERS, code_line:match(\"^%s*(%S+)\")) then\n      conflict_markers[l] = code_line\n      code_line = \"\"\n    end\n    lines[#lines + 1] = code_line\n  end\n\n  -- generate virt lines\n  table.insert(lines, 1, hunk.context or \"\") -- add hunk context for syntax highlighting\n  local ft = vim.filetype.match({ filename = block.file, contents = lines }) or \"\"\n  local text = H.get_virtual_lines(table.concat(lines, \"\\n\"), { ft = ft })\n  local context = table.remove(text, 1) -- remove hunk context virt lines\n  table.remove(lines, 1) -- remove hunk context code line\n\n  ---@class snacks.diff.hunk.Parse\n  local ret = {\n    len = #diff, -- number of lines in hunk\n    versions = versions, -- positions of each version\n    lines = lines, -- code lines of hunk\n    text = text, -- virt lines of hunk\n    prefixes = prefixes, -- diff prefixes of hunk\n    conflict_markers = conflict_markers, -- conflict markers lines of hunk\n    context = context, -- virt lines of hunk context\n    unmerged = unmerged, -- whether hunk is unmerged\n  }\n  return ret\nend\n\n--- Build hunk line index for each version\n---@param parse snacks.diff.hunk.Parse\nfunction M.build_hunk_index(parse)\n  local versions = parse.versions\n  local index = {} ---@type table<number, number>[]|{max: number}\n  local idx = {} ---@type number[]\n  for p, pos in ipairs(versions) do\n    idx[p] = idx[p] or ((pos.line or 1) - 1)\n  end\n  local max = 0\n  for l = 1, parse.len do\n    local prefix = parse.prefixes[l]\n    index[l] = {}\n\n    if not parse.conflict_markers[l] then\n      -- Increment parent versions\n      for i = 1, #versions - 1 do\n        local char = prefix:sub(i, i)\n        if char == \" \" or char == \"-\" then\n          idx[i] = idx[i] + 1\n          index[l][i] = idx[i]\n          max = math.max(max, #tostring(idx[i]))\n        end\n      end\n    end\n\n    -- Increment working (right)\n    -- Working increments if any char is ' ' or '+' (i.e., NOT all are '-')\n    local has_working = false\n    for i = 1, #prefix do\n      if prefix:sub(i, i) ~= \"-\" then\n        has_working = true\n        break\n      end\n    end\n    if has_working then\n      idx[#idx] = idx[#idx] + 1\n      index[l][#idx] = idx[#idx]\n      max = math.max(max, #tostring(idx[#idx]))\n    end\n  end\n  index.max = max\n  return index\nend\n\n---@param parse snacks.diff.hunk.Parse\nfunction M.format_hunk_header(parse)\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  local header = {} ---@type snacks.picker.Highlight[]\n  header[#header + 1] = { \"  \" }\n  header[#header + 1] = { \" \", \"Special\" }\n  header[#header + 1] = { \" \" }\n  H.extend(header, parse.context)\n  local context_width = H.offset(parse.context)\n  ret[#ret + 1] = {\n    { string.rep(\"─\", context_width + 7) .. \"┐\", \"FloatBorder\" },\n  }\n  header[#header + 1] = { \"  │\", \"FloatBorder\" }\n  ret[#ret + 1] = header\n  ret[#ret + 1] = {\n    { string.rep(\"─\", context_width + 7) .. \"┘\", \"FloatBorder\" },\n  }\n  return ret\nend\n\n---@param ctx snacks.diff.ctx\nfunction M.format_hunk(ctx)\n  local block = assert(ctx.block)\n  local ret = {} ---@type snacks.picker.Highlight[][]\n\n  local parse = M.parse_hunk(ctx)\n\n  local annotations = {} ---@type table<string, snacks.picker.Highlight[][]>\n  for _, annotation in ipairs(ctx.opts.annotations or {}) do\n    if annotation.file == block.file then\n      annotations[(\"%s:%d\"):format(annotation.side, annotation.line)] = annotation.text\n    end\n  end\n\n  local index = M.build_hunk_index(parse)\n\n  if ctx.opts.hunk_header ~= false then\n    vim.list_extend(ret, M.format_hunk_header(parse))\n  end\n\n  local in_conflict = false\n  for l = 1, parse.len do\n    local have_left, have_right = index[l][1] ~= nil, index[l][#parse.versions] ~= nil\n    local hl = (parse.conflict_markers[l] and \"SnacksDiffConflict\")\n      or (have_right and not have_left and \"SnacksDiffAdd\")\n      or (have_left and not have_right and \"SnacksDiffDelete\")\n      or \"SnacksDiffContext\"\n\n    local prefix = parse.prefixes[l]\n    if parse.unmerged then\n      local p = \"  \"\n      local marker = parse.conflict_markers[l] or \"\"\n      marker = marker:match(\"^%s*(%S+)\") or \"\"\n      if marker == \"<<<<<<<\" then\n        in_conflict = true\n        p = \"┌╴\"\n      elseif marker == \">>>>>>>\" then\n        in_conflict = false\n        p = \"└╴\"\n      elseif marker == \"=======\" or marker == \"|||||||\" then\n        p = \"├╴\"\n      elseif in_conflict then\n        p = \"│ \"\n      end\n      prefix = U.align(p, 2) .. prefix\n    end\n\n    local line = {} ---@type snacks.picker.Highlight[]\n\n    local line_nr = {} ---@type string[]\n    for i = 1, #parse.versions do\n      line_nr[i] =\n        U.align(tostring(index[l][i] or \"\"), index.max, { align = i == #parse.versions and \"right\" or \"left\" })\n    end\n    local line_col = \" \" .. table.concat(line_nr, \"  \") .. \" \"\n    local prefix_col = \" \" .. prefix .. \" \"\n\n    -- empty linenr overlay that will be used for wrapped lines\n    line[#line + 1] = {\n      col = 0,\n      virt_text = { { string.rep(\" \", #line_col), hl .. \"LineNr\" } },\n      virt_text_pos = \"overlay\",\n      hl_mode = \"replace\",\n      virt_text_repeat_linebreak = true,\n    }\n\n    -- linenr overlay\n    line[#line + 1] = {\n      col = 0,\n      virt_text = { { line_col, hl .. \"LineNr\" } },\n      virt_text_pos = \"overlay\",\n      hl_mode = \"replace\",\n    }\n\n    -- empty prefix overlay that will be used for wrapped lines\n    local ws = (parse.conflict_markers[l] or parse.lines[l]):match(\"^(%s*)\") -- add ws for breakindent\n    line[#line + 1] = {\n      col = #line_col,\n      virt_text = { { U.align(prefix_col:gsub(\"[%-%+]\", \" \"), #ws + #prefix_col), hl } },\n      virt_text_pos = \"overlay\",\n      hl_mode = \"replace\",\n      virt_text_repeat_linebreak = true,\n    }\n\n    -- prefix overlay\n    line[#line + 1] = {\n      col = #line_col,\n      virt_text = { { prefix_col, hl } },\n      virt_text_pos = \"overlay\",\n      hl_mode = \"replace\",\n    }\n\n    if have_left or have_right then\n      line[#line + 1] = {\n        \"\",\n        meta = {\n          ---@type snacks.diff.Meta\n          diff = {\n            side = have_right and \"right\" or \"left\",\n            file = block.file,\n            line = have_right and index[l][#parse.versions] or index[l][1],\n            code = parse.lines[l],\n          },\n        },\n      }\n    end\n\n    ret[#ret + 1] = line\n\n    local annot_left = \"left:\" .. (index[l][1] or \"\")\n    local annot_right = \"right:\" .. (index[l][#parse.versions] or \"\")\n    local ann = annotations[annot_left] or annotations[annot_right]\n    if ann then\n      vim.list_extend(\n        ret,\n        M.format_annotation(ann, {\n          indent = { line[1] },\n          indent_width = #line_col,\n          hl = hl,\n        })\n      )\n    end\n\n    local vl = H.indent({}, #line_col + #prefix_col)\n    if parse.conflict_markers[l] then\n      vl[#vl + 1] = { parse.conflict_markers[l], hl }\n    else\n      vim.list_extend(vl, parse.text[l] or {})\n    end\n    H.insert_hl(vl, hl)\n    H.extend(line, vl)\n    H.add_eol(line, hl)\n  end\n  return ret\nend\n\n---@param annotation snacks.picker.Highlight[][]\n---@param ctx {indent: snacks.picker.Highlight[][], indent_width: number, hl: string}\nfunction M.format_annotation(annotation, ctx)\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  local box, width = M.format_box(annotation)\n\n  local empty = vim.deepcopy(ctx.indent) ---@type snacks.picker.Highlight[]\n  vim.list_extend(empty, H.indent({}, ctx.indent_width + 2, ctx.hl))\n  H.add_eol(empty, ctx.hl)\n\n  ret[#ret + 1] = vim.deepcopy(empty)\n  for _, line in ipairs(box) do\n    for _, chunk in ipairs(line) do\n      if chunk.virt_text_win_col then\n        chunk.virt_text_win_col = chunk.virt_text_win_col + ctx.indent_width + 2\n      end\n    end\n    local al = vim.deepcopy(ctx.indent)\n    local vl = H.indent({}, ctx.indent_width + 2, ctx.hl)\n    vl[#vl + 1] = { -- repeat indent for the space before box\n      col = ctx.indent_width,\n      virt_text = { { \"  \", ctx.hl } },\n      virt_text_pos = \"overlay\",\n      hl_mode = \"replace\",\n      virt_text_repeat_linebreak = true,\n    }\n    H.extend(al, vl)\n    H.extend(al, vim.deepcopy(line))\n    H.add_eol(al, ctx.hl, width + ctx.indent_width + 6)\n    ret[#ret + 1] = al\n  end\n  ret[#ret + 1] = vim.deepcopy(empty)\n  return ret\nend\n\n---@param lines snacks.picker.Highlight[][]\n---@param border_hl? string\nfunction M.format_box(lines, border_hl)\n  border_hl = border_hl or \"FloatBorder\"\n  local ret = {} ---@type snacks.picker.Highlight[][]\n  local width = 0\n  for _, line in ipairs(lines) do\n    width = math.max(width, H.offset(line, { char_idx = true }))\n  end\n  width = math.max(width, 50) --[[@as number]]\n\n  ---@param text snacks.picker.Highlight[]\n  ---@param col? number\n  local function vt(text, col)\n    ---@type snacks.picker.Highlight\n    return {\n      col = 0,\n      virt_text_pos = \"overlay\",\n      virt_text_win_col = col,\n      virt_text = text,\n      virt_text_repeat_linebreak = true,\n    }\n  end\n\n  ret[#ret + 1] = {\n    vt({\n      { \"┌\", border_hl },\n      { string.rep(\"─\", width + 2), border_hl },\n      { \"┐\", border_hl },\n    }),\n  }\n  for _, line in ipairs(lines) do\n    ret[#ret + 1] = {\n      vt({\n        { \"│\", border_hl },\n        { \" \" },\n      }),\n      { \"  \" },\n    }\n    H.extend(ret[#ret], vim.deepcopy(line))\n    table.insert(ret[#ret], vt({ { \"│\", border_hl } }, width + 3))\n  end\n  ret[#ret + 1] = {\n    vt({\n      { \"└\", border_hl },\n      { string.rep(\"─\", width + 2), border_hl },\n      { \"┘\", border_hl },\n    }),\n  }\n  return ret, width\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/highlight.lua",
    "content": "---@class snacks.picker.highlight\nlocal M = {}\n\n---@class (private) vim.var_accessor\n---@field snacks_meta? table<number,snacks.picker.Meta>\n\nM.langs = {} ---@type table<string, boolean>\nM._scratch = {} ---@type table<string, number>\n\n---@param source string\n---@param lang string\nfunction M.scratch_buf(source, lang)\n  local buf = M._scratch[lang]\n  if not (buf and vim.api.nvim_buf_is_valid(buf)) then\n    buf = vim.api.nvim_create_buf(false, true)\n    vim.api.nvim_buf_set_name(buf, \"snacks://picker/highlight/\" .. lang)\n    M._scratch[lang] = buf\n  end\n  vim.bo[buf].fixeol = false\n  vim.bo[buf].eol = false\n  vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(source, \"\\n\", { plain = true }))\n  return buf\nend\n\n---@param opts? {buf?:number, code?:string, ft?:string, lang?:string, file?:string, extmarks?:boolean}\nfunction M.get_highlights(opts)\n  opts = opts or {}\n  assert(opts.buf or opts.code, \"buf or code is required\")\n  assert(not (opts.buf and opts.code), \"only one of buf or code is allowed\")\n\n  local ret = {} ---@type table<number, snacks.picker.Extmark[]>\n\n  local ft = opts.ft\n    or (opts.buf and vim.bo[opts.buf].filetype)\n    or (opts.file and vim.filetype.match({ filename = opts.file, buf = 0 }))\n    or vim.bo.filetype\n  local lang = Snacks.util.get_lang(opts.lang or ft)\n  lang = lang and lang:lower() or nil\n  local parser, buf ---@type vim.treesitter.LanguageTree?, number?\n\n  if lang then\n    local ok = false\n    buf = opts.buf or M.scratch_buf(opts.code, lang)\n    ok, parser = pcall(vim.treesitter.get_parser, buf, lang)\n    parser = ok and parser or nil\n  end\n\n  if parser and buf then\n    parser:parse(true)\n    parser:for_each_tree(function(tstree, tree)\n      if not tstree then\n        return\n      end\n      local query = vim.treesitter.query.get(tree:lang(), \"highlights\")\n      -- Some injected languages may not have highlight queries.\n      if not query then\n        return\n      end\n\n      for capture, node, metadata in query:iter_captures(tstree:root(), buf) do\n        ---@type string\n        local name = query.captures[capture]\n        if name ~= \"spell\" then\n          local range = { node:range() } ---@type number[]\n          local multi = range[1] ~= range[3]\n          local text = multi\n              and vim.split(vim.treesitter.get_node_text(node, buf, metadata[capture]), \"\\n\", { plain = true })\n            or {}\n          for row = range[1] + 1, range[3] + 1 do\n            local first, last = row == range[1] + 1, row == range[3] + 1\n            local end_col = last and range[4] or #(text[row - range[1]] or \"\")\n            end_col = multi and first and end_col + range[2] or end_col\n            ret[row] = ret[row] or {}\n            table.insert(ret[row], {\n              col = first and range[2] or 0,\n              end_col = end_col,\n              priority = (tonumber(metadata.priority or metadata[capture] and metadata[capture].priority) or 100),\n              conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal,\n              hl_group = \"@\" .. name .. \".\" .. lang,\n            })\n          end\n        end\n      end\n    end)\n  end\n\n  --- Add buffer extmarks\n  if opts.buf and opts.extmarks then\n    local extmarks = vim.api.nvim_buf_get_extmarks(opts.buf, -1, 0, -1, { details = true })\n    for _, extmark in pairs(extmarks) do\n      local row = extmark[2] + 1\n      ret[row] = ret[row] or {}\n      local e = extmark[4]\n      if e then\n        e.sign_name = nil\n        e.sign_text = nil\n        e.ns_id = nil\n        e.end_row = nil\n        e.col = extmark[3]\n        if e.virt_text_pos and not vim.tbl_contains({ \"eol\", \"overlay\", \"right_align\", \"inline\" }, e.virt_text_pos) then\n          e.virt_text = nil\n          e.virt_text_pos = nil\n        end\n        table.insert(ret[row], e)\n      end\n    end\n  end\n\n  return ret\nend\n\n---@param source string|number\n---@param opts? {ft:string, bg?: string}\n---@return snacks.picker.Text[][]\nfunction M.get_virtual_lines(source, opts)\n  opts = opts or {}\n\n  local lines = type(source) == \"number\" and vim.api.nvim_buf_get_lines(source, 0, -1, false)\n    or vim.split(source --[[@as string]], \"\\n\")\n\n  local extmarks = M.get_highlights({\n    buf = type(source) == \"number\" and source or nil,\n    code = type(source) == \"string\" and source or nil,\n    ft = opts.ft,\n    lang = nil,\n  })\n  if not extmarks then\n    return vim.tbl_map(function(line)\n      return { { line } }\n    end, lines)\n  end\n\n  local index = {} ---@type table<number, table<number, string>>\n  for row, exts in pairs(extmarks) do\n    for _, e in ipairs(exts) do\n      if e.hl_group and e.end_col then\n        index[row] = index[row] or {}\n        for i = e.col + 1, e.end_col do\n          index[row][i] = e.hl_group\n        end\n      end\n    end\n  end\n\n  local ret = {} ---@type snacks.picker.Text[][]\n  for i = 1, #lines do\n    ret[i] = {}\n    local line = lines[i]\n    local from = 0\n    local hl_group = nil ---@type string?\n\n    ---@param to number\n    local function add(to)\n      if to >= from then\n        local text = line:sub(from, to)\n        local hl = opts.bg and { hl_group or \"Normal\", opts.bg } or hl_group\n        if #text > 0 then\n          table.insert(ret[i], { text, hl })\n        end\n      end\n      from = to + 1\n      hl_group = nil\n    end\n\n    for col = 1, #line do\n      local hl = index[i] and index[i][col]\n      if hl ~= hl_group then\n        add(col - 1)\n        hl_group = hl\n      end\n    end\n    add(#line)\n  end\n  return ret\nend\n\n---@param line snacks.picker.Highlight[]\n---@param opts? {char_idx?:boolean}\nfunction M.offset(line, opts)\n  opts = opts or {}\n  local offset = 0\n  for _, t in ipairs(line) do\n    if type(t[1]) == \"string\" and not t.inline then\n      if t.virtual then\n        offset = offset + vim.api.nvim_strwidth(t[1])\n      elseif opts.char_idx then\n        offset = offset + vim.api.nvim_strwidth(t[1])\n      else\n        offset = offset + #t[1]\n      end\n    elseif t.virt_text_pos == \"inline\" and t.virt_text and opts.char_idx then\n      offset = offset + M.offset(t.virt_text) + (t.col or 0)\n    end\n  end\n  return offset\nend\n\nfunction M.rule()\n  ---@type snacks.picker.Highlight[]\n  return {\n    {\n      col = 0,\n      virt_text_win_col = 0,\n      virt_text = { { string.rep(\"-\", math.max(vim.o.columns, 500)), \"SnacksPickerRule\" } },\n      priority = 100,\n    },\n  }\nend\n\n---@param line snacks.picker.Highlight[]\n---@param positions number[]\n---@param offset? number\nfunction M.matches(line, positions, offset)\n  offset = offset or 0\n  for _, pos in ipairs(positions) do\n    table.insert(line, {\n      col = pos - 1 + offset,\n      end_col = pos + offset,\n      hl_group = \"SnacksPickerMatch\",\n    })\n  end\n  return line\nend\n\n---@param line snacks.picker.Highlight[]\n---@param item snacks.picker.Item\n---@param text string\n---@param opts? {hl_group?:string, lang?:string}\nfunction M.format(item, text, line, opts)\n  opts = opts or {}\n  local offset = M.offset(line)\n  item._ = item._ or {}\n  item._.ts = item._.ts or {}\n  local highlights = item._.ts[text] ---@type table<number, snacks.picker.Extmark[]>?\n  if not highlights then\n    highlights = M.get_highlights({ code = text, ft = item.ft, lang = opts.lang or item.lang, file = item.file })[1]\n      or {}\n    item._.ts[text] = highlights\n  end\n  highlights = vim.deepcopy(highlights)\n  for _, extmark in ipairs(highlights) do\n    extmark.col = extmark.col + offset\n    extmark.end_col = extmark.end_col + offset\n    line[#line + 1] = extmark\n  end\n  line[#line + 1] = { text, opts.hl_group }\nend\n\n---@param line snacks.picker.Highlight[]\n---@param patterns table<string,string>\nfunction M.highlight(line, patterns)\n  local offset = M.offset(line)\n  local text ---@type string?\n  for i = #line, 1, -1 do\n    if type(line[i][1]) == \"string\" then\n      text = line[i][1]\n      break\n    end\n  end\n  if not text then\n    return\n  end\n  offset = offset - #text\n  for pattern, hl in pairs(patterns) do\n    local from, to, match = text:find(pattern)\n    while from do\n      if match then\n        from, to = text:find(match, from, true)\n      end\n      table.insert(line, {\n        col = offset + from - 1,\n        end_col = offset + to,\n        hl_group = hl,\n      })\n      from, to = text:find(pattern, to + 1)\n    end\n  end\nend\n\n---@param line snacks.picker.Highlight[]\nfunction M.markdown(line)\n  M.highlight(line, {\n    [\"^# .*\"] = \"@markup.heading.1.markdown\",\n    [\"^## .*\"] = \"@markup.heading.2.markdown\",\n    [\"^### .*\"] = \"@markup.heading.3.markdown\",\n    [\"^#### .*\"] = \"@markup.heading.4.markdown\",\n    [\"^##### .*\"] = \"@markup.heading.5.markdown\",\n    [\"`.-`\"] = \"SnacksPickerCode\",\n    [\"^%s*[%-%*]\"] = \"@markup.list.markdown\",\n    [\"%*.-%*\"] = \"SnacksPickerItalic\",\n    [\"%*%*.-%*%*\"] = \"SnacksPickerBold\",\n  })\nend\n\n---@param prefix string\n---@param links? table<string, string>\nfunction M.winhl(prefix, links)\n  links = links or {}\n  local winhl = {\n    NormalFloat = \"\",\n    FloatBorder = \"Border\",\n    FloatTitle = \"Title\",\n    FloatFooter = \"Footer\",\n    CursorLine = \"CursorLine\",\n  }\n  local ret = {} ---@type string[]\n  local groups = {} ---@type table<string, string>\n  for k, v in pairs(winhl) do\n    groups[v] = links[k] or (prefix == \"SnacksPicker\" and k or (\"SnacksPicker\" .. v))\n    ret[#ret + 1] = (\"%s:%s%s\"):format(k, prefix, v)\n  end\n  Snacks.util.set_hl(groups, { prefix = prefix, default = true })\n  return table.concat(ret, \",\")\nend\n\n--- Resolves the first flex text in the line.\n---@param line snacks.picker.Highlight[]\n---@param max_width number\nfunction M.resolve(line, max_width)\n  while true do\n    local offset = 0\n    local width = 0\n    local resolve ---@type number?\n    for t, text in ipairs(line) do\n      local w = M.offset({ text }, { char_idx = true })\n      if not resolve and type(text) == \"table\" and text.resolve then\n        ---@cast text snacks.picker.Text\n        resolve = t\n      elseif resolve then\n        width = width + w\n      else\n        width = width + w\n        offset = offset + w\n      end\n    end\n\n    if resolve then\n      local ret = {} ---@type snacks.picker.Highlight[]\n      vim.list_extend(ret, line, 1, resolve - 1)\n      offset = M.offset(ret)\n      vim.list_extend(ret, line[resolve].resolve(math.max(max_width - width, 1)))\n      local diff = M.offset(ret) - offset\n      vim.list_extend(ret, line, resolve + 1)\n      M.fix_offset(ret, diff, resolve + 1)\n      line = ret\n    else\n      return line\n    end\n  end\nend\n\n---@param line snacks.picker.Highlight[]\n---@param hl_group string\nfunction M.insert_hl(line, hl_group)\n  for _, t in ipairs(line) do\n    if type(t[1]) == \"string\" then\n      if t[2] == nil then\n        t[2] = hl_group\n      elseif type(t[2]) == \"string\" then\n        t[2] = { hl_group, t[2] }\n      elseif type(t[2]) == \"table\" then\n        table.insert(t[2], 1, hl_group)\n      end\n    end\n  end\n  return line\nend\n\n---@param line snacks.picker.Highlight[]\n---@param indent number\n---@param hl_group? string|string[]\nfunction M.indent(line, indent, hl_group)\n  local ret = {} ---@type snacks.picker.Highlight[]\n  ret[#ret + 1] = { string.rep(\" \", indent), hl_group }\n  M.extend(ret, line)\n  return ret\nend\n\n---@param line snacks.picker.Highlight[]\n---@param hl_group string\n---@param offset? number\nfunction M.add_eol(line, hl_group, offset)\n  line[#line + 1] = {\n    col = M.offset(line),\n    virt_text = { { string.rep(\" \", 1000), hl_group } },\n    virt_text_pos = \"overlay\",\n    hl_mode = \"replace\",\n    virt_text_win_col = offset,\n    virt_text_repeat_linebreak = true,\n  }\n  return line\nend\n\n---@param line snacks.picker.Highlight[]\n---@param opts? {offset?:number}\nfunction M.to_text(line, opts)\n  local offset = opts and opts.offset or 0\n  local ret = {} ---@type snacks.picker.Extmark[]\n  local meta = {} ---@type snacks.picker.Meta\n  local col = offset\n  local parts = {} ---@type string[]\n  for _, text in ipairs(line) do\n    if (type(text[2]) == \"string\" and text[1] == nil) or vim.tbl_isempty(text) then\n      text[1] = \"\"\n    end\n    for k, v in pairs(text.meta or {}) do\n      meta[k] = v\n    end\n    if type(text[1]) == \"string\" and #text[1] > 0 then\n      ---@cast text snacks.picker.Text\n      if text.virtual then\n        table.insert(ret, {\n          col = col,\n          virt_text = { { text[1], text[2] } },\n          virt_text_pos = \"overlay\",\n          hl_mode = \"combine\",\n        })\n        parts[#parts + 1] = string.rep(\" \", vim.api.nvim_strwidth(text[1]))\n      elseif text.inline then\n        table.insert(ret, {\n          col = col,\n          virt_text = { { text[1], text[2] } },\n          virt_text_pos = \"inline\",\n          hl_mode = \"replace\",\n        })\n        parts[#parts + 1] = \"\"\n      else\n        table.insert(ret, {\n          col = col,\n          end_col = col + #text[1],\n          hl_group = text[2],\n          field = text.field,\n        })\n        parts[#parts + 1] = text[1]\n      end\n      col = col + #parts[#parts]\n    elseif type(text[1]) ~= \"string\" then\n      text = vim.deepcopy(text)\n      text.col = text.col or 0\n      if text.col < 0 then\n        text.col = col + text.col\n      end\n      if text.end_col and text.end_col < 0 then\n        text.end_col = col + text.end_col\n      end\n      ---@cast text snacks.picker.Extmark\n      -- fix extmark col and end_col\n      text.col = text.col + offset\n      if text.end_col then\n        text.end_col = text.end_col + offset\n      end\n      table.insert(ret, text)\n    end\n  end\n  return table.concat(parts), ret, not vim.tbl_isempty(meta) and meta or nil\nend\n\n---@param hl snacks.picker.Highlight[]\n---@param start_idx? number\nfunction M.fix_offset(hl, offset, start_idx)\n  for i, t in ipairs(hl) do\n    if start_idx == nil or i >= start_idx then\n      if t.col and t.col >= 0 then\n        t.col = t.col + offset\n      end\n      if t.end_col and t.end_col >= 0 then\n        t.end_col = t.end_col + offset\n      end\n    end\n  end\n  return hl\nend\n\n--- tables with number as keys are stored in vim.b as an array,\n--- so we need to filter out vim.NIL\n---@param buf number\nfunction M.meta(buf)\n  local ret = {} ---@type table<number, snacks.picker.Meta>\n  for k, v in pairs(vim.b[buf].snacks_meta or {}) do\n    if v ~= vim.NIL then\n      ret[k] = v\n    end\n  end\n  return not vim.tbl_isempty(ret) and ret or nil\nend\n\n---@param dst snacks.picker.Highlight[]\n---@param src snacks.picker.Highlight[]\nfunction M.extend(dst, src)\n  local offset = M.offset(dst)\n  M.fix_offset(src, offset)\n  return vim.list_extend(dst, src)\nend\n\n---@param buf number\n---@param ns number\n---@param lines snacks.picker.Highlight[][]\n---@param opts? {append?:boolean}\nfunction M.render(buf, ns, lines, opts)\n  opts = opts or {}\n  local old_lines = opts.append and {} or vim.api.nvim_buf_get_lines(buf, 0, -1, false)\n\n  vim.bo[buf].modifiable = true\n  if not opts.append then\n    vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)\n  end\n\n  local meta = {} ---@type table<number, snacks.picker.Meta>\n\n  local changed = #lines ~= #old_lines\n  local offset = opts.append and vim.api.nvim_buf_line_count(buf) or 0\n  offset = offset == 1 and (vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] or \"\"):find(\"^%s*$\") and 0 or offset\n  for l, line in ipairs(lines) do\n    local line_text, extmarks, line_meta = Snacks.picker.highlight.to_text(line)\n    if line_text ~= old_lines[l] then\n      vim.api.nvim_buf_set_lines(buf, offset + l - 1, offset + l, false, { line_text })\n      changed = true\n    end\n    if line_meta then\n      meta[offset + l] = line_meta\n    end\n    for _, extmark in ipairs(extmarks) do\n      local e = vim.deepcopy(extmark)\n      e.col, e.row, e.field = nil, nil, nil\n      local ok, err = pcall(vim.api.nvim_buf_set_extmark, buf, ns, offset + l - 1, extmark.col, e)\n      if not ok then\n        Snacks.notify.error(\n          \"Failed to set extmark. This should not happen. Please report.\\n\"\n            .. err\n            .. \"\\n```lua\\n\"\n            .. vim.inspect(extmark)\n            .. \"\\n```\"\n        )\n      end\n    end\n  end\n\n  if not opts.append and #lines < #old_lines then\n    vim.api.nvim_buf_set_lines(buf, #lines, -1, false, {})\n  end\n\n  if not vim.tbl_isempty(meta) then\n    vim.b[buf].snacks_meta = meta\n  end\n\n  vim.bo[buf].modified = false\n  vim.bo[buf].modifiable = false\n  return changed\nend\n\n---@alias snacks.picker.badge.color string|{ fg:string, bg:string }\nlocal badge_cache = {} ---@type table<string, {hl:string, color:snacks.picker.badge.color}>\n\n---@param color snacks.picker.badge.color\nlocal function badge_hl(color)\n  local key = type(color) == \"string\" and color or (\"%s:%s\"):format(color.fg or \"\", color.bg or \"\")\n  if badge_cache[key] then\n    return badge_cache[key].hl\n  end\n\n  local fg, bg ---@type string, string\n  if type(color) == \"string\" then\n    if color:sub(1, 1) == \"#\" then\n      bg = color\n    else\n      fg, bg = Snacks.util.color(color, \"fg\"), Snacks.util.color(color, \"bg\")\n    end\n  else\n    fg, bg = color.fg, color.bg\n  end\n\n  if not fg and not bg then -- default to inverse of Normal\n    fg = Snacks.util.color(\"Normal\", \"bg\") or \"#ffffff\"\n    bg = Snacks.util.color(\"Normal\", \"fg\") or \"#000000\"\n  elseif fg and not bg then -- set bg to a blended version of fg and Normal bg\n    bg = bg or Snacks.util.color(\"Normal\", \"bg\") or \"#000000\"\n    bg = Snacks.util.blend(fg, bg, 0.1)\n  elseif bg and not fg then -- calculate fg based on bg brightness\n    local light, dark = \"#ffffff\", \"#000000\"\n    do\n      local normal_fg = Snacks.util.color(\"Normal\", \"fg\")\n      local normal_bg = Snacks.util.color(\"Normal\", \"bg\")\n      if vim.o.background == \"light\" then\n        normal_fg, normal_bg = normal_bg, normal_fg\n      end\n      light = normal_fg or light\n      dark = normal_bg or dark\n    end\n    local r, g, b = bg:match(\"#?(%x%x)(%x%x)(%x%x)\")\n    r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)\n    local yiq = (r * 299 + g * 587 + b * 114) / 1000\n    fg = yiq >= 128 and dark or light\n  end\n\n  local hl_group = (\"SnacksBadge_%s_%s\"):format(fg:sub(2), bg:sub(2))\n  vim.api.nvim_set_hl(0, hl_group, { fg = fg, bg = bg })\n  vim.api.nvim_set_hl(0, hl_group .. \"Inv\", { fg = bg })\n  badge_cache[key] = { hl = hl_group, color = color }\n  return hl_group\nend\n\n--- Renders a badge\n---@param text string\n---@param color snacks.picker.badge.color\n---@param opts? {virtual?:boolean}\nfunction M.badge(text, color, opts)\n  local left_sep, right_sep = \"\", \"\"\n\n  local hl_group = badge_hl(color)\n  ---@type snacks.picker.Highlight[]\n  return {\n    { left_sep, hl_group .. \"Inv\", inline = true },\n    { text, hl_group },\n    { right_sep, hl_group .. \"Inv\", inline = true },\n    { \" \" },\n  }\nend\n\nvim.api.nvim_create_autocmd(\"ColorScheme\", {\n  group = vim.api.nvim_create_augroup(\"snacks.picker.highlight,badges\", { clear = true }),\n  callback = function(ev)\n    local badges = badge_cache\n    badge_cache = {}\n    for _, v in pairs(badges) do\n      badge_hl(v.color)\n    end\n  end,\n})\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/history.lua",
    "content": "---@class snacks.picker.History\n---@field path string\n---@field kv snacks.picker.KeyValue\n---@field idx number\n---@field cursor number\nlocal M = {}\nM.__index = M\n\n---@type table<string, snacks.picker.KeyValue>\nM.stores = {}\n\n-- Save the history on exit\nvim.api.nvim_create_autocmd(\"ExitPre\", {\n  group = vim.api.nvim_create_augroup(\"snacks_history\", { clear = true }),\n  callback = function()\n    for n, kv in pairs(M.stores) do\n      kv:close()\n      M.stores[n] = nil\n    end\n  end,\n})\n\n---@param name string\n---@param opts? {filter?: fun(value: string): boolean}\nfunction M.new(name, opts)\n  opts = opts or {}\n  local self = setmetatable({}, M)\n  self.path = vim.fn.stdpath(\"data\") .. \"/snacks/\" .. name .. \".history\"\n  if not M.stores[name] then\n    M.stores[name] = require(\"snacks.picker.util.kv\").new(self.path, {\n      max_size = 1000,\n      ---@param a snacks.picker.KeyValue.entry\n      ---@param b snacks.picker.KeyValue.entry\n      cmp = function(a, b)\n        return a.key > b.key\n      end,\n    })\n  end\n  self.kv = M.stores[name]\n  -- re-index the data\n  self.kv.data = vim.tbl_values(self.kv.data)\n  if opts.filter then\n    self.kv.data = vim.tbl_filter(opts.filter, self.kv.data)\n  end\n  self.idx = #self.kv.data + 1\n  self.cursor = self.idx\n\n  return self\nend\n\nfunction M:is_current()\n  return self.cursor == self.idx\nend\n\nfunction M:record(value)\n  -- don't record value if it's identical to the last recorded value\n  if vim.deep_equal(self.kv:get(math.max(self.idx - 1, 1)), value) then\n    return\n  end\n  self.kv:set(self.idx, value)\nend\n\nfunction M:next()\n  self.cursor = math.min(self.cursor + 1, self.idx)\n  return self:get()\nend\n\nfunction M:prev()\n  self.cursor = math.max(self.cursor - 1, 1)\n  return self:get()\nend\n\nfunction M:get()\n  return self.kv:get(self.cursor)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/init.lua",
    "content": "---@class snacks.picker.util\nlocal M = {}\n\nlocal uv = vim.uv or vim.loop\n\nlocal str_byteindex_new = pcall(vim.str_byteindex, \"aa\", \"utf-8\", 1)\n\n---@param item snacks.picker.Item\n---@return string?\nfunction M.path(item)\n  if not (item and item.file) then\n    return\n  end\n  item._path = item._path\n    or svim.fs.normalize(item.cwd and item.cwd .. \"/\" .. item.file or item.file, { _fast = true, expand_env = false })\n  return item._path\nend\n\n---@param path string\n---@param len number\n---@param opts? {cwd?: string, kind?: \"left\" | \"center\" | \"right\"}\nfunction M.truncpath(path, len, opts)\n  opts = opts or {}\n  local cwd = svim.fs.normalize(opts and opts.cwd or vim.fn.getcwd(0), { _fast = true, expand_env = false })\n  local home = svim.fs.normalize(\"~\")\n  path = svim.fs.normalize(path, { _fast = true, expand_env = false })\n\n  if path:find(cwd .. \"/\", 1, true) == 1 and #path > #cwd then\n    path = path:sub(#cwd + 2)\n  else\n    local root = Snacks.git.get_root(path)\n    if root and root ~= \"\" and path:find(root, 1, true) == 1 then\n      local tail = vim.fn.fnamemodify(root, \":t\")\n      path = \"⋮\" .. tail .. \"/\" .. path:sub(#root + 2)\n    elseif path:find(home, 1, true) == 1 then\n      path = \"~\" .. path:sub(#home + 1)\n    end\n  end\n  path = path:gsub(\"/$\", \"\")\n\n  if opts.kind == \"left\" then\n    return M.truncate(path, len, true)\n  elseif opts.kind == \"right\" then\n    return M.truncate(path, len, false)\n  end\n\n  if vim.api.nvim_strwidth(path) <= len then\n    return path\n  end\n\n  local parts = vim.split(path, \"/\")\n  if #parts < 2 then\n    return path\n  end\n  local ret = table.remove(parts)\n  local first = table.remove(parts, 1)\n  if first == \"~\" and #parts > 0 then\n    first = \"~/\" .. table.remove(parts, 1)\n  end\n  local width = vim.api.nvim_strwidth(ret) + vim.api.nvim_strwidth(first) + 3\n  if width > len then\n    return first .. \"/…/\" .. M.truncate(ret, len - vim.api.nvim_strwidth(first) - 3, true)\n  end\n  while width < len and #parts > 0 do\n    local part = table.remove(parts) .. \"/\"\n    local w = vim.api.nvim_strwidth(part)\n    if width + w > len then\n      break\n    end\n    ret = part .. ret\n    width = width + w\n  end\n  return first .. \"/…/\" .. ret\nend\n\n---@param prompt string\n---@param fn fun()\nfunction M.confirm(prompt, fn)\n  Snacks.picker.select({ \"No\", \"Yes\" }, {\n    prompt = prompt,\n    snacks = {\n      layout = {\n        layout = {\n          max_width = 60,\n        },\n      },\n    },\n  }, function(_, idx)\n    if idx == 2 then\n      fn()\n    end\n  end)\nend\n\n---@alias snacks.picker.util.cmd.Opts {env?: table<string, string>, cwd?: string, input?: string}\n---@param cmd string|string[]\n---@param cb fun(output: string[], code: number)\n---@param opts? snacks.picker.util.cmd.Opts\nfunction M.cmd(cmd, cb, opts)\n  opts = opts or {}\n  local output = {} ---@type string[]\n  local id = vim.fn.jobstart(\n    cmd,\n    vim.tbl_extend(\"force\", opts or {}, {\n      on_stdout = function(_, data)\n        output[#output + 1] = table.concat(data, \"\\n\")\n      end,\n      on_stderr = function(_, data)\n        output[#output + 1] = table.concat(data, \"\\n\")\n      end,\n      on_exit = function(_, code)\n        if code == 0 then\n          cb(output, code)\n          return\n        end\n        Snacks.debug.cmd({\n          header = \"Command failed\",\n          cmd = cmd,\n          props = { code = code, [\"vim.o.shell\"] = vim.o.shell },\n          footer = vim.trim(table.concat(output, \"\")),\n          level = vim.log.levels.ERROR,\n        })\n      end,\n    })\n  )\n  if id <= 0 then\n    Snacks.notify.error((\"Failed to start job `%s`\"):format(cmd))\n  elseif opts.input then\n    vim.fn.chansend(id, opts.input .. \"\\n\")\n    vim.fn.chanclose(id, \"stdin\")\n  end\n  return id > 0 and id or nil\nend\n\n---@param item table<string, any>\n---@param keys string[]\nfunction M.text(item, keys)\n  local buffer = require(\"string.buffer\").new()\n  for _, key in ipairs(keys) do\n    if item[key] then\n      if #buffer > 0 then\n        buffer:put(\" \")\n      end\n      if key == \"pos\" or key == \"end_pos\" then\n        buffer:putf(\"%d:%d\", item[key][1], item[key][2])\n      else\n        buffer:put(tostring(item[key]))\n      end\n    end\n  end\n  return buffer:get()\nend\n\n---@param text? string\n---@param width number\n---@param opts? {align?: \"left\" | \"right\" | \"center\", truncate?: boolean}\nfunction M.align(text, width, opts)\n  text = text or \"\"\n  opts = opts or {}\n  opts.align = opts.align or \"left\"\n  local tw = vim.api.nvim_strwidth(text)\n  if tw > width then\n    return opts.truncate and (vim.fn.strcharpart(text, 0, width - 1) .. \"…\") or text\n  end\n  local left = math.floor((width - tw) / 2)\n  local right = width - tw - left\n  if opts.align == \"left\" then\n    left, right = 0, width - tw\n  elseif opts.align == \"right\" then\n    left, right = width - tw, 0\n  end\n  return (\" \"):rep(left) .. text .. (\" \"):rep(right)\nend\n\n---@param text string\n---@param width number\n---@param left? boolean\nfunction M.truncate(text, width, left)\n  local tw = vim.api.nvim_strwidth(text)\n  if tw > width then\n    return left and \"…\" .. vim.fn.strcharpart(text, tw - width + 1, width - 1)\n      or vim.fn.strcharpart(text, 0, width - 1) .. \"…\"\n  end\n  return text\nend\n\n-- Stops visual mode and returns the selected text\nfunction M.visual()\n  local modes = { \"v\", \"V\", Snacks.util.keycode(\"<C-v>\") }\n  local mode = vim.fn.mode():sub(1, 1) ---@type string\n  if not vim.tbl_contains(modes, mode) then\n    return\n  end\n  -- stop visual mode\n  vim.cmd(\"normal! \" .. mode)\n\n  local pos = vim.api.nvim_buf_get_mark(0, \"<\")\n  local end_pos = vim.api.nvim_buf_get_mark(0, \">\")\n\n  -- for some reason, sometimes the column is off by one\n  -- see: https://github.com/folke/snacks.nvim/issues/190\n  local col_to = math.min(end_pos[2] + 1, #vim.api.nvim_buf_get_lines(0, end_pos[1] - 1, end_pos[1], false)[1])\n\n  local lines = vim.api.nvim_buf_get_text(0, pos[1] - 1, pos[2], end_pos[1] - 1, col_to, {})\n  local text = table.concat(lines, \"\\n\")\n  ---@class snacks.picker.Visual\n  local ret = {\n    buf = vim.api.nvim_get_current_buf(),\n    pos = pos,\n    end_pos = end_pos,\n    text = text,\n  }\n  return ret\nend\n\n---@param str string\n---@param data table<string, string|boolean|number>|table<string, string|boolean|number>[]\n---@param opts? {prefix?: string, indent?: boolean, offset?: number[]}\nfunction M.tpl(str, data, opts)\n  opts = opts or {}\n\n  local function get(key)\n    if not vim.tbl_isempty(data) and svim.islist(data) and not getmetatable(data) then\n      for _, d in ipairs(data) do\n        if d[key] ~= nil then\n          return d[key]\n        end\n      end\n    else\n      if data[key] ~= nil then\n        return data[key]\n      end\n    end\n  end\n\n  local ret = (\n    str:gsub(\n      \"(\" .. vim.pesc(opts.prefix or \"\") .. \"%b{}\" .. \")\",\n      ---@param w string\n      function(w)\n        local inner = w:sub(2 + #(opts.prefix or \"\"), -2)\n        local key, default = inner:match(\"^(.-):(.*)$\")\n        local ret = get(key or inner)\n        if ret == \"\" and default then\n          return default\n        end\n        return ret or w\n      end\n    )\n  )\n  if opts.indent then\n    local lines = vim.split(ret:gsub(\"\\t\", \"  \"), \"\\n\", { plain = true })\n    local indent = 1000\n    for _, line in ipairs(lines) do\n      indent = math.min(indent, line:find(\"%S\") or 1000)\n    end\n    for l, line in ipairs(lines) do\n      lines[l] = line:sub(indent)\n    end\n  end\n  return ret\nend\n\n---@param str string\nfunction M.title(str)\n  return table.concat(\n    vim.tbl_map(function(s)\n      return s:sub(1, 1):upper() .. s:sub(2)\n    end, vim.split(str, \"_\")),\n    \" \"\n  )\nend\n\nfunction M.rtp()\n  local ret = {} ---@type string[]\n  vim.list_extend(ret, vim.api.nvim_get_runtime_file(\"\", true))\n  if package.loaded.lazy then\n    local extra = require(\"lazy.core.util\").get_unloaded_rtp(\"\")\n    vim.list_extend(ret, extra)\n  end\n  return ret\nend\n\n---@param str string\n---@return string text, string[] args\nfunction M.parse(str)\n  -- Format: this is a test -- -g=hello\n  local t, a = str:match(\"^(.-)%s+%-%-%s*(.*)$\")\n  if not t then\n    return str, {}\n  end\n  t, a = vim.trim(t), vim.trim(a:gsub(\"%s+\", \" \"))\n  local args = {} ---@type string[]\n  -- tokenize the args, keeping quoted strings together\n  local in_quote = nil ---@type string?\n  local c = 1\n  for i = 1, #a do\n    local char = a:sub(i, i)\n    if char == \"'\" or char == '\"' then\n      if in_quote == char then\n        in_quote = nil\n      else\n        in_quote = char\n      end\n    elseif char == \" \" and not in_quote then\n      args[#args + 1] = a:sub(c, i - 1)\n      c = i + 1\n    end\n  end\n  if c <= #a then\n    args[#args + 1] = a:sub(c)\n  end\n  return t, args\nend\n\n--- Resolves the item if it has a resolve function\n---@param item snacks.picker.Item?\nfunction M.resolve(item)\n  if item and item.resolve then\n    item.resolve(item)\n    item.resolve = nil\n  end\n  return item\nend\n\n--- Reads the lines of a file.\n--- This is about 8x faster than `vim.fn.readfile`\n--- and 3x faster than `io.lines` using a\n--- test files of 225KB and 8300 lines.\n---@param file string\nfunction M.lines(file)\n  local fd = uv.fs_open(file, \"r\", 438)\n  if not fd then\n    return {}\n  end\n  local stat = assert(uv.fs_fstat(fd))\n  local data = assert(uv.fs_read(fd, stat.size, 0))\n  uv.fs_close(fd)\n\n  local lines, from = {}, 1 --- @type string[], number\n  while from <= #data do\n    local nl = data:find(\"\\n\", from, true)\n    if nl then\n      local cr = data:byte(nl - 1, nl - 1) == 13 -- \\r\n      local line = data:sub(from, nl - (cr and 2 or 1))\n      lines[#lines + 1] = line\n      from = nl + 1\n    else\n      lines[#lines + 1] = data:sub(from)\n      break\n    end\n  end\n  return lines\nend\n\n---@param s string\n---@param index number\n---@param encoding string\n---@param strict_indexing? boolean\nfunction M.str_byteindex(s, index, encoding, strict_indexing)\n  if str_byteindex_new then\n    return vim.str_byteindex(s, encoding, index, strict_indexing)\n  elseif vim.str_byteindex then\n    ---@diagnostic disable-next-line: param-type-mismatch\n    return vim.str_byteindex(s, index, encoding == \"utf-16\")\n  elseif vim.lsp.util._str_byteindex then\n    return vim.lsp.util._str_byteindex(s, index, encoding)\n  end\n  error(\"No str_byteindex function available\")\nend\n\n--- Resolves the location of an item to byte positions\n---@param item snacks.picker.Item\n---@param buf? number\nfunction M.resolve_loc(item, buf)\n  if not item or not item.loc or item.loc.resolved then\n    return item\n  end\n\n  local lines = {} ---@type string[]\n  if item.buf and vim.api.nvim_buf_is_loaded(item.buf) then\n    -- valid and loaded buffer\n    lines = vim.api.nvim_buf_get_lines(item.buf, 0, -1, false)\n  elseif item.buf and vim.uri_from_bufnr(item.buf):sub(1, 4) ~= \"file\" then\n    -- item buffer with a custom uri\n    vim.fn.bufload(item.buf)\n    lines = vim.api.nvim_buf_get_lines(item.buf, 0, -1, false)\n  elseif buf and vim.api.nvim_buf_is_valid(buf) then\n    -- custom buffer (typically for preview)\n    lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)\n  elseif item.file then\n    -- last resort, read the file\n    lines = M.lines(item.file)\n  end\n\n  ---@param pos lsp.Position?\n  local function resolve(pos)\n    if not pos then\n      return\n    end\n    local line = lines[pos.line + 1]\n    local col = line and M.str_byteindex(line, pos.character, item.loc.encoding) or pos.character\n    return { pos.line + 1, col }\n  end\n  item.pos = resolve(item.loc.range[\"start\"])\n  item.end_pos = resolve(item.loc.range[\"end\"]) or item.end_pos\n  item.loc.resolved = true\n  return item\nend\n\n--- Returns the relative time from a given time\n--- as ... ago\n---@param time number in seconds\nfunction M.reltime(time)\n  local delta = os.time() - time\n  local tpl = {\n    { 1, 60, \"just now\", \"just now\" },\n    { 60, 3600, \"a minute ago\", \"%d minutes ago\" },\n    { 3600, 3600 * 24, \"an hour ago\", \"%d hours ago\" },\n    { 3600 * 24, 3600 * 24 * 7, \"yesterday\", \"%d days ago\" },\n    { 3600 * 24 * 7, 3600 * 24 * 7 * 4, \"a week ago\", \"%d weeks ago\" },\n  }\n  for _, v in ipairs(tpl) do\n    if delta < v[2] then\n      local value = math.floor(delta / v[1] + 0.5)\n      return value == 1 and v[3] or v[4]:format(value)\n    end\n  end\n  if os.date(\"%Y\", time) == os.date(\"%Y\") then\n    return os.date(\"%b %d\", time) ---@type string\n  end\n  return os.date(\"%b %d, %Y\", time) ---@type string\nend\n\n---@generic T: table\n---@param t T\n---@return T\nfunction M.shallow_copy(t)\n  local ret = {}\n  for k, v in pairs(t) do\n    ret[k] = v\n  end\n  return setmetatable(ret, getmetatable(t))\nend\n\n---@param opts? {main?: number, float?:boolean, filter?: fun(win:number, buf:number):boolean?}\nfunction M.pick_win(opts)\n  opts = Snacks.config.merge({\n    filter = function(win, buf)\n      return not vim.bo[buf].filetype:find(\"^snacks\")\n    end,\n  }, opts)\n\n  local overlays = {} ---@type snacks.win[]\n  local chars = \"asdfghjkl\"\n  local wins = {} ---@type number[]\n  for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do\n    local buf = vim.api.nvim_win_get_buf(win)\n    local keep = (opts.float or vim.api.nvim_win_get_config(win).relative == \"\")\n      and (not opts.filter or opts.filter(win, buf))\n    if keep then\n      wins[#wins + 1] = win\n    end\n  end\n  if #wins == 1 then\n    return wins[1]\n  elseif #wins == 0 then\n    return\n  end\n  for _, win in ipairs(wins) do\n    local c = chars:sub(1, 1)\n    chars = chars:sub(2)\n    overlays[c] = Snacks.win({\n      backdrop = false,\n      win = win,\n      focusable = false,\n      enter = false,\n      relative = \"win\",\n      width = 7,\n      height = 3,\n      text = (\"       \\n   %s   \\n       \"):format(c),\n      wo = {\n        winhighlight = \"NormalFloat:SnacksPickerPickWin\" .. (win == opts.main and \"Current\" or \"\"),\n      },\n    })\n  end\n  vim.cmd([[redraw!]])\n  local char = vim.fn.getcharstr()\n  for _, overlay in pairs(overlays) do\n    overlay:close()\n  end\n  local win = (char == Snacks.util.keycode(\"<cr>\")) or overlays[char]\n  if win and type(win) == \"table\" then\n    return win.opts.win\n  elseif win then\n    return opts.main\n  end\nend\n\n---@param path string\n---@param cwd? string\n---@return fun(): string?\nfunction M.parents(path, cwd)\n  cwd = cwd or uv.cwd()\n  if not (cwd and path:sub(1, #cwd) == cwd and #path > #cwd) then\n    return function() end\n  end\n  local to = #cwd + 1 ---@type number?\n  return function()\n    to = path:find(\"/\", to + 1, true)\n    return to and path:sub(1, to - 1) or nil\n  end\nend\n\n--- Checks if the path is a directory,\n--- if not it returns the parent directory\n---@param item string|snacks.picker.Item\nfunction M.dir(item)\n  local path = type(item) == \"table\" and M.path(item) or item\n  ---@cast path string\n  path = svim.fs.normalize(path)\n  return Snacks.util.path_type(path) == \"directory\" and path or vim.fs.dirname(path)\nend\n\n---@param paths string[]\n---@param dir string\nfunction M.copy(paths, dir)\n  dir = svim.fs.normalize(dir)\n  paths = vim.tbl_map(svim.fs.normalize, paths) ---@type string[]\n  for _, path in ipairs(paths) do\n    local name = vim.fn.fnamemodify(path, \":t\")\n    local to = dir .. \"/\" .. name\n    M.copy_path(path, to)\n  end\nend\n\n---@param from string\n---@param to string\nfunction M.copy_path(from, to)\n  if not uv.fs_stat(from) then\n    Snacks.notify.error((\"File `%s` does not exist\"):format(from))\n    return\n  end\n  if Snacks.util.path_type(from) == \"directory\" then\n    M.copy_dir(from, to)\n  else\n    M.copy_file(from, to)\n  end\nend\n\n---@param from string\n---@param to string\nfunction M.copy_file(from, to)\n  if vim.fn.filereadable(from) == 0 then\n    Snacks.notify.error((\"File `%s` is not readable\"):format(from))\n    return\n  end\n  if uv.fs_stat(to) then\n    Snacks.notify.error((\"File `%s` already exists\"):format(to))\n    return\n  end\n  local dir = vim.fs.dirname(to)\n  vim.fn.mkdir(dir, \"p\")\n  local ok, err = uv.fs_copyfile(from, to, { excl = true, ficlone = true })\n  if not ok then\n    Snacks.notify.error((\"Failed to copy file:\\n - from: `%s`\\n- to: `%s`\\n%s\"):format(from, to, err))\n  end\nend\n\n---@param from string\n---@param to string\nfunction M.copy_dir(from, to)\n  if vim.fn.isdirectory(from) == 0 then\n    Snacks.notify.error((\"Directory `%s` does not exist\"):format(from))\n    return\n  end\n  vim.fn.mkdir(to, \"p\")\n  for fname in vim.fs.dir(from, { follow = false }) do\n    local path = from .. \"/\" .. fname\n    M.copy_path(path, to .. \"/\" .. fname)\n  end\nend\n\n---@param buf number\n---@return (vim.bo|vim.wo)?\nfunction M.modeline(buf)\n  if not vim.api.nvim_buf_is_valid(buf) then\n    return\n  end\n  local lines = vim.api.nvim_buf_get_lines(buf, 0, vim.o.modelines, false)\n  vim.list_extend(lines, vim.api.nvim_buf_get_lines(buf, -vim.o.modelines, -1, false))\n  for _, line in ipairs(lines) do\n    local m, options = line:match(\"%S*%s+(%w+):%s*(.-)%s*$\")\n    if vim.tbl_contains({ \"vi\", \"vim\", \"ex\" }, m) and options then\n      local set = vim.split(options, \"[:%s]+\")\n      local ret = {} ---@type table<string, any>\n      for _, v in ipairs(set) do\n        if v ~= \"\" then\n          local k, val = v:match(\"([^=]+)=(.+)\")\n          if k then\n            ret[k] = tonumber(val) or val\n          else\n            ret[v:gsub(\"^no\", \"\")] = v:find(\"^no\") and false or true\n          end\n        end\n      end\n      return ret\n    end\n  end\nend\n\n--- Gets the list of binaries in the PATH.\n--- This won't check if the binary is executable.\n--- On Windows, additional extensions are checked.\nfunction M.get_bins()\n  local is_win = jit.os:find(\"Windows\")\n  local path = vim.split(os.getenv(\"PATH\") or \"\", is_win and \";\" or \":\", { plain = true })\n  local bins = {} ---@type table<string, string>\n  for _, p in ipairs(path) do\n    p = svim.fs.normalize(p)\n    for file, t in vim.fs.dir(p) do\n      if t ~= \"directory\" then\n        local fpath = p .. \"/\" .. file\n        local base, ext = file:match(\"^(.*)%.(%a+)$\")\n        if is_win then\n          if base and ext and vim.tbl_contains({ \"exe\", \"bat\", \"com\", \"cmd\" }, ext) then\n            bins[base] = bins[base] or fpath\n          end\n        else\n          bins[file] = bins[file] or fpath\n        end\n      end\n    end\n  end\n  return bins\nend\n\n---@param glob string\nfunction M.glob2pattern(glob)\n  local pattern = \"\"\n  local i = 1\n  while i <= #glob do\n    local c = glob:sub(i, i)\n    if c == \"*\" then\n      if i + 1 <= #glob and glob:sub(i + 1, i + 1) == \"*\" then -- '**'\n        pattern = pattern .. \".*\"\n        i = i + 2\n      else -- '*'\n        pattern = pattern .. \"[^/]*\"\n        i = i + 1\n      end\n    elseif c == \"?\" then\n      pattern = pattern .. \"[^/]\" -- Match exactly one non-'/' character\n      i = i + 1\n    else\n      c = c:match(\"^[%^%$%(%)%%%.%[%]%+%-]$\") and \"%\" .. c or c\n      pattern = pattern .. c\n      i = i + 1\n    end\n  end\n  pattern = pattern .. \"$\"\n  pattern = pattern\n    :gsub(\"^\" .. vim.pesc(\"[^/]*\"), \"\")\n    :gsub(\"^\" .. vim.pesc(\".*\"), \"\")\n    :gsub(vim.pesc(\"[^/]*$\") .. \"$\", \"\")\n    :gsub(vim.pesc(\".*$\") .. \"$\", \"\")\n  return pattern\nend\n\n---@param globs string[]\n---@return fun(file: string): boolean\nfunction M.globber(globs)\n  local patterns = {} ---@type string[]\n  for _, glob in ipairs(globs) do\n    table.insert(patterns, M.glob2pattern(glob))\n  end\n  ---@param file string\n  return function(file)\n    for _, pattern in ipairs(patterns) do\n      if file:find(pattern) then\n        return true\n      end\n    end\n    return false\n  end\nend\n\n---@param buf number\nfunction M.spinner(buf)\n  return require(\"snacks.picker.util.spinner\").new(buf)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/kv.lua",
    "content": "---@class snacks.picker.KeyValue\n---@field data table<string, number>\n---@field loaded_time number\n---@field path string\n---@field max_size number\n---@field cmp fun(a:snacks.picker.KeyValue.entry, b:snacks.picker.KeyValue.entry): boolean\nlocal M = {}\nM.__index = M\nlocal uv = vim.uv or vim.loop\n\n---@alias snacks.picker.KeyValue.entry {key:string, value:number}\n\n---@param path string\n---@param opts? {max_size?: number, cmp?: fun(a:snacks.picker.KeyValue.entry, b:snacks.picker.KeyValue.entry): boolean}\nfunction M.new(path, opts)\n  local self = setmetatable({}, M)\n  self.data = {}\n  self.path = path\n  self.max_size = opts and opts.max_size or 10000\n  ---@param a snacks.picker.KeyValue.entry\n  ---@param b snacks.picker.KeyValue.entry\n  self.cmp = opts and opts.cmp or function(a, b)\n    return a.value > b.value\n  end\n  self.loaded_time = os.time()\n  local fd = io.open(path, \"rb\")\n  if fd then\n    ---@type string\n    local data = fd:read(\"*a\")\n    fd:close()\n    local ok, decoded = pcall(require(\"string.buffer\").decode, data)\n    self.data = ok and decoded or {} --[[@as table<string, number>]]\n  end\n  return self\nend\n\nfunction M:set(key, value)\n  self.data[key] = value\nend\n\nfunction M:get(key)\n  return self.data[key]\nend\n\nfunction M:get_all()\n  return self.data\nend\n\nfunction M:close()\n  vim.fn.mkdir(vim.fn.fnamemodify(self.path, \":h\"), \"p\")\n  local stat = uv.fs_stat(self.path)\n  -- check if the file was modified since we loaded it\n  if self.loaded_time > 0 and stat and stat.mtime.sec > self.loaded_time then\n    return\n  end\n  local entries = {} ---@type snacks.picker.KeyValue.entry[]\n  for k, v in pairs(self.data) do\n    table.insert(entries, { key = k, value = v })\n  end\n  table.sort(entries, self.cmp)\n\n  self.data = {}\n  for i = 1, math.min(#entries, self.max_size) do\n    local entry = entries[i]\n    self.data[entry.key] = entry.value\n  end\n  local data = require(\"string.buffer\").encode(self.data)\n  local fd = io.open(self.path, \"w+b\")\n  if not fd then\n    return\n  end\n  fd:write(data)\n  fd:close()\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/markdown.lua",
    "content": "local M = {}\n\nlocal ns = vim.api.nvim_create_namespace(\"snacks.picker.util.markdown\")\nlocal did_setup = false\n\n---@private\nlocal function setup()\n  if did_setup then\n    return\n  end\n  did_setup = true\n\n  -- trigger plugin loading if available\n  pcall(require, \"render-markdown\")\n  pcall(require, \"markview\")\nend\n\n---@param buf number\n---@param opts? {images: boolean, bullets?: boolean}\nfunction M.render(buf, opts)\n  setup()\n  opts = opts or {}\n\n  local ft = vim.bo[buf].filetype\n  if not ft:find(\"^markdown\") then\n    ft = ft:gsub(\"%.?markdown%.?\", \"\")\n    -- set filetype to markdown but preserve existing ft as a suffix\n    -- use eventignore to avoid triggering autocmds\n    local ei = vim.o.eventignore\n    vim.o.eventignore = \"all\"\n    vim.bo[buf].filetype = table.concat({ \"markdown\", ft ~= \"\" and ft or \"\" }, \".\")\n    vim.o.eventignore = ei\n  end\n\n  if not pcall(vim.treesitter.start, buf, \"markdown\") then\n    vim.bo[buf].syntax = \"markdown\"\n  end\n\n  if opts.images ~= false then\n    vim.b[buf].snacks_image_conceal = true\n    Snacks.image.doc.attach(buf)\n  end\n\n  if package.loaded[\"render-markdown\"] then\n    require(\"render-markdown\").render({\n      buf = buf,\n      event = \"Snacks\",\n      config = {\n        render_modes = true,\n        bullet = { enabled = opts.bullets ~= false },\n      },\n    })\n  elseif package.loaded[\"markview\"] then\n    local render = require(\"markview\").strict_render\n    render:render(buf, nil, {\n      markdown = {\n        list_items = {\n          enable = opts.bullets ~= false,\n        },\n      },\n    })\n  else\n    M.render_fallback(buf)\n  end\nend\n\n-- Fallback simple highlighting for headings and horizontal rules\n---@param buf number\nfunction M.render_fallback(buf)\n  vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)\n  local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)\n  for l, line in ipairs(lines) do\n    local _, level = line:find(\"^#+()\")\n    if level then\n      vim.api.nvim_buf_set_extmark(buf, ns, l - 1, 0, {\n        line_hl_group = \"@markup.heading.\" .. tostring(level) .. \".markdown\",\n      })\n    elseif line:find(\"^%-%-%-+%s*$\") then\n      vim.api.nvim_buf_set_extmark(buf, ns, l - 1, 0, {\n        virt_text_win_col = 0,\n        virt_text = { { string.rep(\"-\", vim.go.columns), \"SnacksPickerRule\" } },\n        priority = 100,\n      })\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/minheap.lua",
    "content": "---@class snacks.picker.MinHeap\n---@field data any[]          -- the heap array\n---@field cmp fun(a:any, b:any):boolean  -- determines \"priority\"; if cmp(a,b) == true, a is considered 'larger' for top-k\n---@field capacity number\n---@field sorted? snacks.picker.Item[]\nlocal M = {}\nM.__index = M\n\n---@class snacks.picker.minheap.Config\n---@field cmp? fun(a, b):boolean\n---@field capacity number\n\n---@param opts? snacks.picker.minheap.Config\nfunction M.new(opts)\n  opts = opts or {}\n  local self = setmetatable({}, M)\n\n  -- Default comparator means: a > b => a is 'better' (we want the top by value)\n  -- So if we want the top K largest items, the heap is min-heap based on that comparator\n  self.cmp = opts.cmp or function(a, b)\n    return a > b\n  end\n  self.capacity = assert(opts.capacity, \"capacity is required\")\n  assert(self.capacity > 0, \"capacity must be greater than 0\")\n  self.data = {}\n  return self\nend\n\nfunction M:clear()\n  self.data = {}\n  self.sorted = nil\nend\n\n-- Private: swap two indices\nfunction M:_swap(i, j)\n  self.data[i], self.data[j] = self.data[j], self.data[i]\nend\n\n-- Private: heapify up (bubble up)\nfunction M:_heapify_up(idx)\n  while idx > 1 do\n    local parent = math.floor(idx / 2)\n    -- If child is 'less' than parent under the min-heap logic, swap\n    -- Because self.cmp(child, parent) == true => child is 'bigger' => for min-heap we want bigger below\n    -- So we invert self.cmp because we want to keep the smallest at top:\n    if self.cmp(self.data[parent], self.data[idx]) then\n      self:_swap(parent, idx)\n      idx = parent\n    else\n      break\n    end\n  end\nend\n\n-- Private: heapify down\nfunction M:_heapify_down(idx)\n  local size = #self.data\n  while true do\n    local left = 2 * idx\n    local right = left + 1\n    local smallest = idx\n\n    if left <= size and self.cmp(self.data[smallest], self.data[left]) then\n      smallest = left\n    end\n    if right <= size and self.cmp(self.data[smallest], self.data[right]) then\n      smallest = right\n    end\n    if smallest ~= idx then\n      self:_swap(idx, smallest)\n      idx = smallest\n    else\n      break\n    end\n  end\nend\n\n--- Insert value into the min-heap of capacity K.\n--- If the heap is not full, just insert.\n--- If it's full and the value is 'larger' than the min (root), replace the root & heapify.\n---@generic T\n---@param value T\n---@return boolean added, T? evicted\nfunction M:add(value)\n  local size = #self.data\n  if size < self.capacity then\n    -- Just insert at the end, heapify up\n    table.insert(self.data, value)\n    self:_heapify_up(#self.data)\n    self.sorted = nil\n    return true\n  else\n    -- If new value is larger than the root (which is the smallest in the min-heap),\n    -- then pop root & insert new value\n    if self.cmp(value, self.data[1]) then\n      local evicted = self.data[1]\n      self.data[1] = value\n      self:_heapify_down(1)\n      self.sorted = nil\n      return true, evicted\n    end\n  end\n  return false\nend\n\nfunction M:count()\n  return #self.data\nend\n\n---@return any|nil\nfunction M:min()\n  return self.data[1]\nend\n\n---@return any|nil\nfunction M:max()\n  -- might need to scan if you want the max element in a min-heap\n  local size = #self.data\n  if size == 0 then\n    return nil\n  end\n  local maximum = self.data[1]\n  for i = 2, size do\n    if self.cmp(self.data[i], maximum) then\n      maximum = self.data[i]\n    end\n  end\n  return maximum\nend\n\n---@param idx number\n---@return snacks.picker.Item?\n---@overload fun(self: snacks.picker.MinHeap): snacks.picker.Item[]\nfunction M:get(idx)\n  if not self.sorted then\n    self.sorted = {}\n    for i = 1, #self.data do\n      table.insert(self.sorted, self.data[i])\n    end\n    table.sort(self.sorted, self.cmp)\n  end\n  if idx then\n    return self.sorted[idx]\n  end\n  return self.sorted\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/queue.lua",
    "content": "--- Efficient queue implementation.\n--- Prevents need to shift elements when popping.\n---@class snacks.picker.queue\n---@field queue any[]\n---@field first number\n---@field last number\nlocal M = {}\nM.__index = M\n\nfunction M.new()\n  local self = setmetatable({}, M)\n  self:clear()\n  return self\nend\n\nfunction M:push(value)\n  self.last = self.last + 1\n  self.queue[self.last] = value\nend\n\nfunction M:size()\n  return self.last - self.first + 1\nend\n\nfunction M:empty()\n  return self:size() == 0\nend\n\nfunction M:clear()\n  self.first, self.last, self.queue = 0, -1, {}\nend\n\nfunction M:pop()\n  if self:empty() then\n    return\n  end\n  local value = self.queue[self.first]\n  self.queue[self.first] = nil\n  self.first = self.first + 1\n  return value\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/picker/util/spinner.lua",
    "content": "---@class snacks.util.spinner.Opts\n---@field extmark? fun(spinner:string): vim.api.keyset.set_extmark\n\n---@class snacks.util.Spinner\n---@field buf number\n---@field opts snacks.util.spinner.Opts\n---@field timer? uv.uv_timer_t\n---@field extmark_id? number\nlocal M = {}\nM.__index = M\n\nlocal ns = vim.api.nvim_create_namespace(\"snacks.picker.util.spinner\")\n\n---@param opts? snacks.util.spinner.Opts\n---@param buf number\nfunction M.new(buf, opts)\n  local self = setmetatable({}, M)\n  self.buf = buf\n  self.opts = opts or {}\n  self:start()\n  return self\nend\n\nfunction M:start()\n  if self:running() then\n    return\n  end\n  self:stop()\n  if not self:buf_valid() then\n    return\n  end\n  self.timer = assert(vim.uv.new_timer())\n  self.timer:start(0, 60, function()\n    vim.schedule(function()\n      self:step()\n    end)\n  end)\nend\n\nfunction M:buf_valid()\n  return self.buf and vim.api.nvim_buf_is_valid(self.buf)\nend\n\nfunction M:step()\n  if not self:running() then\n    return\n  end\n  if not self:buf_valid() then\n    return self:stop()\n  end\n  local lines = vim.api.nvim_buf_get_lines(self.buf, 0, -1, false)\n  local row = math.max(#lines - 1, 0)\n  while row > 0 and lines[row + 1]:match(\"^%s*$\") do\n    row = row - 1\n  end\n\n  local spinner = Snacks.util.spinner()\n\n  ---@type vim.api.keyset.set_extmark\n  local extmark = {}\n  if type(self.opts.extmark) == \"function\" then\n    extmark = self.opts.extmark(spinner)\n  else\n    if row > 0 then\n      extmark.virt_lines = { { { spinner, \"SnacksPickerSpinner\" } } }\n    else\n      extmark.virt_text = { { spinner, \"SnacksPickerSpinner\" } }\n    end\n  end\n  extmark.id = self.extmark_id\n  extmark.priority = 1000\n  self.extmark_id = vim.api.nvim_buf_set_extmark(self.buf, ns, row, 0, extmark)\nend\n\nfunction M:running()\n  return self.timer and not self.timer:is_closing()\nend\n\nfunction M:stop()\n  if self.timer and not self.timer:is_closing() then\n    self.timer:stop()\n    self.timer:close()\n    self.timer = nil\n  end\n  if self:buf_valid() then\n    vim.api.nvim_buf_clear_namespace(self.buf, ns, 0, -1)\n  end\nend\n\n---@param msg? string\n---@param opts? snacks.win.Config\nfunction M.loading(msg, opts)\n  opts = opts or {}\n  local parent_win = opts.win or vim.api.nvim_get_current_win()\n  msg = msg or \"Loading...\"\n  msg = \"   \" .. msg\n  opts = Snacks.win.resolve({\n    backdrop = false,\n    win = vim.api.nvim_get_current_win(),\n    focusable = false,\n    enter = false,\n    relative = \"win\",\n    zindex = (vim.api.nvim_win_get_config(parent_win).zindex or 50) + 1,\n    width = vim.api.nvim_strwidth(msg) + 1,\n    height = 1,\n    border = \"rounded\",\n    text = msg,\n  }, opts)\n  local win = Snacks.win(opts)\n  local spinner ---@type snacks.util.Spinner\n  win:on(\"WinClosed\", function(_, ev)\n    if ev.match == tostring(parent_win) then\n      win:close()\n      spinner:stop()\n    end\n  end)\n  spinner = M.new(win.buf, {\n    extmark = function(text)\n      return {\n        virt_text = { { text, \"SnacksPickerSpinner\" } },\n        virt_text_pos = \"overlay\",\n        virt_text_win_col = 1,\n      }\n    end,\n  })\n  local stop = spinner.stop\n  spinner.stop = function()\n    stop(spinner)\n    win:close()\n  end\n  return spinner\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/profiler/core.lua",
    "content": "---@class snacks.profiler.core\nlocal M = {}\n\nlocal hrtime = (vim.uv or vim.loop).hrtime\nlocal nvim_create_autocmd = vim.api.nvim_create_autocmd\n\nM._require = _G.require\nM.attached = {} ---@type table<unknown, boolean>\nM.events = {} ---@type snacks.profiler.Event[]\nM.filter_fn = error ---@type fun(str:string):boolean\nM.filter_mod = error ---@type fun(str:string):boolean\nM.id = 0\nM.me = debug.getinfo(1, \"S\").source:sub(2)\nM.pids = {} ---@type table<string, number>\nM.running = false\nM.skips = { -- these modules are always be skipped\n  [\"_G\"] = true,\n  [\"bit\"] = true,\n  [\"coroutine\"] = true,\n  [\"debug\"] = true,\n  [\"ffi\"] = true,\n  [\"io\"] = true,\n  [\"jit\"] = true,\n  [\"jit.opt\"] = true,\n  [\"jit.profile\"] = true,\n  [\"lpeg\"] = true,\n  [\"luv\"] = true,\n  [\"math\"] = true,\n  [\"mpack\"] = true,\n  [\"os\"] = true,\n  [\"package\"] = true,\n  [\"snacks.debug\"] = true,\n  [\"snacks.profiler\"] = true,\n  [\"snacks.profiler.core\"] = true,\n  [\"snacks.profiler.loc\"] = true,\n  [\"snacks.profiler.picker\"] = true,\n  [\"snacks.profiler.tracer\"] = true,\n  [\"snacks.profiler.ui\"] = true,\n  [\"string\"] = true,\n  [\"table\"] = true,\n}\n\nfunction M.skip(it)\n  M.attached[it] = true\nend\n\n---@param spec table<string, boolean>\n---@return fun(str:string):boolean\nfunction M.filter(spec)\n  local filters = {} ---@type {pattern:string, want:boolean, exact:boolean}[]\n  local default = spec.default\n  default = default == nil and true or default\n  for pattern, want in pairs(spec) do\n    if pattern ~= \"default\" then\n      table.insert(filters, { pattern = pattern, want = want, exact = pattern:sub(1, 1) ~= \"^\" })\n    end\n  end\n  -- sort by longest pattern first\n  table.sort(filters, function(a, b)\n    return #a.pattern > #b.pattern\n  end)\n  return function(str)\n    for _, filter in ipairs(filters) do\n      if filter.exact then\n        if str == filter.pattern then\n          return filter.want\n        end\n      elseif str:find(filter.pattern) then\n        return filter.want\n      end\n    end\n    return default\n  end\nend\n\n---@param opts snacks.profiler.Trace.opts\n---@param caller? snacks.profiler.Loc\n---@return ...\nfunction M.trace(opts, caller, ...)\n  local start = hrtime()\n  local thread = tostring(coroutine.running() or \"main\")\n  local pid = M.pids[thread] or 0\n  M.id = M.id + 1\n  M.pids[thread] = M.id\n  ---@type snacks.profiler.Event\n  local entry = { id = M.id, start = start, pid = pid, ref = caller, opts = opts }\n  M.events[#M.events + 1] = entry\n  local ret = { pcall(opts.fn, ...) }\n  M.pids[thread] = pid\n  entry.stop = hrtime()\n  if not ret[1] then\n    error(ret[2])\n  end\n  return select(2, unpack(ret))\nend\n\n---@param depth? number\n---@param max_depth? number\n---@return snacks.profiler.Loc?\nfunction M.caller(depth, max_depth)\n  for i = depth or 3, max_depth or 10 do\n    local info = debug.getinfo(i, \"Sl\")\n    if not info then\n      return\n    end\n    local source = info.source:sub(2)\n    if info.what ~= \"C\" and source ~= M.me then\n      return { file = source, line = info.currentline }\n    end\n  end\nend\n\n---@param opts snacks.profiler.Trace.opts\nfunction M.attach_fn(opts)\n  if M.attached[opts.fn] then\n    return opts.fn\n  end\n  M.attached[opts.fn] = true\n  local ret = function(...)\n    if not M.running then\n      return opts.fn(...)\n    end\n    return M.trace(opts, M.caller() or nil, ...)\n  end\n  M.attached[ret] = true\n  return ret\nend\n\n---@param modname string\n---@param mod table<string, function>\n---@param opts? {force?:boolean}\nfunction M.attach_mod(modname, mod, opts)\n  if type(mod) ~= \"table\" or M.attached[mod] then\n    return\n  end\n  opts = opts or {}\n  if (M.skips[modname] or not M.filter_mod(modname)) and opts.force ~= true then\n    return\n  end\n  M.attached[mod] = true\n  for k, v in pairs(mod) do\n    if type(k) == \"string\" and type(v) == \"function\" and not M.attached[v] then\n      local name = modname .. \".\" .. k\n      if M.filter_fn(name) then\n        mod[k] = M.attach_fn({ modname = modname, fname = k, name = name, fn = v })\n      end\n    end\n  end\nend\n\nfunction M.require(modname)\n  if not M.running or package.loaded[modname] or M.skips[modname] then\n    return M._require(modname)\n  end\n  local ret = {\n    M.trace({\n      fname = \"require\",\n      name = \"require:\" .. modname,\n      require = modname,\n      fn = M._require,\n    }, M.caller(), modname),\n  }\n  if type(ret[1]) == \"table\" then\n    M.attach_mod(modname, ret[1])\n  end\n  return unpack(ret)\nend\n\n---@param event any (string|array) Event(s) that will trigger the handler (`callback` or `command`).\n---@param opts vim.api.keyset.create_autocmd Options dict:\nfunction M.autocmd(event, opts)\n  if opts and type(opts.callback) == \"function\" then\n    local name = { type(event) == \"string\" and event or table.concat(event, \"|\") }\n    if opts.pattern then\n      name[#name + 1] = type(opts.pattern) == \"string\" and opts.pattern or table.concat(opts.pattern, \"|\")\n    end\n    local autocmd = table.concat(name, \":\")\n    local trace = { name = \"autocmd:\" .. autocmd, fn = opts.callback, autocmd = autocmd }\n    opts.callback = function(...)\n      if not M.running then\n        return trace.fn(...)\n      end\n      return M.trace(trace, M.caller(), ...)\n    end\n  end\n  return nvim_create_autocmd(event, opts)\nend\n\n---@param opts snacks.profiler.Config\nfunction M.start(opts)\n  assert(not M.running, \"Profiler is already enabled\")\n\n  -- Clear events\n  M.events = {}\n\n  -- Setup filters and include globals\n  local filter_mod = vim.deepcopy(opts.filter_mod)\n  for _, global in ipairs(opts.globals) do\n    filter_mod[global] = true\n  end\n  M.filter_mod = M.filter(filter_mod)\n  M.filter_fn = M.filter(opts.filter_fn)\n\n  -- Attach to require\n  _G.require = M.require\n\n  -- Attach to autocmds\n  if opts.autocmds then\n    vim.api.nvim_create_autocmd = M.autocmd\n  end\n\n  -- Attach to globals\n  for _, name in ipairs(opts.globals) do\n    M.attach_mod(name, vim.tbl_get(_G, unpack(vim.split(name, \".\", { plain = true }))))\n  end\n\n  -- Attach to loaded modules\n  ---@diagnostic disable-next-line: no-unknown\n  for modname, mod in pairs(package.loaded) do\n    M.attach_mod(modname, mod)\n  end\n\n  -- Enable the profiler\n  M.running = true\n  vim.api.nvim_exec_autocmds(\"User\", { pattern = \"SnacksProfilerStarted\", modeline = false })\nend\n\nfunction M.stop()\n  assert(M.running, \"Profiler is not enabled\")\n  _G.require = M._require\n  vim.api.nvim_create_autocmd = nvim_create_autocmd\n  M.running = false\n  vim.api.nvim_exec_autocmds(\"User\", { pattern = \"SnacksProfilerStopped\", modeline = false })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/profiler/init.lua",
    "content": "require(\"snacks\")\n\n-- ### Traces\n--\n---@class snacks.profiler.Trace\n---@field name string fully qualified name of the function\n---@field time number time in nanoseconds\n---@field depth number stack depth\n---@field [number] snacks.profiler.Trace child traces\n---@field fname string function name\n---@field fn function function reference\n---@field modname? string module name\n---@field require? string special case for require\n---@field autocmd? string special case for autocmd\n---@field count? number number of calls\n---@field def? snacks.profiler.Loc location of the definition\n---@field ref? snacks.profiler.Loc location of the reference (caller)\n---@field loc? snacks.profiler.Loc normalized location\n\n---@class snacks.profiler.Loc\n---@field file string path to the file\n---@field line number line number\n---@field loc? string normalized location\n---@field modname? string module name\n---@field plugin? string plugin name\n\n-- ### Pick: grouping, filtering and sorting\n--\n---@class snacks.profiler.Find\n---@field structure? boolean show traces as a tree or flat list\n---@field sort? \"time\"|\"count\"|false sort by time or count, or keep original order\n---@field loc? \"def\"|\"ref\" what location to show in the preview\n---@field group? boolean|snacks.profiler.Field group traces by field\n---@field filter? snacks.profiler.Filter filter traces by field(s)\n---@field min_time? number only show grouped traces with `time >= min_time`\n\n---@class snacks.profiler.Pick: snacks.profiler.Find\n---@field picker? snacks.profiler.Picker\n\n---@alias snacks.profiler.Picker \"snacks\"|\"trouble\"\n---@alias snacks.profiler.Pick.spec snacks.profiler.Pick|{preset?:string}|fun():snacks.profiler.Pick\n\n---@alias snacks.profiler.Field\n---| \"name\" fully qualified name of the function\n---| \"def\" definition\n---| \"ref\" reference (caller)\n---| \"require\" require\n---| \"autocmd\" autocmd\n---| \"modname\" module name of the called function\n---| \"def_file\" file of the definition\n---| \"def_modname\" module name of the definition\n---| \"def_plugin\" plugin that defines the function\n---| \"ref_file\" file of the reference\n---| \"ref_modname\" module name of the reference\n---| \"ref_plugin\" plugin that references the function\n\n---@class snacks.profiler.Filter\n---@field name? string|boolean fully qualified name of the function\n---@field def? string|boolean location of the definition\n---@field ref? string|boolean location of the reference (caller)\n---@field require? string|boolean special case for require\n---@field autocmd? string|boolean special case for autocmd\n---@field modname? string|boolean module name\n---@field def_file? string|boolean file of the definition\n---@field def_modname? string|boolean module name of the definition\n---@field def_plugin? string|boolean plugin that defines the function\n---@field ref_file? string|boolean file of the reference\n---@field ref_modname? string|boolean module name of the reference\n---@field ref_plugin? string|boolean plugin that references the function\n\n-- ### UI\n--\n---@alias snacks.profiler.Badge {icon:string, text:string, padding?:boolean, level?:string}\n---@alias snacks.profiler.Badge.type \"time\"|\"pct\"|\"count\"|\"name\"|\"trace\"\n\n---@class snacks.profiler.Highlights\n---@field min_time? number only highlight entries with time >= min_time\n---@field max_shade? number -- time in ms for the darkest shade\n---@field badges? snacks.profiler.Badge.type[] badges to show\n---@field align? \"right\"|\"left\"|number align the badges right, left or at a specific column\n\n-- ### Other\n--\n---@class snacks.profiler.Startup\n---@field event? string\n---@field pattern? string|string[] pattern to match for the autocmd\n\n---@alias snacks.profiler.GroupFn fun(entry:snacks.profiler.Trace):{key:string, name?:string}?\n\n---@class snacks.profiler\n---@field core snacks.profiler.core\n---@field loc snacks.profiler.loc\n---@field tracer snacks.profiler.tracer\n---@field ui snacks.profiler.ui\n---@field picker snacks.profiler.picker\nlocal M = {}\n\nM.meta = {\n  desc = \"Neovim lua profiler\",\n}\n\nlocal mods = { core = true, loc = true, tracer = true, ui = true, picker = true }\nsetmetatable(M, {\n  __index = function(t, k)\n    if mods[k] then\n      ---@diagnostic disable-next-line: no-unknown\n      t[k] = require(\"snacks.profiler.\" .. k)\n    end\n    return rawget(t, k)\n  end,\n})\n\n---@class snacks.profiler.Config\nlocal defaults = {\n  autocmds = true,\n  runtime = vim.env.VIMRUNTIME, ---@type string\n  -- thresholds for buttons to be shown as info, warn or error\n  -- value is a tuple of [warn, error]\n  thresholds = {\n    time = { 2, 10 },\n    pct = { 10, 20 },\n    count = { 10, 100 },\n  },\n  on_stop = {\n    highlights = true, -- highlight entries after stopping the profiler\n    pick = true, -- show a picker after stopping the profiler (uses the `on_stop` preset)\n  },\n  ---@type snacks.profiler.Highlights\n  highlights = {\n    min_time = 0, -- only highlight entries with time > min_time (in ms)\n    max_shade = 20, -- time in ms for the darkest shade\n    badges = { \"time\", \"pct\", \"count\", \"trace\" },\n    align = 80,\n  },\n  pick = {\n    picker = \"snacks\", ---@type snacks.profiler.Picker\n    ---@type snacks.profiler.Badge.type[]\n    badges = { \"time\", \"count\", \"name\" },\n    ---@type snacks.profiler.Highlights\n    preview = {\n      badges = { \"time\", \"pct\", \"count\" },\n      align = \"right\",\n    },\n  },\n  startup = {\n    event = \"VimEnter\", -- stop profiler on this event. Defaults to `VimEnter`\n    after = true, -- stop the profiler **after** the event. When false it stops **at** the event\n    pattern = nil, -- pattern to match for the autocmd\n    pick = true, -- show a picker after starting the profiler (uses the `startup` preset)\n  },\n  ---@type table<string, snacks.profiler.Pick|fun():snacks.profiler.Pick?>\n  presets = {\n    startup = { min_time = 1, sort = false },\n    on_stop = {},\n    filter_by_plugin = function()\n      return { filter = { def_plugin = vim.fn.input(\"Filter by plugin: \") } }\n    end,\n  },\n  ---@type string[]\n  globals = {\n    -- \"vim\",\n    -- \"vim.api\",\n    -- \"vim.keymap\",\n    -- \"Snacks.dashboard.Dashboard\",\n  },\n  -- filter modules by pattern.\n  -- longest patterns are matched first\n  filter_mod = {\n    default = true, -- default value for unmatched patterns\n    [\"^vim%.\"] = false,\n    [\"mason-core.functional\"] = false,\n    [\"mason-core.functional.data\"] = false,\n    [\"mason-core.optional\"] = false,\n    [\"which-key.state\"] = false,\n  },\n  filter_fn = {\n    default = true,\n    [\"^.*%._[^%.]*$\"] = false,\n    [\"trouble.filter.is\"] = false,\n    [\"trouble.item.__index\"] = false,\n    [\"which-key.node.__index\"] = false,\n    [\"smear_cursor.draw.wo\"] = false,\n    [\"^ibl%.utils%.\"] = false,\n  },\n  -- stylua: ignore\n  icons = {\n    time    = \" \",\n    pct     = \" \",\n    count   = \" \",\n    require = \"󰋺 \",\n    modname = \"󰆼 \",\n    plugin  = \" \",\n    autocmd = \"⚡\",\n    file    = \" \",\n    fn      = \"󰊕 \",\n    status  = \"󰈸 \",\n  },\n  debug = false,\n}\n\nM.config = Snacks.config.get(\"profiler\", defaults)\n\nlocal attached_debug = false\nlocal loaded = false\n\n-- Toggle the profiler\nfunction M.toggle()\n  if M.core.running then\n    M.stop()\n  else\n    M.start()\n  end\n  return M.core.running\nend\n\n-- Statusline component\nfunction M.status()\n  return {\n    function()\n      return (\"%s %d events\"):format(M.config.icons.status, #M.core.events)\n    end,\n    color = \"DiagnosticError\",\n    cond = function()\n      return M.core.running\n    end,\n  }\nend\n\n-- Start the profiler\n---@param opts? snacks.profiler.Config\nfunction M.start(opts)\n  if M.core.running then\n    return Snacks.notify.warn(\"Profiler is already enabled\")\n  end\n  M.config = Snacks.config.get(\"profiler\", defaults, opts)\n\n  M.highlight(false)\n  M.core.start(M.config)\nend\n\nlocal function load()\n  if loaded then\n    return\n  end\n  loaded = true\n  M.tracer.load() -- load traces\n  M.loc.load() -- add and normalize locations\n  M.ui.load() -- load highlights\n  vim.api.nvim_exec_autocmds(\"User\", { pattern = \"SnacksProfilerLoaded\", modeline = false })\nend\n\n-- Stop the profiler\n---@param opts? {highlights?:boolean, pick?:snacks.profiler.Pick.spec}\nfunction M.stop(opts)\n  if not M.core.running then\n    return Snacks.notify.warn(\"Profiler is not enabled\")\n  end\n  M.core.stop()\n  opts = vim.tbl_extend(\"force\", {}, M.config.on_stop, opts or {})\n  if opts.pick == true then\n    opts.pick = M.config.presets.on_stop or {}\n  elseif opts.pick == false then\n    opts.pick = nil\n  end\n  loaded = false\n  vim.schedule(function()\n    load()\n    if opts.highlights then\n      M.highlight(true)\n    end\n    if opts.pick then\n      M.pick(opts.pick)\n    end\n  end)\nend\n\n-- Check if the profiler is running\nfunction M.running()\n  return M.core.running\nend\n\n-- Profile the profiler\n---@private\nfunction M.debug()\n  if not M.core.running then\n    return Snacks.notify.warn(\"Profiler is not enabled\")\n  end\n  if loaded then\n    return Snacks.notify.warn(\"Profiler is already loaded\")\n  end\n  if not attached_debug then\n    attached_debug = true\n    M.core.skip(M.core.caller)\n    M.core.skip(M.core.trace)\n    M.core.skip(M.loc.loc)\n    M.core.skip(M.loc.norm)\n    M.core.skip(M.loc.realpath)\n    M.core.attach_mod(\"vim.fs\", vim.fs, { force = true })\n    M.core.attach_mod(\"snacks.profiler\", M, { force = true })\n    for mod in pairs(mods) do\n      M.core.attach_mod(\"snacks.profiler.\" .. mod, M[mod], { force = true })\n    end\n  end\n  local event_count = #M.core.events\n  local me = M.core.me\n  M.core.me = \"__ignore__\"\n  load()\n  M.pick({ picker = \"foo\", group = \"name\", structure = true })\n  M.core.events = vim.list_slice(M.core.events, event_count)\n  loaded = false\n  M.stop()\n  M.core.me = me\nend\n\n-- Group and filter traces\n---@param opts snacks.profiler.Find\nfunction M.find(opts)\n  load()\n  return M.tracer.find(opts)\nend\n\n-- Group and filter traces and open a picker\n---@param opts? snacks.profiler.Pick.spec\nfunction M.pick(opts)\n  load()\n  if type(opts) == \"function\" then\n    opts = opts()\n    if not opts then\n      return\n    end\n  end\n  opts = opts or {}\n  if opts.preset then\n    local preset = M.config.presets[opts.preset]\n    preset = type(preset) == \"function\" and preset()\n    if not preset then\n      return\n    end\n    opts = vim.tbl_deep_extend(\"force\", {}, preset, opts)\n  end\n  ---@cast opts snacks.profiler.Pick\n  return M.picker.open(opts)\nend\n\n--- Open a scratch buffer with the profiler picker options\nfunction M.scratch()\n  return Snacks.scratch({\n    ft = \"lua\",\n    icon = \" \",\n    name = \"Profiler Picker Options\",\n    template = (\"---@module 'snacks'\\n\\nSnacks.profiler.pick(%s)\"):format(vim.inspect({\n      structure = true,\n      group = \"name\",\n      sort = \"time\",\n      min_time = 1,\n    })),\n  })\nend\n\n-- Start the profiler on startup, and stop it after the event has been triggered.\n---@param opts snacks.profiler.Config\nfunction M.startup(opts)\n  M.config = Snacks.config.get(\"profiler\", defaults, opts)\n  local event, pattern = M.config.startup.event or \"VimEnter\", M.config.startup.pattern\n  if event == \"VeryLazy\" then\n    event, pattern = \"User\", event\n  end\n  local cb = function()\n    local pick = M.config.startup.pick and M.config.presets.startup\n    Snacks.profiler.stop({ pick = pick })\n  end\n  if M.config.startup.after then\n    cb = vim.schedule_wrap(cb)\n  end\n  vim.api.nvim_create_autocmd(event, { pattern = pattern, once = true, callback = cb })\n  M.start(opts)\nend\n\n-- Toggle the profiler highlights\n---@param enable? boolean\nfunction M.highlight(enable)\n  if enable == nil then\n    enable = not M.ui.enabled\n  end\n  if enable == M.ui.enabled then\n    return\n  end\n  if enable then\n    load()\n    M.ui.show()\n  else\n    M.ui.hide()\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/profiler/loc.lua",
    "content": "---@class snacks.profiler.loc\n---@field vim_runtime string\n---@field user_runtime string\n---@field user_config string\nlocal M = {}\n\nlocal fun_cache = {} ---@type table<function, snacks.profiler.Loc|false>\nlocal norm_cache = {} ---@type table<string, table<number,snacks.profiler.Loc>>\nlocal path_cache = {} ---@type table<string, string>\nlocal ts_cache = {} ---@type table<string, table<string, snacks.profiler.Loc>>\nlocal ts_query ---@type vim.treesitter.Query?\n\n-- add and normalize locations\nfunction M.load()\n  local opts = Snacks.profiler.config\n  M.vim_runtime = M.realpath(vim.env.VIMRUNTIME)\n  M.user_runtime = M.realpath(opts.runtime or M.vim_runtime)\n  M.user_config = M.realpath(vim.fn.stdpath(\"config\") .. \"\")\n\n  Snacks.profiler.tracer.walk(function(entry)\n    entry.def = M.loc(entry)\n    entry.ref = entry.ref and M.norm(entry.ref) or nil\n  end)\nend\n\n-- Get the location at the cursor\nfunction M.current()\n  local cursor = vim.api.nvim_win_get_cursor(0)\n  return M.norm({ file = vim.api.nvim_buf_get_name(0), line = cursor[1] })\nend\n\n--- Get the real path of a file\n---@param path string\nfunction M.realpath(path)\n  if path_cache[path] then\n    return path_cache[path]\n  end\n  path = svim.fs.normalize(path, { expand_env = false })\n  path_cache[path] = svim.fs.normalize(vim.uv.fs_realpath(path) or path, { expand_env = false, _fast = true })\n  return path_cache[path]\nend\n\n---@param loc snacks.profiler.Loc\nfunction M.norm(loc)\n  local file, line = loc.file, loc.line\n  local ret = norm_cache[file] and norm_cache[file][line]\n  if not ret then\n    ret = M._norm(loc)\n    norm_cache[file] = norm_cache[file] or {}\n    norm_cache[file][line] = ret\n  end\n  return ret\nend\n\n---@param loc snacks.profiler.Loc\nfunction M._norm(loc)\n  if loc.file:sub(1, 4) == \"vim/\" then\n    loc.file = M.user_runtime .. \"/lua/\" .. loc.file\n  elseif loc.file:find(\"runtime\", 1, true) then\n    if loc.file:sub(1, #M.vim_runtime) == M.vim_runtime then\n      loc.file = M.user_runtime .. \"/\" .. loc.file:sub(#M.vim_runtime + 2)\n    end\n  end\n  loc.file = M.realpath(loc.file)\n  loc.line = loc.line == 0 and 1 or loc.line\n  loc.loc = (\"%s:%d\"):format(loc.file, loc.line)\n  if loc.file:find(M.user_config, 1, true) == 1 then\n    local relpath = loc.file:sub(#M.user_config + 2)\n    local modpath = relpath:match(\"^lua/(.*)%.lua$\")\n    loc.modname = modpath and modpath:gsub(\"/\", \".\"):gsub(\"%.init$\", \"\") or \"vimrc\"\n    loc.plugin = \"user\"\n  else\n    local plugin, modpath = loc.file:match(\"/([^/]+)/lua/(.*)%.lua$\")\n    if plugin and modpath then\n      plugin = plugin == \"runtime\" and \"nvim\" or plugin\n      loc.plugin = plugin\n      loc.modname = modpath:gsub(\"/\", \".\"):gsub(\"%.init$\", \"\")\n    end\n  end\n  return loc\nend\n\n---@param entry snacks.profiler.Trace\n---@return snacks.profiler.Loc?\nfunction M.loc(entry)\n  local ret = fun_cache[entry.fn]\n  if ret == nil then\n    local info = debug.getinfo(entry.fn, \"S\")\n    if info and info.what ~= \"C\" then\n      ret = { file = info.source:sub(2), line = info.linedefined }\n      if entry.fname and ret.file:sub(1, 4) == \"vim/\" then\n        ret.file = M.user_runtime .. \"/lua/\" .. ret.file\n        local ts_loc = M.ts_locs(ret.file)[entry.fname]\n        if ts_loc then\n          ret.file, ret.line = ts_loc.file, ts_loc.line\n        end\n      end\n      ret = M.norm(ret)\n    end\n    fun_cache[entry.fn] = ret or false\n  end\n  return ret or nil\nend\n\n---@param file string\nfunction M.ts_locs(file)\n  if ts_cache[file] then\n    return ts_cache[file]\n  end\n  ts_query = ts_query\n    or vim.treesitter.query.parse(\n      \"lua\",\n      [[((function_declaration name: (_) @fun_name) @fun\n          (#has-parent? @fun chunk))\n        ((return_statement (expression_list (identifier) @ret_name)) @ret\n          (#has-parent? @ret chunk))]]\n    )\n  local source = table.concat(vim.fn.readfile(file), \"\\n\")\n  local parser = vim.treesitter.get_string_parser(source, \"lua\")\n  parser:parse()\n  local ret, ret_name = {}, nil ---@type table<string, snacks.profiler.Loc>, string?\n  local funs = {} ---@type table<string, number>\n  for id, node in ts_query:iter_captures(parser:trees()[1]:root(), source) do\n    local name = ts_query.captures[id]\n    if name == \"fun_name\" then\n      funs[vim.treesitter.get_node_text(node, source)] = node:start() + 1\n    elseif name == \"ret_name\" then\n      ret_name = vim.treesitter.get_node_text(node, source)\n    end\n  end\n  for fname, line in pairs(funs) do\n    fname = ret_name and fname:gsub(\"^\" .. ret_name .. \"%.\", \"\") or fname\n    ret[fname] = { file = file, line = line }\n  end\n  ts_cache[file] = ret\n  return ret\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/profiler/picker.lua",
    "content": "---@class snacks.profiler.picker\nlocal M = {}\n\n---@param opts? snacks.profiler.Pick\nfunction M.open(opts)\n  opts = opts or {}\n\n  local picker = opts and opts.picker or Snacks.profiler.config.pick.picker\n  -- special case for trouble, since it does its own thing\n  if picker == \"trouble\" then\n    return require(\"trouble\").open({ mode = \"profiler\", params = opts })\n  end\n\n  local traces, _, fopts = Snacks.profiler.tracer.find(opts)\n\n  return Snacks.picker({\n    title = \"Snacks Profiler\",\n    finder = function()\n      local items = {} ---@type snacks.picker.finder.Item[]\n      for _, trace in ipairs(traces) do\n        items[#items + 1] = {\n          text = trace.name,\n          file = trace.loc and trace.loc.file,\n          pos = trace.loc and { trace.loc.line, 0 },\n          item = trace,\n        }\n      end\n      return items\n    end,\n    format = function(item)\n      ---@type snacks.profiler.Trace\n      local trace = item.item\n      local ret = Snacks.profiler.ui.format(\n        Snacks.profiler.ui.badges(trace, {\n          badges = Snacks.profiler.config.pick.badges,\n          indent = fopts.group == false or fopts.structure,\n        }),\n        { widths = { 8, 4, 1 } }\n      )\n      for _, text in ipairs(ret) do\n        if text[2] == \"Normal\" or text[2] == \"SnacksProfilerBadgeTrace\" then\n          text[2] = nil\n        end\n      end\n      return ret\n    end,\n    preview = function(ctx)\n      Snacks.picker.preview.file(ctx)\n      Snacks.util.wo(ctx.win, { cursorline = true })\n      Snacks.profiler.ui.highlight(\n        ctx.buf,\n        vim.tbl_extend(\"force\", {}, Snacks.profiler.config.pick.preview, { file = ctx.item.file })\n      )\n    end,\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/profiler/tracer.lua",
    "content": "---@alias snacks.profiler.Trace.opts snacks.profiler.Trace|{id?:number, pid?:number, time?:number, depth?:number}\n---@alias snacks.profiler.Event {id:number, pid:number, start:number, stop:number, measurements:number, ref?:snacks.profiler.Loc, idx:number, opts:snacks.profiler.Trace.opts}\n---@alias snacks.profiler.Node {group:string, trace:snacks.profiler.Trace, children:table<string|number,snacks.profiler.Node>, order:(string|number)[]}\n\n---@class snacks.profiler.tracer\nlocal M = {}\n\nM.root = {} ---@type snacks.profiler.Trace[]\n\nfunction M.load()\n  M.root = {}\n  local traces = {} ---@type snacks.profiler.Trace[]\n  for _, event in ipairs(Snacks.profiler.core.events) do\n    local trace = setmetatable({}, { __index = event.opts })\n    trace.id = event.id\n    trace.pid = event.pid\n    trace.ref = event.ref\n    trace.depth = 0\n    if event.stop then\n      trace.time = event.stop - event.start\n      traces[event.id] = trace\n      if traces[event.pid] then\n        trace.depth = traces[event.pid].depth + 1\n        table.insert(traces[event.pid], trace)\n      elseif trace.time then\n        table.insert(M.root, trace)\n      end\n    end\n  end\nend\n\n---@param on_start? fun(entry:snacks.profiler.Trace):any?\n---@param on_end? fun(entry:snacks.profiler.Trace, start?:any)\nfunction M.walk(on_start, on_end)\n  ---@param entry snacks.profiler.Trace\n  local function walk(entry)\n    local start = on_start and on_start(entry)\n    for _, child in ipairs(entry) do\n      walk(child)\n    end\n    if on_end then\n      on_end(entry, start)\n    end\n  end\n  for _, child in ipairs(M.root) do\n    walk(child)\n  end\nend\n\n---@param fn snacks.profiler.GroupFn\n---@param opts? {structure?:boolean, sort?:\"time\"|\"count\"}\nfunction M.group(fn, opts)\n  opts = opts or {}\n  ---@type snacks.profiler.Node[]\n  local nodes = { { children = {}, order = {} } } -- root node\n\n  ---@param entry snacks.profiler.Trace\n  M.walk(function(entry)\n    local group = fn(entry)\n    if group then\n      local key, parent, recursive = group.key, nodes[1], false\n      for n = 2, #nodes do\n        local node = nodes[n]\n        if node.group == key then\n          recursive = true\n          break\n        end\n        parent = opts.structure and node or parent\n      end\n      local node = parent.children[key]\n      if not node then\n        local trace = vim.tbl_extend(\"force\", { time = 0, count = 0, name = key, depth = #nodes - 1 }, group)\n        node = { group = key, trace = trace, children = {}, order = {} } ---@type snacks.profiler.Node\n        ---@diagnostic disable-next-line: no-unknown\n        parent.children[key] = node\n        table.insert(parent.order, key)\n      end\n      if not recursive then\n        table.insert(nodes, node)\n        node.trace.time = node.trace.time + entry.time\n      end\n      node.trace.count = node.trace.count + 1\n      table.insert(node.trace, entry)\n      return not recursive\n    end\n  end, function(_, start)\n    if start then\n      table.remove(nodes)\n    end\n  end)\n\n  assert(#nodes == 1, \"node stack not empty\")\n  return nodes[1]\nend\n\n---@param node snacks.profiler.Node\n---@param opts? snacks.profiler.Find\nfunction M.flatten(node, opts)\n  opts = opts or {}\n  local ret = {} ---@type snacks.profiler.Trace[]\n  ---@param n snacks.profiler.Node\n  local function walk(n)\n    if n.trace and (n.trace.time / 1e6 >= (opts.min_time or 0)) then\n      table.insert(ret, n.trace)\n    end\n    if opts.sort then\n      local children = vim.tbl_values(n.children) ---@type snacks.profiler.Node[]\n      if opts.sort == \"time\" then\n        table.sort(children, function(a, b)\n          return a.trace.time > b.trace.time\n        end)\n      elseif opts.sort == \"count\" then\n        table.sort(children, function(a, b)\n          return a.trace.count > b.trace.count\n        end)\n      end\n      for _, child in ipairs(children) do\n        walk(child)\n      end\n    else\n      for _, key in ipairs(n.order) do\n        walk(n.children[key])\n      end\n    end\n  end\n  walk(node)\n  return ret\nend\n\n---@param opts snacks.profiler.Find\nfunction M.find(opts)\n  opts = opts or {}\n  opts = vim.tbl_extend(\"force\", {\n    group = \"name\",\n    structure = opts.group ~= false,\n    sort = (opts.group ~= false) and \"time\",\n  }, opts or {})\n  opts.group = opts.group == true and \"name\" or opts.group\n  opts.sort = opts.sort == true and \"time\" or opts.sort\n  ---@cast opts snacks.profiler.Find\n  local key_parts = {} ---@type table<string, string[]>\n  local id = 0\n\n  ---@param entry snacks.profiler.Trace\n  ---@param key string|false\n  local function get(entry, key)\n    if key == false then\n      id = id + 1\n      return tostring(id), entry.name\n    end\n    local parts = key_parts[key]\n    if not parts then\n      parts = vim.split(key, \"[_%.]\")\n      if #parts == 1 and (parts[1] == \"ref\" or parts[1] == \"def\") then\n        parts[2] = \"loc\"\n      end\n      key_parts[key] = parts\n    end\n    local value = vim.tbl_get(entry, unpack(parts)) ---@type string?\n    if not value then\n      return\n    end\n    local name, loc = value, entry.def\n    if parts[1] == \"ref\" or parts[1] == \"require\" then\n      loc = entry.ref\n    elseif parts[1] == \"name\" and entry.require then\n      loc = entry.ref\n    end\n    if parts[2] == \"def\" or parts[1] == \"name\" then\n      name = entry.name\n    else\n      name = parts[#parts] .. \":\" .. value\n    end\n    return value, name, loc\n  end\n\n  -- Build the filter\n  local filter = {} ---@type table<string, string|boolean>\n  local current ---@type snacks.profiler.Trace?\n  for k, v in pairs(opts.filter or {}) do\n    if v == true then\n      -- If the value is true, then we want the current location\n      if k:find(\"[rd]ef\") then\n        if not current then\n          local loc = Snacks.profiler.loc.current()\n          ---@diagnostic disable-next-line: missing-fields\n          current = { def = loc, ref = loc }\n        end\n        v = get(current, k) or false\n      else -- match all\n        v = \"^.*$\"\n      end\n    end\n    filter[k] = v\n  end\n\n  ---@param entry snacks.profiler.Trace\n  local function match(entry)\n    for key, m in pairs(filter) do\n      local value = get(entry, key) or false\n      if type(m) == \"string\" and m:sub(1, 1) == \"^\" then\n        if not (value and value:find(m)) then\n          return false\n        end\n      elseif value ~= m then\n        return false\n      end\n    end\n    return true\n  end\n\n  ---@type snacks.profiler.GroupFn\n  local group_fn = function(entry)\n    if opts.filter and not match(entry) then\n      return\n    end\n    local key, name, loc = get(entry, opts.group --[[@as string|false]])\n    if key then\n      loc = opts.loc and entry[opts.loc] or loc or entry.def or entry.ref\n      return { key = key, name = name, loc = loc, ref = entry.ref, def = entry.def }\n    end\n  end\n\n  local node = M.group(group_fn, opts)\n  return M.flatten(node, opts), node, opts\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/profiler/ui.lua",
    "content": "---@class snacks.profiler.ui\nlocal M = {}\n\nM.highlights = {} ---@type table<string, table<number, snacks.profiler.Trace>>\nM.max_time = 0\nM.ns = vim.api.nvim_create_namespace(\"snacks_profiler\")\nM.shades = 20\nM.enabled = true\nM.max_time = 0\n\n---@type table<string, fun(entry:snacks.profiler.Trace):snacks.profiler.Badge>\nM.badge_formats = {\n  time = function(entry)\n    local ms = entry.time / 1e6\n    return { icon = Snacks.profiler.config.icons.time, text = (\"%.2f ms\"):format(ms), level = M.get_level(ms, \"time\") }\n  end,\n  pct = function(entry)\n    local pct = entry.time / M.max_time * 100\n    return { icon = Snacks.profiler.config.icons.pct, text = (\"%d%%\"):format(pct), level = M.get_level(pct, \"pct\") }\n  end,\n  count = function(entry)\n    local count = entry.count or 1\n    return { icon = \" \", text = (\"%d\"):format(count), level = M.get_level(count, \"count\") }\n  end,\n  trace = function(entry)\n    local field, value = entry.name:match(\"^(%w+):(.*)$\") ---@type string?, string?\n    value = field == \"file\" and vim.fn.fnamemodify(value, \":~:.\") or value\n    value = field == \"require\" and (\"require(%q)\"):format(value) or value\n    value = field == \"autocmd\" and (\"autocmd %s\"):format(value) or value\n    value = Snacks.profiler.config.icons[field] and value or entry.name\n    return {\n      icon = Snacks.profiler.config.icons[field] or Snacks.profiler.config.icons.fn,\n      text = value,\n      padding = false,\n      level = \"Trace\",\n    }\n  end,\n}\n\nfunction M.toggle()\n  if M.enabled then\n    M.hide()\n  else\n    M.show()\n  end\nend\n\nfunction M.hide()\n  assert(M.enabled, \"Highlights are not enabled\")\n  M.enabled = false\n  for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n    if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == \"\" then\n      vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)\n    end\n  end\nend\n\nfunction M.show()\n  assert(not M.enabled, \"Highlights are already enabled\")\n  M.enabled = true\n  for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n    if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == \"\" then\n      M.highlight(buf, Snacks.profiler.config.highlights)\n    end\n  end\n  vim.api.nvim_create_autocmd(\"BufReadPost\", {\n    group = vim.api.nvim_create_augroup(\"snacks_profiler_highlights\", { clear = true }),\n    callback = function(ev)\n      if M.enabled then\n        M.highlight(ev.buf, Snacks.profiler.config.highlights)\n      end\n    end,\n  })\nend\n\n---@param trace snacks.profiler.Trace\nfunction M.dump(trace)\n  local ret = {}\n  ---@diagnostic disable-next-line: no-unknown\n  for k, v in pairs(trace) do\n    if type(k) == \"string\" then\n      ---@diagnostic disable-next-line: no-unknown\n      ret[k] = v\n    end\n  end\n  return ret\nend\n\nfunction M.load()\n  M.highlights = {}\n  M.max_time = 10 * 1e6\n  M.colors()\n  local groups = {\n    defs = Snacks.profiler.tracer.find({ group = \"def\", structure = false, sort = false }),\n    refs = Snacks.profiler.tracer.find({ group = \"ref\", structure = false, sort = false }),\n  }\n  for group, entries in pairs(groups) do\n    for _, entry in pairs(entries) do\n      local loc = entry.loc\n      if loc then\n        ---@diagnostic disable-next-line: inject-field\n        entry._group = group\n        M.max_time = math.max(M.max_time, entry.time)\n        M.highlights[loc.file] = M.highlights[loc.file] or {}\n        if Snacks.profiler.config.debug and M.highlights[loc.file][loc.line] then\n          local old = M.highlights[loc.file][loc.line]\n          Snacks.debug.inspect({ group = group, old = M.dump(old), new = M.dump(entry) })\n        end\n        M.highlights[loc.file][loc.line] = entry\n      end\n    end\n  end\nend\n\nfunction M.get_level(value, t)\n  return value > Snacks.profiler.config.thresholds[t][2] and \"Error\"\n    or value > Snacks.profiler.config.thresholds[t][1] and \"Warn\"\n    or \"Info\"\nend\n\n---@param entry snacks.profiler.Trace\n---@param opts? { badges?: snacks.profiler.Badge.type[], indent?: boolean }\n---@return snacks.profiler.Badge[]\nfunction M.badges(entry, opts)\n  opts = opts or {}\n  opts.badges = opts.badges or { \"time\", \"pct\", \"count\", \"name\", \"trace\" }\n  local ret = {} ---@type snacks.profiler.Badge[]\n  local done = {} ---@type table<string, boolean>\n  local indented = false\n  for _, b in ipairs(opts.badges) do\n    if b == \"trace\" or b == \"name\" then\n      local entries = {} ---@type snacks.profiler.Trace[]\n      if b == \"name\" then\n        table.insert(entries, entry)\n      end\n      if b == \"trace\" then\n        vim.list_extend(entries, entry)\n      end\n      for _, e in ipairs(entries) do\n        if not done[e.name] then\n          done[e.name] = true\n          local badge = M.badge_formats.trace(e)\n          if opts.indent and not indented then\n            indented = true\n            badge.text = (\"  \"):rep(e.depth) .. badge.text\n          end\n          table.insert(ret, badge)\n        end\n      end\n    else\n      table.insert(ret, M.badge_formats[b](entry))\n    end\n  end\n  return ret\nend\n\n---@param badges snacks.profiler.Badge[]\n---@param opts? {widths?:number[]}\nfunction M.format(badges, opts)\n  local text = {} ---@type string[][]\n  text[#text + 1] = { \"  \", \"Normal\" }\n  for b, badge in ipairs(badges) do\n    local level = badge.level or \"\"\n    local padding = badge.padding ~= false\n        and opts\n        and opts.widths\n        and (opts.widths[b] - vim.api.nvim_strwidth(badge.text))\n      or 0\n    text[#text + 1] = { badge.icon, \"SnacksProfilerIcon\" .. level }\n    text[#text + 1] = { \" \" .. (\" \"):rep(padding) .. badge.text .. \" \", \"SnacksProfilerBadge\" .. level }\n    text[#text + 1] = { \"  \", \"Normal\" }\n  end\n  return text\nend\n\n---@param buf number\n---@param opts? snacks.profiler.Highlights|{file?:string}\nfunction M.highlight(buf, opts)\n  opts = opts or {}\n  vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)\n  local file = Snacks.profiler.loc.norm({ file = opts.file or vim.api.nvim_buf_get_name(buf), line = 0 }).file\n  local highlights = M.highlights[file]\n  if not highlights then\n    return\n  end\n\n  local keep = {} ---@type table<number, snacks.profiler.Trace>\n  for l, entry in pairs(highlights) do\n    if entry.time >= (opts.min_time or 0) then\n      keep[l] = entry\n    end\n  end\n  highlights = keep\n\n  local align = opts.align or 80\n  local buttons = {} ---@type table<number, snacks.profiler.Badge[]>\n  local widths = {} ---@type number[]\n  for line, entry in pairs(highlights) do\n    buttons[line] = M.badges(entry, opts --[[@as snacks.profiler.Highlights]])\n    for b, button in ipairs(buttons[line]) do\n      widths[b] = math.max(widths[b] or 0, vim.api.nvim_strwidth(button.text))\n    end\n  end\n\n  for line, entry in pairs(highlights) do\n    local text = M.format(buttons[line], { widths = widths })\n    if type(align) == \"number\" then\n      text[#text + 1] = { (\" \"):rep(vim.o.columns), \"Normal\" }\n    end\n    local mmax = math.min(M.max_time, 1e6 * Snacks.profiler.config.highlights.max_shade)\n    vim.api.nvim_buf_set_extmark(buf, M.ns, line - 1, 0, {\n      hl_mode = \"combine\",\n      virt_text = text,\n      virt_text_win_col = type(align) == \"number\" and align or nil,\n      virt_text_pos = align == \"right\" and \"right_align\" or align == \"left\" and \"eol\" or nil,\n      line_hl_group = (\"SnacksProfilerHot%02d\"):format(\n        math.max(math.min(math.floor(entry.time / mmax * M.shades), M.shades), 1)\n      ),\n    })\n  end\nend\n\nfunction M.colors()\n  ---@type snacks.util.hl\n  local hl_groups = {\n    Icon = \"SnacksProfilerIconInfo\",\n    Badge = \"SnacksProfilerBadgeInfo\",\n    IconTrace = \"SnacksProfilerIconInfo\",\n    BadgeTrace = \"SnacksProfilerBadgeInfo\",\n  }\n  local fallbacks = { Info = \"#0ea5e9\", Warn = \"#f59e0b\", Error = \"#dc2626\" }\n  local bg = Snacks.util.color(\"Normal\", \"bg\") or \"#000000\"\n  local red = Snacks.util.color(\"DiagnosticError\") or fallbacks.Error\n  for _, s in ipairs({ \"Info\", \"Warn\", \"Error\" }) do\n    local color = Snacks.util.color(\"Diagnostic\" .. s) or fallbacks[s]\n    hl_groups[\"Icon\" .. s] = { fg = color, bg = Snacks.util.blend(color, bg, 0.3) }\n    hl_groups[\"Badge\" .. s] = { fg = color, bg = Snacks.util.blend(color, bg, 0.1) }\n  end\n  for i = 1, M.shades do\n    hl_groups[(\"Hot%02d\"):format(i)] = { bg = Snacks.util.blend(red, bg, i / (M.shades + 1)) }\n  end\n  Snacks.util.set_hl(hl_groups, { prefix = \"SnacksProfiler\", managed = false, default = true })\n  vim.api.nvim_create_autocmd(\"ColorScheme\", {\n    group = vim.api.nvim_create_augroup(\"snacks_profiler_colors\", { clear = true }),\n    callback = M.colors,\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/quickfile.lua",
    "content": "---@private\n---@class snacks.quickfile\nlocal M = {}\n\nM.meta = {\n  desc = \"When doing `nvim somefile.txt`, it will render the file as quickly as possible, before loading your plugins.\",\n  needs_setup = true,\n}\n\n---@class snacks.quickfile.Config\nlocal defaults = {\n  -- any treesitter langs to exclude\n  exclude = { \"latex\" },\n}\n\n---@private\nfunction M.setup()\n  local opts = Snacks.config.get(\"quickfile\", defaults)\n  -- Skip if we already entered vim\n  if vim.v.vim_did_enter == 1 then\n    return\n  end\n  if vim.bo.filetype == \"bigfile\" then\n    return\n  end\n\n  local buf = vim.api.nvim_get_current_buf()\n\n  -- Try to guess the filetype (may change later on during Neovim startup)\n  local ft = vim.filetype.match({ buf = buf })\n  if ft then\n    -- Add treesitter highlights and fallback to syntax\n    local lang = vim.treesitter.language.get_lang(ft)\n\n    -- disable treesitter for some langs\n    if vim.tbl_contains(opts.exclude, lang) then\n      lang = nil\n    end\n\n    if not (lang and pcall(vim.treesitter.start, buf, lang)) then\n      vim.bo[buf].syntax = ft\n    end\n\n    -- Trigger early redraw\n    vim.cmd([[redraw]])\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/rename.lua",
    "content": "---@class snacks.rename\nlocal M = {}\n\nM.meta = {\n  desc = \"LSP-integrated file renaming with support for plugins like [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim) and [mini.files](https://github.com/nvim-mini/mini.files).\",\n}\n\n-- Renames the provided file, or the current buffer's file.\n-- Prompt for the new filename if `to` is not provided.\n-- do the rename, and trigger LSP handlers\n---@param opts? {from?: string, to?:string, on_rename?: fun(to:string, from:string, ok:boolean)}\nfunction M.rename_file(opts)\n  opts = opts or {}\n  local from = vim.fn.fnamemodify(opts.from or opts.file or vim.api.nvim_buf_get_name(0), \":p\")\n  local to = opts.to and vim.fn.fnamemodify(opts.to, \":p\") or nil\n\n  from, to = svim.fs.normalize(from), to and svim.fs.normalize(to) or nil\n\n  local function rename()\n    assert(to, \"to is required\")\n    M.on_rename_file(from, to, function()\n      local ok = M._rename(from, to)\n      if opts.on_rename then\n        opts.on_rename(to, from, ok)\n      end\n    end)\n  end\n\n  if to then\n    return rename()\n  end\n\n  local root = svim.fs.normalize(vim.fn.getcwd(0))\n\n  if from:find(root, 1, true) ~= 1 then\n    -- file is outside cwd, use its parent dir as root\n    root = vim.fs.dirname(from)\n  end\n\n  local extra = from:sub(#root + 2)\n\n  vim.ui.input({\n    prompt = \"New File Name: \",\n    default = extra,\n    completion = \"file\",\n  }, function(value)\n    if not value or value == \"\" or value == extra then\n      return\n    end\n    to = svim.fs.normalize(root .. \"/\" .. value)\n    rename()\n  end)\nend\n\n--- Rename a file and update buffers\n---@param from string\n---@param to string\n---@return boolean ok\nfunction M._rename(from, to)\n  from = vim.fn.fnamemodify(from, \":p\")\n  to = vim.fn.fnamemodify(to, \":p\")\n  vim.fn.mkdir(vim.fs.dirname(to), \"p\") -- ensure target directory exists\n  -- rename the file\n  local ret = vim.fn.rename(from, to)\n  if ret ~= 0 then\n    Snacks.notify.error(\"Failed to rename file: `\" .. from .. \"`\")\n    return false\n  end\n\n  -- replace buffer in all windows\n  local from_buf = vim.fn.bufnr(from)\n  if from_buf >= 0 then\n    local to_buf = vim.fn.bufadd(to)\n    vim.bo[to_buf].buflisted = true\n    for _, win in ipairs(vim.fn.win_findbuf(from_buf)) do\n      vim.api.nvim_win_call(win, function()\n        vim.cmd(\"buffer \" .. to_buf)\n      end)\n    end\n    vim.api.nvim_buf_delete(from_buf, { force = true })\n  end\n  return true\nend\n\n--- Lets LSP clients know that a file has been renamed\n---@param from string\n---@param to string\n---@param rename? fun()\nfunction M.on_rename_file(from, to, rename)\n  local changes = { files = { {\n    oldUri = vim.uri_from_fname(from),\n    newUri = vim.uri_from_fname(to),\n  } } }\n\n  local clients = (vim.lsp.get_clients or vim.lsp.get_active_clients)()\n  for _, client in ipairs(clients) do\n    if client.supports_method(\"workspace/willRenameFiles\") then\n      local resp = client.request_sync(\"workspace/willRenameFiles\", changes, 1000, 0)\n      if resp and resp.result ~= nil then\n        vim.lsp.util.apply_workspace_edit(resp.result, client.offset_encoding)\n      end\n    end\n  end\n\n  if rename then\n    rename()\n  end\n\n  for _, client in ipairs(clients) do\n    if client.supports_method(\"workspace/didRenameFiles\") then\n      client.notify(\"workspace/didRenameFiles\", changes)\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/scope.lua",
    "content": "---@class snacks.scope\nlocal M = {}\n\nM.meta = {\n  desc = \"Scope detection, text objects and jumping based on treesitter or indent\",\n  needs_setup = true,\n}\n\n---@class snacks.scope.Opts: snacks.scope.Config,{}\n---@field buf? number\n---@field pos? {[1]:number, [2]:number} -- (1,0) indexed\n---@field end_pos? {[1]:number, [2]:number} -- (1,0) indexed\n---@field async? boolean run scope detection asynchronously (defaults to true)\n\n---@class snacks.scope.TextObject: snacks.scope.Opts\n---@field linewise? boolean if nil, use visual mode. Defaults to `false` when not in visual mode\n---@field notify? boolean show a notification when no scope is found (defaults to true)\n\n---@class snacks.scope.Jump: snacks.scope.Opts\n---@field bottom? boolean if true, jump to the bottom of the scope, otherwise to the top\n---@field notify? boolean show a notification when no scope is found (defaults to true)\n\n---@alias snacks.scope.Attach.cb fun(win: number, buf: number, scope:snacks.scope.Scope?, prev:snacks.scope.Scope?)\n\n---@class snacks.scope.Config\n---@field max_size? number\n---@field enabled? boolean\nlocal defaults = {\n  -- absolute minimum size of the scope.\n  -- can be less if the scope is a top-level single line scope\n  min_size = 2,\n  -- try to expand the scope to this size\n  max_size = nil,\n  cursor = true, -- when true, the column of the cursor is used to determine the scope\n  edge = true, -- include the edge of the scope (typically the line above and below with smaller indent)\n  siblings = false, -- expand single line scopes with single line siblings\n  -- what buffers to attach to\n  filter = function(buf)\n    return vim.bo[buf].buftype == \"\" and vim.b[buf].snacks_scope ~= false and vim.g.snacks_scope ~= false\n  end,\n  -- debounce scope detection in ms\n  debounce = 30,\n  treesitter = {\n    -- detect scope based on treesitter.\n    -- falls back to indent based detection if not available\n    enabled = true,\n    injections = true, -- include language injections when detecting scope (useful for languages like `vue`)\n    ---@type string[]|{enabled?:boolean}\n    blocks = {\n      enabled = false, -- enable to use the following blocks\n      \"function_declaration\",\n      \"function_definition\",\n      \"method_declaration\",\n      \"method_definition\",\n      \"class_declaration\",\n      \"class_definition\",\n      \"do_statement\",\n      \"while_statement\",\n      \"repeat_statement\",\n      \"if_statement\",\n      \"for_statement\",\n    },\n    -- these treesitter fields will be considered as blocks\n    field_blocks = {\n      \"local_declaration\",\n    },\n  },\n  -- These keymaps will only be set if the `scope` plugin is enabled.\n  -- Alternatively, you can set them manually in your config,\n  -- using the `Snacks.scope.textobject` and `Snacks.scope.jump` functions.\n  keys = {\n    ---@type table<string, snacks.scope.TextObject|{desc?:string}|false>\n    textobject = {\n      ii = {\n        min_size = 2, -- minimum size of the scope\n        edge = false, -- inner scope\n        cursor = false,\n        treesitter = { blocks = { enabled = false } },\n        desc = \"inner scope\",\n      },\n      ai = {\n        cursor = false,\n        min_size = 2, -- minimum size of the scope\n        treesitter = { blocks = { enabled = false } },\n        desc = \"full scope\",\n      },\n    },\n    ---@type table<string, snacks.scope.Jump|{desc?:string}|false>\n    jump = {\n      [\"[i\"] = {\n        min_size = 1, -- allow single line scopes\n        bottom = false,\n        cursor = false,\n        edge = true,\n        treesitter = { blocks = { enabled = false } },\n        desc = \"jump to top edge of scope\",\n      },\n      [\"]i\"] = {\n        min_size = 1, -- allow single line scopes\n        bottom = true,\n        cursor = false,\n        edge = true,\n        treesitter = { blocks = { enabled = false } },\n        desc = \"jump to bottom edge of scope\",\n      },\n    },\n  },\n}\n\nlocal id = 0\n\n---@alias snacks.scope.scope {buf: number, from: number, to: number, indent?: number}\n\n---@class snacks.scope.Scope\n---@field buf number\n---@field from number\n---@field to number\n---@field indent? number\n---@field opts snacks.scope.Opts\nlocal Scope = {}\nScope.__index = Scope\n\n---@generic T: snacks.scope.Scope\n---@param self T\n---@param scope snacks.scope.scope\n---@param opts snacks.scope.Opts\n---@return T\nfunction Scope:new(scope, opts)\n  local ret = setmetatable(scope, { __index = self, __eq = self.__eq, __tostring = self.__tostring })\n  ret.opts = opts\n  return ret\nend\n\nfunction Scope:__eq(other)\n  return other\n    and self.buf == other.buf\n    and self.from == other.from\n    and self.to == other.to\n    and self.indent == other.indent\nend\n\n---@generic T: snacks.scope.Scope\n---@param self T\n---@param opts snacks.scope.Opts\n---@return T?\nfunction Scope:find(opts)\n  error(\"not implemented\")\nend\n\n---@generic T: snacks.scope.Scope\n---@param self T\n---@return T?\nfunction Scope:parent()\n  error(\"not implemented\")\nend\n\n---@generic T: snacks.scope.Scope\n---@param self T\n---@return T\nfunction Scope:with_edge()\n  error(\"not implemented\")\nend\n\n---@generic T: snacks.scope.scope\n---@param self T\n---@return T\nfunction Scope:inner()\n  error(\"not implemented\")\nend\n\n---@param line number\nfunction Scope.get_indent(line)\n  local ret = vim.fn.indent(line)\n  return ret == -1 and nil or ret, line\nend\n\n---@generic T: snacks.scope.Scope\n---@param self T\n---@param opts {buf?: number, from?: number, to?: number, indent?: number}}\n---@return T?\nfunction Scope:with(opts)\n  opts = vim.tbl_extend(\"keep\", opts, self)\n  return setmetatable(opts, getmetatable(self)) --[[ @as snacks.scope.Scope ]]\nend\n\nfunction Scope:size()\n  return self.to - self.from + 1\nend\n\nfunction Scope:size_with_edge()\n  return self:with_edge():size()\nend\n\n---@generic T: snacks.scope.Scope\n---@param self T\n---@return T?\nfunction Scope:expand(line)\n  local ret = self ---@type snacks.scope.Scope?\n  while ret do\n    if line >= ret.from and line <= ret.to then\n      return ret\n    end\n    ret = ret:parent()\n  end\nend\n\n---@class snacks.scope.IndentScope: snacks.scope.Scope\nlocal IndentScope = setmetatable({}, Scope)\nIndentScope.__index = IndentScope\n\n---@param line number 1-indexed\n---@param indent number\n---@param up? boolean\nfunction IndentScope._expand(line, indent, up)\n  local next = up and vim.fn.prevnonblank or vim.fn.nextnonblank\n  while line do\n    local i, l = IndentScope.get_indent(next(line + (up and -1 or 1)))\n    if (i or 0) == 0 or i < indent or l == 0 then\n      return line\n    end\n    line = l\n  end\n  return line\nend\n\n-- Inner indent scope is all lines with higher indent than the current scope\nfunction IndentScope:inner()\n  local from, to, indent = nil, nil, math.huge\n  for l = self.from, self.to do\n    local i, il = IndentScope.get_indent(vim.fn.nextnonblank(l))\n    if il == l then\n      if i > self.indent then\n        from = from or l\n        to = l\n        indent = math.min(indent, i)\n      end\n    end\n  end\n  return from and to and self:with({ from = from, to = to, indent = indent }) or self\nend\n\nfunction IndentScope:with_edge()\n  if self.indent == 0 then\n    return self\n  end\n  local before_i, before_l = Scope.get_indent(vim.fn.prevnonblank(self.from - 1))\n  local after_i, after_l = Scope.get_indent(vim.fn.nextnonblank(self.to + 1))\n  local indent = math.min(math.max(before_i or self.indent, after_i or self.indent), self.indent)\n  local from = before_i and before_i == indent and before_l or self.from\n  local to = after_i and after_i == indent and after_l or self.to\n  if from == 0 or to == 0 or indent < 0 then\n    return self\n  end\n  return self:with({ from = from, to = to, indent = indent })\nend\n\n---@param opts snacks.scope.Opts\nfunction IndentScope:find(opts)\n  local indent, line = Scope.get_indent(opts.pos[1])\n  local prev_i, prev_l = Scope.get_indent(vim.fn.prevnonblank(line - 1))\n  local next_i, next_l = Scope.get_indent(vim.fn.nextnonblank(line + 1))\n\n  -- fix indent when line is empty\n  if vim.fn.prevnonblank(line) ~= line then\n    indent, line = Scope.get_indent(prev_i > next_i and prev_l or next_l)\n    prev_i, prev_l = Scope.get_indent(vim.fn.prevnonblank(line - 1))\n    next_i, next_l = Scope.get_indent(vim.fn.nextnonblank(line + 1))\n  end\n\n  if line == 0 then\n    return\n  end\n\n  -- adjust line to the nearest indent block\n  if prev_i <= indent and next_i > indent then\n    -- at top edge\n    line = next_l\n    indent = next_i\n  elseif next_i <= indent and prev_i > indent then\n    -- at bottom edge\n    line = prev_l\n    indent = prev_i\n  elseif next_i > indent and prev_i > indent then\n    -- at edge of two blocks. Prefer the one below.\n    line = next_l\n    indent = next_i\n  end\n\n  if opts.cursor then\n    indent = math.min(indent, vim.fn.virtcol(opts.pos) + 1)\n  end\n\n  -- expand to include bigger indents\n  return IndentScope:new({\n    buf = opts.buf,\n    from = IndentScope._expand(line, indent, true),\n    to = IndentScope._expand(line, indent, false),\n    indent = indent,\n  }, opts)\nend\n\nfunction IndentScope:parent()\n  for i = self.indent - 1, 1, -1 do\n    local u, d = IndentScope._expand(self.from, i, true), IndentScope._expand(self.to, i, false)\n    if u ~= self.from or d ~= self.to then -- update only when expanded\n      return self:with({ from = u, to = d, indent = i })\n    end\n  end\nend\n\n---@class snacks.scope.TSScope: snacks.scope.Scope\n---@field node TSNode\nlocal TSScope = setmetatable({}, Scope)\nTSScope.__index = TSScope\n\n-- Expand the scope to fill the range of the node\nfunction TSScope:fill()\n  local n = self.node\n  local u, _, d = n:range()\n  while n do\n    local uu, _, dd = n:range()\n    if uu == u and dd == d and not self:is_field(n) then\n      self.node = n\n    else\n      break\n    end\n    n = n:parent()\n  end\nend\n\nfunction TSScope:fix()\n  self:fill()\n  self.from, _, self.to = self.node:range()\n  self.from, self.to = self.from + 1, self.to + 1\n  self.indent = math.min(vim.fn.indent(self.from), vim.fn.indent(self.to))\n  return self\nend\n\n---@param node? TSNode\nfunction TSScope:is_field(node)\n  node = node or self.node\n  local parent = node:parent()\n  parent = parent ~= node:tree():root() and parent or nil\n  if not parent then\n    return false\n  end\n  for child, field in parent:iter_children() do\n    if child == node then\n      return not (field == nil or vim.tbl_contains(self.opts.treesitter.field_blocks, field))\n    end\n  end\n  error(\"node not found in parent\")\nend\n\nfunction TSScope:with_edge()\n  local ret = self ---@type snacks.scope.TSScope?\n  while ret do\n    if ret:size() >= 1 and not ret:is_field() then\n      return ret\n    end\n    ret = ret:parent()\n  end\n  return self\nend\n\nfunction TSScope:root()\n  if type(self.opts.treesitter.blocks) ~= \"table\" or not self.opts.treesitter.blocks.enabled then\n    return self:fix()\n  end\n  local root = self.node --[[@as TSNode?]]\n  while root do\n    if vim.tbl_contains(self.opts.treesitter.blocks, root:type()) then\n      return self:with({ node = root })\n    end\n    root = root:parent()\n  end\n  return self:fix()\nend\n\n---@param opts {buf?: number, from?: number, to?: number, indent?: number, node?: TSNode}}\nfunction TSScope:with(opts)\n  local ret = Scope.with(self, opts) --[[ @as snacks.scope.TSScope ]]\n  return ret:fix()\nend\n\n---@param opts snacks.scope.Opts\nfunction TSScope:parser(opts)\n  local lang = vim.bo[opts.buf].filetype\n  local has_parser, parser = pcall(vim.treesitter.get_parser, opts.buf, lang, { error = false })\n  return has_parser and parser or nil\nend\n\n---@param cb fun()\n---@param opts snacks.scope.Opts\nfunction TSScope:init(cb, opts)\n  local parser = self:parser(opts)\n  if not parser then\n    return cb()\n  end\n  if opts.async == false then\n    parser:parse()\n    cb()\n  else\n    Snacks.util.parse(parser, opts.treesitter.injections, cb)\n  end\nend\n\n---@param opts snacks.scope.Opts\nfunction TSScope:find(opts)\n  local lang = vim.treesitter.language.get_lang(vim.bo[opts.buf].filetype)\n  local line = vim.fn.nextnonblank(opts.pos[1])\n  line = line == 0 and vim.fn.prevnonblank(opts.pos[1]) or line\n  -- FIXME:\n  local pos = {\n    math.max(line - 1, 0),\n    (vim.fn.getline(line):find(\"%S\") or 1) - 1, -- find first non-space character\n  }\n\n  local node = vim.treesitter.get_node({\n    pos = pos,\n    bufnr = opts.buf,\n    lang = lang,\n    ignore_injections = not opts.treesitter.injections,\n  })\n  if not node then\n    return\n  end\n\n  if opts.cursor then\n    -- expand to biggest ancestor with a lower start position\n    local n = node ---@type TSNode?\n    local virtcol = vim.fn.virtcol(opts.pos)\n    while n and n ~= n:tree():root() do\n      local r, c = n:range()\n      local virtcol_n = vim.fn.virtcol({ r + 1, c })\n      if virtcol_n > virtcol then\n        node, n = n, n:parent()\n      else\n        break\n      end\n    end\n  end\n\n  local ret = TSScope:new({ buf = opts.buf, node = node }, opts):root()\n  return ret\nend\n\nfunction TSScope:parent()\n  local parent = self.node:parent()\n  return parent and parent ~= self.node:tree():root() and self:with({ node = parent }):root() or nil\nend\n\n-- Inner treesitter scope includes all lines for which the node\n-- has a start position lower than the start of the scope.\nfunction TSScope:inner()\n  local from, to, indent = nil, nil, math.huge\n  for l = self.from + 1, self.to do\n    if l == vim.fn.nextnonblank(l) then\n      local col = (vim.fn.getline(l):find(\"%S\") or 1) - 1\n      local node = vim.treesitter.get_node({ pos = { l - 1, col }, bufnr = self.buf })\n      local s = TSScope:new({ buf = self.buf, node = node }, self.opts):fix()\n      if s and s.from > self.from and s.to <= self.to then\n        from = from or l\n        to = l\n        indent = math.min(indent, vim.fn.indent(l))\n      end\n    end\n  end\n  return from and to and IndentScope:new({ from = from, to = to, indent = indent }, self.opts) or self\nend\n\nfunction Scope:__tostring()\n  local meta = getmetatable(self)\n  return (\"%s(buf=%d, from=%d, to=%d, indent=%d)\"):format(\n    rawequal(meta, TSScope) and \"TSScope\" or rawequal(meta, IndentScope) and \"IndentSCope\" or \"Scope\",\n    self.buf or -1,\n    self.from or -1,\n    self.to or -1,\n    self.indent or 0\n  )\nend\n\n---@param cb fun(scope?: snacks.scope.Scope)\n---@param opts? snacks.scope.Opts|{parse?:boolean}\nfunction M.get(cb, opts)\n  opts = Snacks.config.get(\"scope\", defaults, opts or {}) --[[ @as snacks.scope.Opts ]]\n  opts.buf = (opts.buf == nil or opts.buf == 0) and vim.api.nvim_get_current_buf() or opts.buf\n  if not opts.pos then\n    assert(opts.buf == vim.api.nvim_win_get_buf(0), \"missing pos\")\n    opts.pos = vim.api.nvim_win_get_cursor(0)\n  end\n\n  -- run in the context of the buffer if not current\n  if vim.api.nvim_get_current_buf() ~= opts.buf then\n    vim.api.nvim_buf_call(opts.buf, function()\n      M.get(cb, opts)\n    end)\n    return\n  end\n\n  ---@type snacks.scope.Scope\n  local Class = (opts.treesitter.enabled and Snacks.util.get_lang(opts.buf)) and TSScope or IndentScope\n  if rawequal(Class, TSScope) and opts.parse ~= false then\n    TSScope:init(function()\n      opts.parse = false\n      M.get(cb, opts)\n    end, opts)\n    return\n  end\n  local scope = Class:find(opts) --[[ @as snacks.scope.Scope? ]]\n\n  -- fallback to indent based detection\n  if not scope and rawequal(Class, TSScope) then\n    Class = IndentScope\n    scope = Class:find(opts)\n  end\n\n  -- when end_pos is provided, get its scope and expand the current scope\n  -- to include it.\n  if scope and opts.end_pos and not vim.deep_equal(opts.pos, opts.end_pos) then\n    local end_scope = Class:find(vim.tbl_extend(\"keep\", { pos = opts.end_pos }, opts)) --[[ @as snacks.scope.Scope? ]]\n    if end_scope and end_scope.from < scope.from then\n      scope = scope:expand(end_scope.from) or scope\n    end\n    if end_scope and end_scope.to > scope.to then\n      scope = scope:expand(end_scope.to) or scope\n    end\n  end\n\n  local min_size = opts.min_size or 2\n  local max_size = opts.max_size or min_size\n\n  -- expand block with ancestors until min_size is reached\n  -- or max_size is reached\n  if scope then\n    local s = scope --- @type snacks.scope.Scope?\n    while s do\n      if opts.edge and scope:size_with_edge() >= min_size and s:size_with_edge() > max_size then\n        break\n      elseif not opts.edge and scope:size() >= min_size and s:size() > max_size then\n        break\n      end\n      scope, s = s, s:parent()\n    end\n    -- expand with edge\n    if opts.edge then\n      scope = scope:with_edge() --[[@as snacks.scope.Scope]]\n    end\n  end\n\n  -- expand single line blocks with single line siblings\n  if opts.siblings and scope and scope:size() == 1 then\n    while scope and scope:size() < min_size do\n      local prev, next = vim.fn.prevnonblank(scope.from - 1), vim.fn.nextnonblank(scope.to + 1) ---@type number, number\n      local prev_dist, next_dist = math.abs(opts.pos[1] - prev), math.abs(opts.pos[1] - next)\n      local prev_s = prev > 0 and Class:find(vim.tbl_extend(\"keep\", { pos = { prev, 0 } }, opts))\n      local next_s = next > 0 and Class:find(vim.tbl_extend(\"keep\", { pos = { next, 0 } }, opts))\n      prev_s = prev_s and prev_s:size() == 1 and prev_s\n      next_s = next_s and next_s:size() == 1 and next_s\n      local s = prev_dist < next_dist and prev_s or next_s or prev_s\n      if s and (s.from < scope.from or s.to > scope.to) then\n        scope = Scope.with(scope, { from = math.min(scope.from, s.from), to = math.max(scope.to, s.to) })\n      else\n        break\n      end\n    end\n  end\n  cb(scope)\nend\n\n---@class snacks.scope.Listener\n---@field id integer\n---@field cb snacks.scope.Attach.cb\n---@field opts snacks.scope.Config\n---@field dirty table<number, boolean>\n---@field timer uv.uv_timer_t\n---@field augroup integer\n---@field enabled boolean\n---@field active table<number, snacks.scope.Scope>\nlocal Listener = {}\n\n---@param cb snacks.scope.Attach.cb\n---@param opts? snacks.scope.Config\nfunction Listener.new(cb, opts)\n  local self = setmetatable({}, { __index = Listener })\n  self.cb = cb\n  self.dirty = {}\n  self.timer = assert((vim.uv or vim.loop).new_timer())\n  self.enabled = false\n  self.opts = Snacks.config.get(\"scope\", defaults, opts or {}) --[[ @as snacks.scope.Opts ]]\n  id = id + 1\n  self.id = id\n  self.active = {}\n  return self\nend\n\n--- Check if the scope has changed in the window / buffer\nfunction Listener:check(win)\n  local buf = vim.api.nvim_win_get_buf(win)\n  if not self.opts.filter(buf) then\n    if self.active[win] then\n      local prev = self.active[win]\n      self.active[win] = nil\n      self.cb(win, buf, nil, prev)\n    end\n    return\n  end\n\n  M.get(\n    function(scope)\n      local prev = self.active[win]\n      if prev == scope then\n        return -- no change\n      end\n      self.active[win] = scope\n      self.cb(win, buf, scope, prev)\n    end,\n    vim.tbl_extend(\"keep\", {\n      buf = buf,\n      pos = vim.api.nvim_win_get_cursor(win),\n    }, self.opts)\n  )\nend\n\n--- Get the active scope for a window\nfunction Listener:get(win)\n  local scope = self.active[win]\n  return scope and vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == scope.buf and scope or nil\nend\n\n--- Cleanup invalid scopes\nfunction Listener:clean()\n  for win in pairs(self.active) do\n    self.active[win] = self:get(win)\n  end\nend\n\n--- Iterate over active scopes\nfunction Listener:iter()\n  self:clean()\n  return pairs(self.active)\nend\n\n--- Schedule a scope update\n---@param wins? number|number[]\n---@param opts? {now?: boolean}\nfunction Listener:update(wins, opts)\n  wins = type(wins) == \"number\" and { wins } or wins or vim.api.nvim_list_wins() --[[ @as number[] ]]\n  for _, b in ipairs(wins) do\n    self.dirty[b] = true\n  end\n  local function update()\n    self:_update()\n  end\n  if opts and opts.now then\n    update()\n  end\n  self.timer:start(self.opts.debounce, 0, vim.schedule_wrap(update))\nend\n\n--- Process all pending updates\nfunction Listener:_update()\n  for win in pairs(self.dirty) do\n    if vim.api.nvim_win_is_valid(win) then\n      self:check(win)\n    end\n  end\n  self.dirty = {}\nend\n\n--- Start listening for scope changes\nfunction Listener:enable()\n  assert(not self.enabled, \"already enabled\")\n  self.enabled = true\n  self.augroup = vim.api.nvim_create_augroup(\"snacks_scope_\" .. self.id, { clear = true })\n  vim.api.nvim_create_autocmd({ \"CursorMoved\", \"CursorMovedI\" }, {\n    group = self.augroup,\n    callback = function(ev)\n      for _, win in ipairs(vim.fn.win_findbuf(ev.buf)) do\n        self:update(win)\n      end\n    end,\n  })\n  vim.api.nvim_create_autocmd({ \"WinClosed\", \"BufDelete\", \"BufWipeout\" }, {\n    group = self.augroup,\n    callback = function()\n      self:clean()\n    end,\n  })\n  self:update(nil, { now = true })\nend\n\n--- Stop listening for scope changes\nfunction Listener:disable()\n  assert(self.enabled, \"already disabled\")\n  self.enabled = false\n  vim.api.nvim_del_augroup_by_id(self.augroup)\n  self.timer:stop()\n  self.active = {}\n  self.dirty = {}\nend\n\n--- Attach a scope listener\n---@param cb snacks.scope.Attach.cb\n---@param opts? snacks.scope.Config\n---@return snacks.scope.Listener\nfunction M.attach(cb, opts)\n  local ret = Listener.new(cb, opts)\n  ret:enable()\n  return ret\nend\n\n-- Text objects for indent scopes.\n-- Best to use with Treesitter disabled.\n-- When in visual mode, it will select the scope containing the visual selection.\n-- When the scope is the same as the visual selection, it will select the parent scope instead.\n---@param opts? snacks.scope.TextObject\nfunction M.textobject(opts)\n  opts = Snacks.config.get(\"scope\", defaults, opts or {}) --[[ @as snacks.scope.TextObject ]]\n\n  local mode = vim.fn.mode()\n  local selection = mode:find(\"[vV]\") ~= nil\n\n  -- prepare for visual mode and determine linewise\n  if mode == \"v\" then\n    vim.cmd(\"normal! v\")\n  elseif mode == \"V\" then\n    vim.cmd(\"normal! V\")\n    opts.linewise = opts.linewise == nil and true or opts.linewise\n  end\n\n  -- use the actual range instead of the cursor position\n  -- in case of visual mode\n  if selection then\n    opts.pos = vim.api.nvim_buf_get_mark(0, \"<\")\n    opts.end_pos = vim.api.nvim_buf_get_mark(0, \">\")\n  end\n  local inner = not opts.edge\n  opts.edge = true -- always include the edge of the scope to make inner work\n  opts.async = false -- run synchronously\n\n  M.get(function(scope)\n    if not scope then\n      return opts.notify ~= false and Snacks.notify.warn(\"No scope in range\")\n    end\n\n    scope = inner and scope:inner() or scope\n    -- determine scope range\n    local from, to =\n      { scope.from, opts.linewise and 0 or vim.fn.indent(scope.from) },\n      { scope.to, opts.linewise and 0 or vim.fn.col({ scope.to, \"$\" }) - 2 }\n\n    -- select the range\n    vim.api.nvim_win_set_cursor(0, from)\n    vim.cmd(\"normal! \" .. (opts.linewise and \"V\" or \"v\"))\n    vim.api.nvim_win_set_cursor(0, to)\n  end, opts)\nend\n\n--- Jump to the top or bottom of the scope\n--- If the scope is the same as the current scope, it will jump to the parent scope instead.\n---@param opts? snacks.scope.Jump\nfunction M.jump(opts)\n  opts = Snacks.config.get(\"scope\", defaults, opts or {}) --[[ @as snacks.scope.Jump ]]\n  M.get(function(scope)\n    if not scope then\n      return opts.notify ~= false and Snacks.notify.warn(\"No scope in range\")\n    end\n    while scope do\n      local line = opts.bottom and scope.to or scope.from\n      local pos = { line, vim.fn.indent(line) }\n      if not vim.deep_equal(vim.api.nvim_win_get_cursor(0), pos) then\n        return vim.api.nvim_win_set_cursor(0, { line, vim.fn.indent(line) })\n      end\n      scope = scope:parent()\n    end\n  end, opts)\nend\n\n---@private\nfunction M.setup()\n  local keys = Snacks.config.get(\"scope\", defaults).keys\n  for key, opts in pairs(keys.textobject) do\n    if opts then\n      vim.keymap.set({ \"x\", \"o\" }, key, function()\n        M.textobject(opts)\n      end, { silent = true, desc = opts.desc })\n    end\n  end\n  for key, opts in pairs(keys.jump) do\n    if opts then\n      vim.keymap.set({ \"n\", \"x\", \"o\" }, key, function()\n        M.jump(opts)\n      end, { silent = true, desc = opts.desc })\n    end\n  end\nend\n\nM.TSScope = TSScope\nM.IdentScope = IndentScope\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/scratch.lua",
    "content": "local uv = vim.uv or vim.loop\n\n---@class snacks.scratch\n---@overload fun(opts?: snacks.scratch.Config): snacks.win\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.open(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Scratch buffers with a persistent file\",\n}\n\nM.version = 1\nM.version_checked = false\n\n---@class snacks.scratch.File\n---@field file string full path to the scratch buffer\n---@field name string name of the scratch buffer\n---@field ft string file type\n---@field icon? string icon for the file type\n---@field icon_hl? string highlight group for the icon\n---@field cwd? string current working directory\n---@field branch? string Git branch\n---@field count? number vim.v.count1 used to open the buffer\n---@field id? string unique id used instead of name for the filename hash\n\n---@class snacks.scratch.Config\n---@field win? snacks.win.Config scratch window\n---@field template? string template for new buffers\n---@field file? string scratch file path. You probably don't need to set this.\n---@field ft? string|fun():string the filetype of the scratch buffer\nlocal defaults = {\n  name = \"Scratch\",\n  ft = function()\n    if vim.bo.buftype == \"\" and vim.bo.filetype ~= \"\" then\n      return vim.bo.filetype\n    end\n    return \"markdown\"\n  end,\n  ---@type string|string[]?\n  icon = nil, -- `icon|{icon, icon_hl}`. defaults to the filetype icon\n  root = vim.fn.stdpath(\"data\") .. \"/scratch\",\n  autowrite = true, -- automatically write when the buffer is hidden\n  -- unique key for the scratch file is based on:\n  -- * name\n  -- * ft\n  -- * vim.v.count1 (useful for keymaps)\n  -- * cwd (optional)\n  -- * branch (optional)\n  filekey = {\n    id = nil, ---@type string? unique id used instead of name for the filename hash\n    cwd = true, -- use current working directory\n    branch = true, -- use current branch name\n    count = true, -- use vim.v.count1\n  },\n  win = { style = \"scratch\" },\n  ---@type table<string, snacks.win.Config>\n  win_by_ft = {\n    lua = {\n      keys = {\n        [\"source\"] = {\n          \"<cr>\",\n          function(self)\n            local name = \"scratch.\" .. vim.fn.fnamemodify(vim.api.nvim_buf_get_name(self.buf), \":e\")\n            Snacks.debug.run({ buf = self.buf, name = name })\n          end,\n          desc = \"Source buffer\",\n          mode = { \"n\", \"x\" },\n        },\n      },\n    },\n  },\n}\n\nSnacks.util.set_hl({\n  Title = \"FloatTitle\",\n}, { prefix = \"SnacksScratch\", default = true })\n\nSnacks.config.style(\"scratch\", {\n  width = 100,\n  height = 30,\n  bo = { buftype = \"\", buflisted = false, bufhidden = \"hide\", swapfile = false },\n  minimal = false,\n  noautocmd = false,\n  -- position = \"right\",\n  zindex = 20,\n  wo = { winhighlight = \"NormalFloat:Normal\" },\n  footer_keys = true,\n  border = true,\n})\n\n--- Return a list of scratch buffers sorted by mtime.\n---@return snacks.scratch.File[]\nfunction M.list()\n  M.migrate()\n  local root = Snacks.config.get(\"scratch\", defaults).root\n  ---@type (snacks.scratch.File|{stat:uv.fs_stat.result})[]\n  local ret = {}\n  for file, t in vim.fs.dir(root) do\n    if t == \"file\" and file:sub(-5) == \".meta\" then\n      local path = svim.fs.normalize(root .. \"/\" .. file:sub(1, -6))\n      local stat = uv.fs_stat(path)\n      if stat then\n        ret[#ret + 1] = M.get({ file = path })\n        ret[#ret].stat = stat\n      end\n    end\n  end\n  table.sort(ret, function(a, b)\n    return a.stat.mtime.sec > b.stat.mtime.sec\n  end)\n  return ret\nend\n\n--- Migrate old scratch files to the new format.\n---@private\nfunction M.migrate()\n  if M.version_checked then\n    return\n  end\n  M.version_checked = true\n  local root = Snacks.config.get(\"scratch\", defaults).root\n  local ok, version = pcall(vim.fn.readfile, root .. \"/.version\")\n  if ok and tonumber(version[1]) == M.version then\n    return\n  end\n  vim.fn.mkdir(root .. \"/bak\", \"p\")\n\n  for file, t in vim.fs.dir(root) do\n    if t == \"file\" then\n      -- old format. Keep for backward compatibility\n      local decoded = Snacks.util.file_decode(file)\n      local count, icon, name, cwd, branch, ft = decoded:match(\"^(%d*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)%.([^|]*)$\")\n      if count and icon and name and cwd and branch and ft then\n        local path = svim.fs.normalize(root .. \"/\" .. file)\n        ---@type snacks.scratch.File\n        local scratch = {\n          file = path,\n          count = count ~= \"\" and tonumber(count) or nil,\n          icon = icon ~= \"\" and icon or nil,\n          name = name,\n          cwd = cwd ~= \"\" and cwd or nil,\n          branch = branch ~= \"\" and branch or nil,\n          ft = ft,\n        }\n        -- backup file\n        vim.fn.filecopy(path, root .. \"/bak/\" .. file)\n        vim.fn.rename(path, M._write_meta(root, scratch))\n      end\n    end\n  end\n  vim.fn.writefile({ tostring(M.version) }, root .. \"/.version\")\nend\n\n--- Select a scratch buffer from a list of scratch buffers.\nfunction M.select()\n  return Snacks.picker.scratch()\nend\n\n--- Open a scratch buffer with the given options.\n--- If a window is already open with the same buffer,\n--- it will be closed instead.\n---@param opts? snacks.scratch.Config\nfunction M.open(opts)\n  M.migrate()\n  opts = Snacks.config.get(\"scratch\", defaults, opts)\n  local scratch = M.get(opts)\n\n  opts.win = Snacks.win.resolve(\"scratch\", opts.win_by_ft[scratch.ft], opts.win, {\n    show = false,\n    bo = { filetype = scratch.ft },\n  })\n\n  opts.win.title = {\n    { \" \", \"SnacksScratchTitle\" },\n    { scratch.icon .. string.rep(\" \", 2 - vim.api.nvim_strwidth(scratch.icon)), scratch.icon_hl },\n    { \" \", \"SnacksScratchTitle\" },\n    { opts.name .. (vim.v.count1 > 1 and \" \" .. vim.v.count1 or \"\"), \"SnacksScratchTitle\" },\n    { \" \", \"SnacksScratchTitle\" },\n  }\n\n  local is_new = not uv.fs_stat(scratch.file)\n  local buf = vim.fn.bufadd(scratch.file)\n\n  local win = vim.fn.bufwinid(buf)\n  if win ~= -1 then\n    vim.schedule(function()\n      vim.api.nvim_win_call(win, function()\n        vim.cmd([[close]])\n      end)\n    end)\n    return\n  end\n\n  opts.win.zindex = Snacks.win.zindex(opts.win.zindex or 20)\n  is_new = is_new\n    and vim.api.nvim_buf_line_count(buf) == 0\n    and #(vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] or \"\") == 0\n\n  if not vim.api.nvim_buf_is_loaded(buf) then\n    vim.fn.bufload(buf)\n  end\n\n  if opts.template then\n    local function reset()\n      vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(opts.template, \"\\n\"))\n    end\n    opts.win.keys = opts.win.keys or {}\n    opts.win.keys.reset = { \"R\", reset, desc = \"Reset buffer\" }\n    if is_new then\n      reset()\n    end\n  end\n\n  opts.win.buf = buf\n  if opts.autowrite then\n    vim.api.nvim_create_autocmd(\"BufHidden\", {\n      group = vim.api.nvim_create_augroup(\"snacks_scratch_autowrite_\" .. buf, { clear = true }),\n      buffer = buf,\n      callback = function(ev)\n        vim.api.nvim_buf_call(ev.buf, function()\n          vim.cmd(\"silent! write\")\n          vim.bo[ev.buf].buflisted = false\n        end)\n      end,\n    })\n  end\n  return Snacks.win(opts.win):show()\nend\n\n---@param opts? snacks.scratch.Config\n---@private\nfunction M.get(opts)\n  opts = Snacks.config.get(\"scratch\", defaults, opts)\n\n  -- File type\n  local ft = \"markdown\" ---@type string\n  if opts.file then\n    ft = vim.filetype.match({ filename = opts.file }) or ft\n  elseif type(opts.ft) == \"function\" then\n    ft = opts.ft()\n  elseif type(opts.ft) == \"string\" then\n    ft = opts.ft --[[@as string]]\n  end\n\n  -- Icon\n  local icon = opts.icon or {}\n  icon = type(icon) == \"string\" and { icon } or icon\n  ---@cast icon string[]\n  if not icon[1] and opts.file then\n    icon[1], icon[2] = Snacks.util.icon(opts.file or \"\", \"file\")\n  elseif not icon[1] and ft then\n    icon[1], icon[2] = Snacks.util.icon(ft, \"filetype\")\n  end\n\n  ---@type snacks.scratch.File\n  local ret = {\n    file = \"\",\n    name = opts.name,\n    ft = ft,\n    icon = icon[1],\n    icon_hl = icon[2],\n  }\n\n  -- File\n  if opts.file then\n    ret.file = svim.fs.normalize(opts.file)\n    local meta = ret.file .. \".meta\"\n    if uv.fs_stat(meta) then\n      local ok, decoded = pcall(vim.json.decode, table.concat(vim.fn.readfile(meta), \"\\n\"))\n      if ok and type(decoded) == \"table\" then\n        ret = Snacks.config.merge(ret, decoded, { file = ret.file })\n      end\n    end\n  else\n    ret.count = opts.filekey.count and vim.v.count1 or nil\n    ret.cwd = opts.filekey.cwd and svim.fs.normalize(assert(uv.cwd())) or nil\n    if opts.filekey.branch and uv.fs_stat(\".git\") then\n      local out = vim.trim(vim.fn.systemlist(\"git branch --show-current\")[1] or \"\")\n      ret.branch = vim.v.shell_error == 0 and out ~= \"\" and out or nil\n    end\n    ret.file = M._write_meta(opts.root, ret)\n  end\n  return ret\nend\n\n---@param root string\n---@param scratch snacks.scratch.File\n---@private\nfunction M._write_meta(root, scratch)\n  local key = { scratch.id or scratch.name }\n  key[#key + 1] = scratch.count and tostring(scratch.count) or nil\n  key[#key + 1] = scratch.cwd and scratch.cwd or nil\n  key[#key + 1] = scratch.branch and scratch.branch or nil\n  vim.fn.mkdir(root, \"p\")\n  local hash = vim.fn.sha256(table.concat(key, \"|\")):sub(1, 8)\n  local file = svim.fs.normalize((\"%s/%s.%s\"):format(root, hash, scratch.ft))\n  vim.fn.writefile(vim.split(vim.json.encode(scratch), \"\\n\"), file .. \".meta\")\n  return file\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/scroll.lua",
    "content": "---@class snacks.scroll\nlocal M = {}\n\nM.meta = {\n  desc = \"Smooth scrolling\",\n  needs_setup = true,\n}\n\n---@alias snacks.scroll.View {topline:number, lnum:number}\n\n---@class snacks.scroll.State\n---@field anim? snacks.animate.Animation\n---@field win number\n---@field buf number\n---@field view vim.fn.winsaveview.ret\n---@field current vim.fn.winsaveview.ret\n---@field target vim.fn.winsaveview.ret\n---@field scrolloff number\n---@field changedtick number\n---@field last number vim.uv.hrtime of last scroll\n---@field _wo vim.wo Backup of window options\nlocal State = {}\nState.__index = State\n\n---@class snacks.scroll.Config\n---@field animate snacks.animate.Config|{}\n---@field animate_repeat snacks.animate.Config|{}|{delay:number}\nlocal defaults = {\n  animate = {\n    duration = { step = 10, total = 200 },\n    easing = \"linear\",\n  },\n  -- faster animation when repeating scroll after delay\n  animate_repeat = {\n    delay = 100, -- delay in ms before using the repeat animation\n    duration = { step = 5, total = 50 },\n    easing = \"linear\",\n  },\n  -- what buffers to animate\n  filter = function(buf)\n    return vim.g.snacks_scroll ~= false and vim.b[buf].snacks_scroll ~= false and vim.bo[buf].buftype ~= \"terminal\"\n  end,\n  debug = false,\n}\n\nlocal mouse_scrolling = false\n\nM.enabled = false\nlocal SCROLL_UP, SCROLL_DOWN = Snacks.util.keycode(\"<c-y>\"), Snacks.util.keycode(\"<c-e>\")\n\nlocal uv = vim.uv or vim.loop\nlocal stats = { targets = 0, animating = 0, reset = 0, skipped = 0, mousescroll = 0, scrolls = 0 }\nlocal config = Snacks.config.get(\"scroll\", defaults)\nlocal debug_timer = assert((vim.uv or vim.loop).new_timer())\nlocal states = {} ---@type table<number, snacks.scroll.State>\n\nlocal function is_enabled(buf)\n  return M.enabled\n    and buf\n    and not vim.o.paste\n    and vim.fn.reg_executing() == \"\"\n    and vim.fn.reg_recording() == \"\"\n    and config.filter(buf)\n    and Snacks.animate.enabled({ buf = buf, name = \"scroll\" })\nend\n\n---@param win number\nfunction State.get(win)\n  local buf = vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win)\n  if not buf or not is_enabled(buf) then\n    states[win] = nil\n    return nil\n  end\n\n  local view = vim.api.nvim_win_call(win, vim.fn.winsaveview) ---@type vim.fn.winsaveview.ret\n  local ret = states[win]\n  if not (ret and ret:valid()) then\n    if ret then\n      ret:stop()\n    end\n    ret = setmetatable({}, State)\n    ret.buf = buf\n    ret._wo = {}\n    ret.changedtick = vim.api.nvim_buf_get_changedtick(buf)\n    ret.current = vim.deepcopy(view)\n    ret.last = 0\n    ret.target = vim.deepcopy(view)\n    ret.win = win\n  end\n  ret.scrolloff = ret._wo.scrolloff or vim.wo[win].scrolloff\n  ret.view = view\n  states[win] = ret\n  return ret\nend\n\nfunction State:stop()\n  self:wo() -- restore window options\n  if self.anim then\n    self.anim:stop()\n    self.anim = nil\n  end\nend\n\n--- Save or restore window options\n---@param opts? vim.wo|{}\nfunction State:wo(opts)\n  if not opts then\n    if vim.api.nvim_win_is_valid(self.win) then\n      for k, v in pairs(self._wo) do\n        vim.wo[self.win][k] = v\n      end\n    end\n    self._wo = {}\n    return\n  else\n    for k, v in pairs(opts) do\n      self._wo[k] = self._wo[k] or vim.wo[self.win][k]\n      vim.wo[self.win][k] = v\n    end\n  end\nend\n\nfunction State:valid()\n  return M.enabled\n    and states[self.win] == self\n    and vim.api.nvim_win_is_valid(self.win)\n    and vim.api.nvim_buf_is_valid(self.buf)\n    and vim.api.nvim_win_get_buf(self.win) == self.buf\n    and vim.api.nvim_buf_get_changedtick(self.buf) == self.changedtick\nend\n\nfunction State:update()\n  if vim.api.nvim_win_is_valid(self.win) then\n    self.current = vim.api.nvim_win_call(self.win, vim.fn.winsaveview)\n  end\nend\n\n--- Reset the scroll state for a buffer\n---@param win number\nfunction State.reset(win)\n  if states[win] then\n    states[win]:stop()\n    states[win] = nil\n  end\nend\n\nfunction M.enable()\n  if M.enabled then\n    return\n  end\n  M.enabled = true\n  states = {}\n  if config.debug then\n    M.debug()\n  end\n\n  -- get initial state for all windows\n  for _, win in ipairs(vim.api.nvim_list_wins()) do\n    State.get(win)\n  end\n\n  local group = vim.api.nvim_create_augroup(\"snacks_scroll\", { clear = true })\n\n  -- track mouse scrolling\n  Snacks.util.on_key(\"<ScrollWheelDown>\", function()\n    mouse_scrolling = true\n  end)\n  Snacks.util.on_key(\"<ScrollWheelUp>\", function()\n    mouse_scrolling = true\n  end)\n\n  -- initialize state for buffers entering windows\n  vim.api.nvim_create_autocmd(\"BufWinEnter\", {\n    group = group,\n    callback = vim.schedule_wrap(function(ev)\n      for _, win in ipairs(vim.fn.win_findbuf(ev.buf)) do\n        State.get(win)\n      end\n    end),\n  })\n\n  -- update state when leaving insert mode or changing text in normal mode\n  vim.api.nvim_create_autocmd({ \"InsertLeave\", \"TextChanged\", \"TextChangedI\" }, {\n    group = group,\n    callback = function(ev)\n      for _, win in ipairs(vim.fn.win_findbuf(ev.buf)) do\n        State.get(win)\n      end\n    end,\n  })\n\n  -- update current state on cursor move\n  vim.api.nvim_create_autocmd({ \"CursorMoved\", \"CursorMovedI\" }, {\n    group = group,\n    callback = vim.schedule_wrap(function(ev)\n      for _, win in ipairs(vim.fn.win_findbuf(ev.buf)) do\n        if states[win] then\n          states[win]:update()\n        end\n      end\n    end),\n  })\n\n  -- clear scroll state when leaving the cmdline after a search with incsearch\n  vim.api.nvim_create_autocmd({ \"CmdlineLeave\" }, {\n    group = group,\n    callback = function(ev)\n      if (ev.file == \"/\" or ev.file == \"?\") and vim.o.incsearch then\n        for _, win in ipairs(vim.fn.win_findbuf(ev.buf)) do\n          State.reset(win)\n        end\n      end\n    end,\n  })\n\n  -- listen to scroll events with topline changes\n  vim.api.nvim_create_autocmd(\"WinScrolled\", {\n    group = group,\n    callback = function()\n      for win, changes in pairs(vim.v.event) do\n        win = tonumber(win)\n        if win and changes.topline ~= 0 then\n          M.check(win)\n        end\n      end\n    end,\n  })\nend\n\nfunction M.disable()\n  if not M.enabled then\n    return\n  end\n  M.enabled = false\n  states = {}\n  vim.api.nvim_del_augroup_by_name(\"snacks_scroll\")\nend\n\n--- Determines the amount of scrollable lines between two window views,\n--- taking folds and virtual lines into account.\n---@param from vim.fn.winsaveview.ret\n---@param to vim.fn.winsaveview.ret\nlocal function scroll_lines(win, from, to)\n  if from.topline == to.topline then\n    return math.abs(from.topfill - to.topfill)\n  end\n  if to.topline < from.topline then\n    from, to = to, from\n  end\n  local start_row, end_row, offset = from.topline - 1, to.topline - 1, 0\n  if from.topfill > 0 then\n    start_row = start_row + 1\n    offset = from.topfill + 1\n  end\n  if to.topfill > 0 then\n    offset = offset - to.topfill\n  end\n  if not vim.api.nvim_win_text_height then\n    return end_row - start_row + offset\n  end\n  return vim.api.nvim_win_text_height(win, { start_row = start_row, end_row = end_row }).all + offset - 1\nend\n\n--- Check if we need to animate the scroll\n---@param win number\n---@private\nfunction M.check(win)\n  local state = State.get(win)\n  if not state then\n    return\n  end\n\n  -- only animate the current window when scrollbind is enabled\n  if vim.wo[state.win].scrollbind and vim.api.nvim_get_current_win() ~= state.win then\n    state:stop()\n    return\n  end\n\n  -- if delta is 0, then we're animating.\n  -- also skip if the difference is less than the mousescroll value,\n  -- since most terminals support smooth mouse scrolling.\n  if mouse_scrolling then\n    state:stop()\n    mouse_scrolling = false\n    stats.mousescroll = stats.mousescroll + 1\n    state.current = vim.deepcopy(state.view)\n    return\n  elseif math.abs(state.view.topline - state.current.topline) <= 1 then\n    stats.skipped = stats.skipped + 1\n    state.current = vim.deepcopy(state.view)\n    return\n  end\n  stats.scrolls = stats.scrolls + 1\n\n  -- new target\n  stats.targets = stats.targets + 1\n  state.target = vim.deepcopy(state.view)\n  state:stop() -- stop any ongoing animation\n  state:wo({ virtualedit = \"all\", scrolloff = 0 })\n\n  local now = uv.hrtime()\n  local repeat_delta = (now - state.last) / 1e6\n  state.last = now\n\n  local is_repeat = repeat_delta <= config.animate_repeat.delay\n  ---@type snacks.animate.Opts\n  local opts = vim.tbl_extend(\"force\", vim.deepcopy(is_repeat and config.animate_repeat or config.animate), {\n    int = true,\n    id = (\"scroll%s%d\"):format(is_repeat and \"_repeat_\" or \"_\", win),\n    buf = state.buf,\n  })\n\n  local scrolls = 0\n  local col_from, col_to = 0, 0\n  local move_from, move_to = 0, 0\n  vim.api.nvim_win_call(state.win, function()\n    move_to = vim.fn.winline()\n    vim.fn.winrestview(state.current) -- reset to current state\n    move_from = vim.fn.winline()\n    state:update()\n    -- calculate the amount of lines to scroll, taking folds into account\n    scrolls = scroll_lines(state.win, state.current, state.target)\n    col_from = vim.fn.virtcol({ state.current.lnum, state.current.col })\n    col_to = vim.fn.virtcol({ state.target.lnum, state.target.col })\n  end)\n\n  local down = state.target.topline > state.current.topline\n    or (state.target.topline == state.current.topline and state.target.topfill < state.current.topfill)\n\n  local scrolled = 0\n\n  state.anim = Snacks.animate(0, scrolls, function(value, ctx)\n    if not state:valid() then\n      state:stop()\n      return\n    end\n\n    vim.api.nvim_win_call(win, function()\n      if ctx.done then\n        vim.fn.winrestview(state.target)\n        state:update()\n        state:stop()\n        return\n      end\n\n      local count = vim.v.count -- backup count\n      local commands = {} ---@type string[]\n\n      -- scroll\n      local scroll_target = math.floor(value)\n      local scroll = scroll_target - scrolled --[[@as number]]\n      if scroll > 0 then\n        scrolled = scrolled + scroll\n        commands[#commands + 1] = (\"%d%s\"):format(scroll, down and SCROLL_DOWN or SCROLL_UP)\n      end\n\n      -- move the cursor vertically\n      local move = math.floor(value * math.abs(move_to - move_from) / scrolls) -- delta to move this step\n      local move_target = move_from + ((move_to < move_from) and -1 or 1) * move -- target line\n      commands[#commands + 1] = (\"%dH\"):format(move_target)\n\n      -- move the cursor horizontally\n      local virtcol = math.floor(col_from + (col_to - col_from) * value / scrolls)\n      commands[#commands + 1] = (\"%d|\"):format(virtcol + 1)\n\n      -- execute all commands in one go\n      vim.cmd((\"keepjumps normal! %s\"):format(table.concat(commands, \"\")))\n\n      -- restore count (see #1024)\n      if vim.v.count ~= count then\n        local cursor = vim.api.nvim_win_get_cursor(win)\n        vim.cmd((\"keepjumps normal! %dzh\"):format(count))\n        vim.api.nvim_win_set_cursor(win, cursor)\n      end\n\n      state:update()\n    end)\n  end, opts)\nend\n\n---@private\nfunction M.debug()\n  if debug_timer:is_active() then\n    return debug_timer:stop()\n  end\n  local last = {}\n  debug_timer:start(50, 50, function()\n    local data = vim.tbl_extend(\"force\", { stats = stats }, states)\n    for key, value in pairs(data) do\n      if not vim.deep_equal(last[key], value) then\n        Snacks.notify(vim.inspect(value), {\n          ft = \"lua\",\n          id = \"snacks_scroll_debug_\" .. key,\n          title = \"Snacks Scroll Debug \" .. key,\n        })\n      end\n    end\n    last = vim.deepcopy(data)\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/statuscolumn.lua",
    "content": "local Snacks = require(\"snacks\")\n\n---@class snacks.statuscolumn\n---@overload fun(): string\nlocal M = setmetatable({}, {\n  __call = function(t)\n    return t.get()\n  end,\n})\n\nM.meta = {\n  desc = \"Pretty status column\",\n  needs_setup = true,\n}\n\n---@class snacks.statuscolumn.FoldInfo\n---@field start number Line number where deepest fold starts\n---@field level number Fold level, when zero other fields are N/A\n---@field llevel number Lowest level that starts in v:lnum\n---@field lines number Number of lines from v:lnum to end of closed fold\n\n---@type ffi.namespace*\nlocal C\n\nlocal function _ffi()\n  if not C then\n    local ffi = require(\"ffi\")\n    ffi.cdef([[\n      typedef struct {} Error;\n      typedef struct {} win_T;\n      typedef struct {\n        int start;  // line number where deepest fold starts\n        int level;  // fold level, when zero other fields are N/A\n        int llevel; // lowest level that starts in v:lnum\n        int lines;  // number of lines from v:lnum to end of closed fold\n      } foldinfo_T;\n      foldinfo_T fold_info(win_T* wp, int lnum);\n      win_T *find_window_by_handle(int Window, Error *err);\n    ]])\n    C = ffi.C\n  end\n  return C\nend\n\n-- Returns fold info for a given window and line number\n---@param win number\n---@param lnum number\nlocal function fold_info(win, lnum)\n  pcall(_ffi)\n  if not C then\n    return\n  end\n  local ffi = require(\"ffi\")\n  local err = ffi.new(\"Error\")\n  local wp = C.find_window_by_handle(win, err)\n  if wp == nil then\n    return\n  end\n  return C.fold_info(wp, lnum) ---@type snacks.statuscolumn.FoldInfo\nend\n\n---@alias snacks.statuscolumn.Component \"mark\"|\"sign\"|\"fold\"|\"git\"\n---@alias snacks.statuscolumn.Components snacks.statuscolumn.Component[]|fun(win:number,buf:number,lnum:number):snacks.statuscolumn.Component[]\n---@alias snacks.statuscolumn.Wanted table<snacks.statuscolumn.Component, boolean>\n\n---@class snacks.statuscolumn.Config\n---@field left snacks.statuscolumn.Components\n---@field right snacks.statuscolumn.Components\n---@field enabled? boolean\nlocal defaults = {\n  left = { \"mark\", \"sign\" }, -- priority of signs on the left (high to low)\n  right = { \"fold\", \"git\" }, -- priority of signs on the right (high to low)\n  folds = {\n    open = false, -- show open fold icons\n    git_hl = false, -- use Git Signs hl for fold icons\n  },\n  git = {\n    -- patterns to match Git signs\n    patterns = { \"GitSign\", \"MiniDiffSign\" },\n  },\n  refresh = 50, -- refresh at most every 50ms\n}\n\nlocal config = Snacks.config.get(\"statuscolumn\", defaults)\n\n---@private\n---@alias snacks.statuscolumn.Sign.type \"mark\"|\"sign\"|\"fold\"|\"git\"\n---@alias snacks.statuscolumn.Sign {name:string, text:string, texthl:string, priority:number, type:snacks.statuscolumn.Sign.type}\n\n-- Cache for signs per buffer and line\n---@type table<number,table<number,snacks.statuscolumn.Sign[]>>\nlocal sign_cache = {}\nlocal cache = {} ---@type table<string,string>\nlocal icon_cache = {} ---@type table<string,string>\n\nlocal did_setup = false\n\n---@private\nfunction M.setup()\n  if did_setup then\n    return\n  end\n  did_setup = true\n  Snacks.util.set_hl({\n    Mark = \"DiagnosticHint\",\n  }, { prefix = \"SnacksStatusColumn\", default = true })\n  local timer = assert((vim.uv or vim.loop).new_timer())\n  timer:start(config.refresh, config.refresh, function()\n    sign_cache = {}\n    cache = {}\n  end)\nend\n\n---@private\n---@param name string\nfunction M.is_git_sign(name)\n  for _, pattern in ipairs(config.git.patterns) do\n    if name:find(pattern) then\n      return true\n    end\n  end\nend\n\n-- Returns a list of regular and extmark signs sorted by priority (low to high)\n---@private\n---@param wanted snacks.statuscolumn.Wanted\n---@return table<number, snacks.statuscolumn.Sign[]>\n---@param buf number\nfunction M.buf_signs(buf, wanted)\n  -- Get regular signs\n  ---@type table<number, snacks.statuscolumn.Sign[]>\n  local signs = {}\n\n  if wanted.git or wanted.sign then\n    if vim.fn.has(\"nvim-0.10\") == 0 then\n      -- Only needed for Neovim <0.10\n      -- Newer versions include legacy signs in nvim_buf_get_extmarks\n      for _, sign in ipairs(vim.fn.sign_getplaced(buf, { group = \"*\" })[1].signs) do\n        local ret = vim.fn.sign_getdefined(sign.name)[1] --[[@as snacks.statuscolumn.Sign]]\n        if ret then\n          ret.priority = sign.priority\n          ret.type = M.is_git_sign(sign.name) and \"git\" or \"sign\"\n          signs[sign.lnum] = signs[sign.lnum] or {}\n          if wanted[ret.type] then\n            table.insert(signs[sign.lnum], ret)\n          end\n        end\n      end\n    end\n\n    -- Get extmark signs\n    local extmarks = vim.api.nvim_buf_get_extmarks(buf, -1, 0, -1, { details = true, type = \"sign\" })\n    for _, extmark in pairs(extmarks) do\n      local lnum = extmark[2] + 1\n      signs[lnum] = signs[lnum] or {}\n      local name = extmark[4].sign_hl_group or extmark[4].sign_name or \"\"\n      local ret = {\n        name = name,\n        type = M.is_git_sign(name) and \"git\" or \"sign\",\n        text = extmark[4].sign_text,\n        texthl = extmark[4].sign_hl_group,\n        priority = extmark[4].priority,\n      }\n      if wanted[ret.type] then\n        table.insert(signs[lnum], ret)\n      end\n    end\n  end\n\n  -- Add marks\n  if wanted.mark then\n    local marks = vim.fn.getmarklist(buf)\n    vim.list_extend(marks, vim.fn.getmarklist())\n    for _, mark in ipairs(marks) do\n      if mark.pos[1] == buf and mark.mark:match(\"[a-zA-Z]\") then\n        local lnum = mark.pos[2]\n        signs[lnum] = signs[lnum] or {}\n        table.insert(signs[lnum], { text = mark.mark:sub(2), texthl = \"SnacksStatusColumnMark\", type = \"mark\" })\n      end\n    end\n  end\n\n  return signs\nend\n\n-- Returns a list of regular and extmark signs sorted by priority (high to low)\n---@private\n---@param win number\n---@param buf number\n---@param lnum number\n---@param wanted snacks.statuscolumn.Wanted\n---@return snacks.statuscolumn.Sign[]\nfunction M.line_signs(win, buf, lnum, wanted)\n  local buf_signs = sign_cache[buf]\n  if not buf_signs then\n    buf_signs = M.buf_signs(buf, wanted)\n    sign_cache[buf] = buf_signs\n  end\n  local signs = buf_signs[lnum] or {}\n\n  -- Get fold signs\n  if wanted.fold then\n    local info = fold_info(win, lnum)\n    if info and info.level > 0 then\n      if info.lines > 0 then\n        signs[#signs + 1] = { text = vim.opt.fillchars:get().foldclose or \"\", texthl = \"Folded\", type = \"fold\" }\n      elseif config.folds.open and info.start == lnum then\n        signs[#signs + 1] = { text = vim.opt.fillchars:get().foldopen or \"\", type = \"fold\" }\n      end\n    end\n  end\n\n  -- Sort by priority\n  table.sort(signs, function(a, b)\n    return (a.priority or 0) > (b.priority or 0)\n  end)\n  return signs\nend\n\n---@private\n---@param sign? snacks.statuscolumn.Sign\nfunction M.icon(sign)\n  if not sign then\n    return \"  \"\n  end\n  local key = (sign.text or \"\") .. (sign.texthl or \"\")\n  if icon_cache[key] then\n    return icon_cache[key]\n  end\n  local text = vim.fn.strcharpart(sign.text or \"\", 0, 2) ---@type string\n  text = text .. string.rep(\" \", 2 - vim.fn.strchars(text))\n  icon_cache[key] = sign.texthl and (\"%#\" .. sign.texthl .. \"#\" .. text .. \"%*\") or text\n  return icon_cache[key]\nend\n\n---@return string\nfunction M._get()\n  if not did_setup then\n    M.setup()\n  end\n  local win = vim.g.statusline_winid\n  local nu = vim.wo[win].number\n  local rnu = vim.wo[win].relativenumber\n  local show_signs = vim.v.virtnum == 0 and vim.wo[win].signcolumn ~= \"no\"\n  local show_folds = vim.v.virtnum == 0 and vim.wo[win].foldcolumn ~= \"0\"\n  local buf = vim.api.nvim_win_get_buf(win)\n  local left_c = type(config.left) == \"function\" and config.left(win, buf, vim.v.lnum) or config.left --[[@as snacks.statuscolumn.Component[] ]]\n  local right_c = type(config.right) == \"function\" and config.right(win, buf, vim.v.lnum) or config.right --[[@as snacks.statuscolumn.Component[] ]]\n\n  ---@type snacks.statuscolumn.Wanted\n  local wanted = { sign = show_signs }\n  for _, c in ipairs(left_c) do\n    wanted[c] = wanted[c] ~= false\n  end\n  for _, c in ipairs(right_c) do\n    wanted[c] = wanted[c] ~= false\n  end\n\n  local components = { \"\", \"\", \"\" } -- left, middle, right\n  if not (show_signs or nu or rnu) then\n    return \"\"\n  end\n\n  if (nu or rnu) and vim.v.virtnum == 0 then\n    local num ---@type number\n    if rnu and nu and vim.v.relnum == 0 then\n      num = vim.v.lnum\n    elseif rnu then\n      num = vim.v.relnum\n    else\n      num = vim.v.lnum\n    end\n    components[2] = \"%=\" .. num .. \" \"\n  end\n\n  if show_signs or show_folds then\n    local signs = M.line_signs(win, buf, vim.v.lnum, wanted)\n\n    if #signs > 0 then\n      local signs_by_type = {} ---@type table<snacks.statuscolumn.Sign.type,snacks.statuscolumn.Sign>\n      for _, s in ipairs(signs) do\n        signs_by_type[s.type] = signs_by_type[s.type] or s\n      end\n\n      ---@param types snacks.statuscolumn.Sign.type[]\n      local function find(types)\n        for _, t in ipairs(types) do\n          if signs_by_type[t] then\n            return signs_by_type[t]\n          end\n        end\n      end\n\n      local left, right = find(left_c), find(right_c)\n\n      if config.folds.git_hl then\n        local git = signs_by_type.git\n        if git and left and left.type == \"fold\" then\n          left.texthl = git.texthl\n        end\n        if git and right and right.type == \"fold\" then\n          right.texthl = git.texthl\n        end\n      end\n      components[1] = left and M.icon(left) or \"  \" -- left\n      components[3] = right and M.icon(right) or \"  \" -- right\n    else\n      components[1] = \"  \"\n      components[3] = \"  \"\n    end\n  end\n  components[1] = vim.b[buf].snacks_statuscolumn_left ~= false and components[1] or \"\"\n  components[3] = vim.b[buf].snacks_statuscolumn_right ~= false and components[3] or \"\"\n\n  local ret = table.concat(components, \"\")\n  return \"%@v:lua.require'snacks.statuscolumn'.click_fold@\" .. ret .. \"%T\"\nend\n\nfunction M.get()\n  local win = vim.g.statusline_winid\n  local buf = vim.api.nvim_win_get_buf(win)\n  local key = (\"%d:%d:%d:%d:%d\"):format(win, buf, vim.v.lnum, vim.v.virtnum ~= 0 and 1 or 0, vim.v.relnum)\n  if cache[key] then\n    return cache[key]\n  end\n  local ok, ret = pcall(M._get)\n  if ok then\n    cache[key] = ret\n    return ret\n  end\n  return \"\"\nend\n\n---@private\nfunction M.health()\n  local ready = vim.o.statuscolumn:find(\"snacks.statuscolumn\", 1, true)\n  if config.enabled and not ready then\n    Snacks.health.warn((\"is not configured\\n- `vim.o.statuscolumn = %q`\"):format(vim.o.statuscolumn))\n  elseif not config.enabled and ready then\n    Snacks.health.ok((\"is manually configured\\n- `vim.o.statuscolumn = %q`\"):format(vim.o.statuscolumn))\n  end\nend\n\nfunction M.click_fold()\n  local pos = vim.fn.getmousepos()\n  vim.api.nvim_win_set_cursor(pos.winid, { pos.line, 1 })\n  vim.api.nvim_win_call(pos.winid, function()\n    if vim.fn.foldlevel(pos.line) > 0 then\n      vim.cmd(\"normal! za\")\n    end\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/terminal.lua",
    "content": "---@class snacks.terminal: snacks.win\n---@field cmd? string | string[]\n---@field opts snacks.terminal.Opts\n---@overload fun(cmd?: string|string[], opts?: snacks.terminal.Opts): snacks.terminal\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.toggle(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Create and toggle floating/split terminals\",\n}\n\n---@class snacks.terminal.Config\n---@field win? snacks.win.Config|{}\n---@field shell? string|string[] The shell to use. Defaults to `vim.o.shell`\n---@field override? fun(cmd?: string|string[], opts?: snacks.terminal.Opts) Use this to use a different terminal implementation\nlocal defaults = {\n  win = { style = \"terminal\" },\n}\n\n---@class snacks.terminal.Opts: snacks.terminal.Config\n---@field cwd? string\n---@field count? integer\n---@field env? table<string, string>\n---@field start_insert? boolean start insert mode when starting the terminal\n---@field auto_insert? boolean start insert mode when entering the terminal buffer\n---@field auto_close? boolean close the terminal buffer when the process exits\n---@field interactive? boolean shortcut for `start_insert`, `auto_close` and `auto_insert` (default: true)\n\nSnacks.config.style(\"terminal\", {\n  bo = {\n    filetype = \"snacks_terminal\",\n  },\n  wo = {},\n  stack = true, -- when enabled, multiple split windows with the same position will be stacked together (useful for terminals)\n  keys = {\n    q = \"hide\",\n    gf = function(self)\n      local f = vim.fn.findfile(vim.fn.expand(\"<cfile>\"), \"**\")\n      if f == \"\" then\n        Snacks.notify.warn(\"No file under cursor\")\n      else\n        self:hide()\n        vim.schedule(function()\n          vim.cmd(\"e \" .. f)\n        end)\n      end\n    end,\n    term_normal = {\n      \"<esc>\",\n      function(self)\n        self.esc_timer = self.esc_timer or (vim.uv or vim.loop).new_timer()\n        if self.esc_timer:is_active() then\n          self.esc_timer:stop()\n          vim.cmd(\"stopinsert\")\n        else\n          self.esc_timer:start(200, 0, function() end)\n          return \"<esc>\"\n        end\n      end,\n      mode = \"t\",\n      expr = true,\n      desc = \"Double escape to normal mode\",\n    },\n  },\n})\n\n---@type table<string, snacks.win>\nlocal terminals = setmetatable({}, {\n  __mode = \"v\",\n})\n\nlocal function jobstart(cmd, opts)\n  opts = opts or {}\n  local fn = vim.fn.jobstart\n  if vim.fn.termopen then\n    opts.term = nil\n    fn = vim.fn.termopen\n  end\n  return fn(cmd, vim.tbl_isempty(opts) and vim.empty_dict() or opts)\nend\n\n--- Open a new terminal window.\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nfunction M.open(cmd, opts)\n  opts = Snacks.config.get(\"terminal\", defaults --[[@as snacks.terminal.Opts]], opts)\n  local id = opts.count or vim.v.count1\n  opts.win = Snacks.win.resolve(\"terminal\", {\n    position = cmd and \"float\" or \"bottom\",\n  }, opts.win, { show = false })\n  opts = vim.deepcopy(opts)\n  opts.win.wo.winbar = opts.win.wo.winbar\n    or (opts.win.position == \"float\" and \"\" or (id .. \": %{get(b:, 'term_title', '')}\"))\n\n  if opts.override then\n    return opts.override(cmd, opts)\n  end\n\n  local interactive = opts.interactive ~= false\n  local auto_insert = opts.auto_insert or (opts.auto_insert == nil and interactive)\n  local start_insert = opts.start_insert or (opts.start_insert == nil and interactive)\n  local auto_close = opts.auto_close or (opts.auto_close == nil and interactive)\n\n  local on_buf = opts.win and opts.win.on_buf\n  ---@param self snacks.terminal\n  opts.win.on_buf = function(self)\n    self.cmd = cmd\n    vim.b[self.buf].snacks_terminal = { cmd = cmd, id = id, cwd = opts.cwd, env = opts.env }\n    if on_buf then\n      on_buf(self)\n    end\n  end\n\n  local on_win = opts.win and opts.win.on_win\n  ---@param self snacks.terminal\n  opts.win.on_win = function(self)\n    if start_insert and vim.api.nvim_get_current_buf() == self.buf then\n      vim.cmd.startinsert()\n    end\n    if on_win then\n      on_win(self)\n    end\n  end\n\n  local terminal = Snacks.win(opts.win)\n  local tid = M.tid(cmd, opts)\n  terminals[tid] = terminal\n\n  if auto_insert then\n    terminal:on(\"BufEnter\", function()\n      vim.cmd.startinsert()\n    end, { buf = true })\n  end\n\n  if auto_close then\n    terminal:on(\"TermClose\", function()\n      if type(vim.v.event) == \"table\" and vim.v.event.status ~= 0 then\n        Snacks.notify.error(\"Terminal exited with code \" .. vim.v.event.status .. \".\\nCheck for any errors.\")\n        return\n      end\n      terminal:close()\n      vim.cmd.checktime()\n    end, { buf = true })\n  end\n\n  terminal:on(\"ExitPre\", function()\n    terminal:close()\n  end)\n\n  terminal:on(\"BufWipeout\", function()\n    terminals[tid] = nil\n    vim.schedule(function()\n      terminal:close()\n    end)\n  end, { buf = true })\n\n  terminal:show()\n  vim.api.nvim_buf_call(terminal.buf, function()\n    jobstart(cmd or M.parse(opts.shell or vim.o.shell), {\n      cwd = opts.cwd,\n      env = opts.env,\n      term = true,\n    })\n  end)\n\n  vim.cmd(\"noh\")\n  return terminal\nend\n\n--- Get a terminal id based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nfunction M.tid(cmd, opts)\n  opts = opts or {}\n  return vim.inspect({\n    cmd = type(cmd) == \"table\" and cmd or { cmd },\n    cwd = opts.cwd or vim.fn.getcwd(0),\n    env = opts.env,\n    count = opts.count or vim.v.count1,\n  })\nend\n\n--- Get or create a terminal window.\n--- The terminal id is based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n--- `opts.create` defaults to `true`.\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts| {create?: boolean}\n---@return snacks.win? terminal, boolean? created\nfunction M.get(cmd, opts)\n  opts = opts or {}\n  local id = M.tid(cmd, opts)\n  local created = false\n  if not (terminals[id] and terminals[id]:buf_valid()) and (opts.create ~= false) then\n    local ret = M.open(cmd, opts)\n    ret:on(\"BufWipeout\", function()\n      terminals[id] = nil\n    end, { buf = true })\n    assert(terminals[id], \"Terminal was not created\")\n    created = true\n  end\n  return terminals[id], created\nend\n\n---@return snacks.win[]\nfunction M.list()\n  return vim.tbl_filter(function(t)\n    return t:buf_valid()\n  end, terminals)\nend\n\n--- Toggle a terminal window.\n--- The terminal id is based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nfunction M.toggle(cmd, opts)\n  local terminal, created = M.get(cmd, opts)\n  return created and terminal or assert(terminal):toggle()\nend\n\n--- Focus a terminal window. If already focused, hide it.\n--- The terminal id is based on the `cmd`, `cwd`, `env` and `vim.v.count1` options.\n---@param cmd? string | string[]\n---@param opts? snacks.terminal.Opts\nfunction M.focus(cmd, opts)\n  local terminal, created = M.get(cmd, opts)\n  if terminal and not created and vim.api.nvim_get_current_buf() == terminal.buf then\n    terminal:hide()\n    return terminal, created\n  end\n  return created and terminal or assert(terminal):show():focus()\nend\n\n--- Parses a shell command into a table of arguments.\n--- - spaces inside quotes (only double quotes are supported) are preserved\n--- - backslash\n---@private\n---@param cmd string|string[]\n---@return string[]\nfunction M.parse(cmd)\n  if type(cmd) == \"table\" then\n    return cmd\n  end\n  local args = {}\n  local in_quotes, escape_next, current = false, false, \"\"\n  local function add()\n    if #current > 0 then\n      table.insert(args, current)\n      current = \"\"\n    end\n  end\n\n  for i = 1, #cmd do\n    local char = cmd:sub(i, i)\n    if escape_next then\n      current = current .. ((char == '\"' or char == \"\\\\\") and \"\" or \"\\\\\") .. char\n      escape_next = false\n    elseif char == \"\\\\\" and in_quotes then\n      escape_next = true\n    elseif char == '\"' then\n      in_quotes = not in_quotes\n    elseif char:find(\"[ \\t]\") and not in_quotes then\n      add()\n    else\n      current = current .. char\n    end\n  end\n  add()\n  return args\nend\n\n--- Colorize the current buffer.\n--- Replaces ansii color codes with the actual colors.\n---\n--- Example:\n---\n--- ```sh\n--- ls -la --color=always | nvim - -c \"lua Snacks.terminal.colorize()\"\n--- ```\nfunction M.colorize()\n  vim.wo.number = false\n  vim.wo.relativenumber = false\n  vim.wo.statuscolumn = \"\"\n  vim.wo.signcolumn = \"no\"\n  vim.opt.listchars = { space = \" \" }\n\n  local buf = vim.api.nvim_get_current_buf()\n\n  local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)\n  while #lines > 0 and vim.trim(lines[#lines]) == \"\" do\n    lines[#lines] = nil\n  end\n  vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})\n\n  vim.api.nvim_chan_send(vim.api.nvim_open_term(buf, {}), table.concat(lines, \"\\r\\n\"))\n  vim.keymap.set(\"n\", \"q\", \"<cmd>q<cr>\", { silent = true, buffer = buf })\n  vim.api.nvim_create_autocmd(\"TextChanged\", {\n    buffer = buf,\n    callback = function()\n      pcall(vim.api.nvim_win_set_cursor, 0, { #lines, 0 })\n    end,\n  })\n  vim.api.nvim_create_autocmd(\"TermEnter\", { buffer = buf, command = \"stopinsert\" })\nend\n\n---@private\nfunction M.health()\n  local opts = Snacks.config.get(\"terminal\", defaults --[[@as snacks.terminal.Opts]])\n  local cmd = M.parse(opts.shell or vim.o.shell)\n  local ok = cmd[1] and (vim.fn.executable(cmd[1]) == 1)\n  local msg = (\"shell %s\\n- `vim.o.shell`: %s\\n- `parsed`: %s\"):format(\n    ok and \"configured\" or \"not found\",\n    vim.o.shell,\n    vim.inspect(cmd)\n  )\n  Snacks.health[ok and \"ok\" or \"error\"](msg)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/toggle.lua",
    "content": "---@class snacks.toggle\n---@overload fun(... :snacks.toggle.Opts): snacks.toggle.Class\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.new(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Toggle keymaps integrated with which-key icons / colors\",\n}\n\n---@class snacks.toggle.Config\n---@field icon? string|{ enabled: string, disabled: string }\n---@field color? string|{ enabled: string, disabled: string }\n---@field wk_desc? string|{ enabled: string, disabled: string }\n---@field map? fun(mode: string|string[], lhs: string, rhs: string|fun(), opts?: vim.keymap.set.Opts)\n---@field which_key? boolean\n---@field notify? boolean|fun(state:boolean, opts: snacks.toggle.Opts)\nlocal defaults = {\n  map = vim.keymap.set, -- keymap.set function to use\n  which_key = true, -- integrate with which-key to show enabled/disabled icons and colors\n  notify = true, -- show a notification when toggling\n  -- icons for enabled/disabled states\n  icon = {\n    enabled = \" \",\n    disabled = \" \",\n  },\n  -- colors for enabled/disabled states\n  color = {\n    enabled = \"green\",\n    disabled = \"yellow\",\n  },\n  wk_desc = {\n    enabled = \"Disable \",\n    disabled = \"Enable \",\n  },\n}\n\n---@type table<string, snacks.toggle.Class>\nM.toggles = {}\n\n---@class snacks.toggle.Opts: snacks.toggle.Config\n---@field id? string\n---@field name string\n---@field get fun():boolean\n---@field set fun(state:boolean)\n\n---@class snacks.toggle.Class\n---@field opts snacks.toggle.Opts\nlocal Toggle = {}\nToggle.__index = Toggle\n\n---@param ... snacks.toggle.Opts\nfunction M.new(...)\n  ---@type snacks.toggle.Class\n  local self = setmetatable({}, Toggle)\n  self.opts = Snacks.config.get(\"toggle\", defaults, ...) --[[@as snacks.toggle.Opts]]\n  local id = self.opts.id or self.opts.name:lower():gsub(\"%W+\", \"_\"):gsub(\"_+$\", \"\"):gsub(\"^_+\", \"\")\n  self.opts.id = id\n  M.toggles[id] = self\n  return self\nend\n\n---@param id string\n---@return snacks.toggle.Class?\nfunction M.get(id)\n  if not M.toggles[id] and M[id] then\n    M[id]()\n  end\n  return M.toggles[id]\nend\n\nfunction Toggle:get()\n  local ok, ret = pcall(self.opts.get)\n  if not ok then\n    Snacks.notify.error({\n      \"Failed to get state for `\" .. self.opts.name .. \"`:\\n\",\n      ret --[[@as string]],\n    }, { title = self.opts.name, once = true })\n    return false\n  end\n  return ret\nend\n\n---@param state boolean\nfunction Toggle:set(state)\n  local ok, err = pcall(self.opts.set, state) ---@type boolean, string?\n  if not ok then\n    Snacks.notify.error({\n      \"Failed to set state for `\" .. self.opts.name .. \"`:\\n\",\n      err --[[@as string]],\n    }, { title = self.opts.name, once = true })\n  end\nend\n\nfunction Toggle:toggle()\n  local state = not self:get()\n  self:set(state)\n  if not self.opts.notify then\n    return\n  end\n  if type(self.opts.notify) == \"function\" then\n    self.opts.notify(state, self.opts)\n    return\n  end\n  Snacks.notify(\n    (state and \"Enabled\" or \"Disabled\") .. \" **\" .. self.opts.name .. \"**\",\n    { title = self.opts.name, level = state and vim.log.levels.INFO or vim.log.levels.WARN }\n  )\nend\n\n---@param keys string\n---@param opts? vim.keymap.set.Opts | { mode: string|string[]}\nfunction Toggle:map(keys, opts)\n  opts = opts or {}\n  local mode = opts.mode or \"n\"\n  opts.mode = nil\n  opts.desc = opts.desc or (\"Toggle \" .. self.opts.name)\n  self.opts.map(mode, keys, function()\n    self:toggle()\n  end, opts)\n  if self.opts.which_key then\n    Snacks.util.on_module(\"which-key\", function()\n      self:_wk(keys, mode)\n    end)\n  end\n  return self\nend\n\nfunction Toggle:_wk(keys, mode)\n  require(\"which-key\").add({\n    {\n      keys,\n      mode = mode,\n      real = true,\n      icon = function()\n        local key = self:get() and \"enabled\" or \"disabled\"\n        return {\n          icon = type(self.opts.icon) == \"string\" and self.opts.icon or self.opts.icon[key],\n          color = type(self.opts.color) == \"string\" and self.opts.color or self.opts.color[key],\n        }\n      end,\n      desc = function()\n        local key = self:get() and \"enabled\" or \"disabled\"\n        return (type(self.opts.wk_desc) == \"string\" and self.opts.wk_desc or self.opts.wk_desc[key]) .. self.opts.name\n      end,\n    },\n  })\nend\n\n---@param option string\n---@param opts? snacks.toggle.Config | {on?: unknown, off?: unknown, global?: boolean}\nfunction M.option(option, opts)\n  opts = opts or {}\n  local on = opts.on == nil and true or opts.on\n  local off = opts.off ~= nil and opts.off or false\n  return M.new({\n    id = option,\n    name = option,\n    get = function()\n      return vim.api.nvim_get_option_value(option, { scope = opts.global and \"global\" or \"local\" }) == on\n    end,\n    set = function(state)\n      local value = state and on or off\n      vim.api.nvim_set_option_value(option, value, { scope = opts.global and \"global\" or \"local\" })\n    end,\n  }, opts)\nend\n\n---@param opts? snacks.toggle.Config\nfunction M.treesitter(opts)\n  return M.new({\n    id = \"treesitter\",\n    name = \"Treesitter Highlight\",\n    get = function()\n      return vim.b.ts_highlight\n    end,\n    set = function(state)\n      vim.treesitter[state and \"start\" or \"stop\"]()\n    end,\n  }, opts)\nend\n\n---@param opts? snacks.toggle.Config\nfunction M.line_number(opts)\n  local number, relativenumber = true, true\n  return M.new({\n    id = \"line_number\",\n    name = \"Line Numbers\",\n    get = function()\n      return vim.opt_local.number:get() or vim.opt_local.relativenumber:get()\n    end,\n    set = function(state)\n      if state then\n        vim.opt_local.number, vim.opt_local.relativenumber = number, relativenumber\n      else\n        number, relativenumber = vim.opt_local.number:get(), vim.opt_local.relativenumber:get()\n        vim.opt_local.number, vim.opt_local.relativenumber = false, false\n      end\n    end,\n  }, opts)\nend\n\n---@param opts? snacks.toggle.Config\nfunction M.inlay_hints(opts)\n  return M.new({\n    id = \"inlay_hints\",\n    name = \"Inlay Hints\",\n    get = function()\n      return vim.lsp.inlay_hint.is_enabled({ bufnr = 0 })\n    end,\n    set = function(state)\n      vim.lsp.inlay_hint.enable(state, { bufnr = 0 })\n    end,\n  }, opts)\nend\n\n---@param opts? snacks.toggle.Config\nfunction M.diagnostics(opts)\n  return M.new({\n    id = \"diagnostics\",\n    name = \"Diagnostics\",\n    get = function()\n      local enabled = false\n      if vim.diagnostic.is_enabled then\n        enabled = vim.diagnostic.is_enabled()\n      elseif vim.diagnostic.is_disabled then\n        enabled = not vim.diagnostic.is_disabled()\n      end\n      return enabled\n    end,\n    set = function(state)\n      if vim.fn.has(\"nvim-0.10\") == 0 then\n        if state then\n          pcall(vim.diagnostic.enable)\n        else\n          pcall(vim.diagnostic.disable)\n        end\n      else\n        vim.diagnostic.enable(state)\n      end\n    end,\n  }, opts)\nend\n\n---@private\nfunction M.health()\n  local ok = pcall(require, \"which-key\")\n  Snacks.health[ok and \"ok\" or \"warn\"]((\"{which-key} is %s\"):format(ok and \"installed\" or \"not installed\"))\nend\n\nfunction M.profiler()\n  return M.new({\n    id = \"profiler\",\n    name = \"Profiler\",\n    get = function()\n      return Snacks.profiler.running()\n    end,\n    set = function(state)\n      if state then\n        Snacks.profiler.start()\n      else\n        Snacks.profiler.stop()\n      end\n    end,\n  })\nend\n\nfunction M.profiler_highlights()\n  return M.new({\n    id = \"profiler_highlights\",\n    name = \"Profiler Highlights\",\n    get = function()\n      return Snacks.profiler.ui.enabled\n    end,\n    set = function(state)\n      if state then\n        Snacks.profiler.ui.show()\n      else\n        Snacks.profiler.ui.hide()\n      end\n    end,\n  })\nend\n\nfunction M.indent()\n  return M.new({\n    id = \"indent\",\n    name = \"Indent Guides\",\n    get = function()\n      return Snacks.indent.enabled\n    end,\n    set = function(state)\n      if state then\n        Snacks.indent.enable()\n      else\n        Snacks.indent.disable()\n      end\n    end,\n  })\nend\n\nfunction M.dim()\n  return M.new({\n    id = \"dim\",\n    name = \"Dimming\",\n    get = function()\n      return Snacks.dim.enabled\n    end,\n    set = function(state)\n      if state then\n        Snacks.dim.enable()\n      else\n        Snacks.dim.disable()\n      end\n    end,\n  })\nend\n\nfunction M.words()\n  return M.new({\n    id = \"words\",\n    name = \"LSP Words\",\n    get = function()\n      return Snacks.words.enabled\n    end,\n    set = function(state)\n      if state then\n        Snacks.words.enable()\n      else\n        Snacks.words.disable()\n      end\n    end,\n  })\nend\n\nfunction M.scroll()\n  return M.new({\n    id = \"scroll\",\n    name = \"Smooth Scroll\",\n    get = function()\n      return Snacks.scroll.enabled\n    end,\n    set = function(state)\n      if state then\n        Snacks.scroll.enable()\n      else\n        Snacks.scroll.disable()\n      end\n    end,\n  })\nend\n\nfunction M.zen()\n  return M.new({\n    id = \"zen\",\n    name = \"Zen Mode\",\n    get = function()\n      return Snacks.zen.win and Snacks.zen.win:valid() or false\n    end,\n    set = function(state)\n      if state then\n        Snacks.zen()\n      elseif Snacks.zen.win then\n        Snacks.zen.win:close()\n      end\n    end,\n  })\nend\n\nfunction M.zoom()\n  return M.new({\n    id = \"zoom\",\n    name = \"Zoom Mode\",\n    get = function()\n      return Snacks.zen.win and Snacks.zen.win:valid() or false\n    end,\n    set = function(state)\n      if state then\n        Snacks.zen.zoom()\n      elseif Snacks.zen.win then\n        Snacks.zen.win:close()\n      end\n    end,\n  })\nend\n\nfunction M.animate()\n  return M.new({\n    id = \"animate\",\n    name = \"Animations\",\n    get = function()\n      return vim.g.snacks_animate ~= false\n    end,\n    set = function(state)\n      vim.g.snacks_animate = state\n    end,\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/util/init.lua",
    "content": "---@class snacks.util\n---@field spawn snacks.spawn\n---@field lsp snacks.lsp\nlocal M = setmetatable({}, {\n  ---@param M snacks.util\n  __index = function(M, k)\n    if vim.tbl_contains({ \"spawn\", \"lsp\" }, k) then\n      M[k] = require(\"snacks.util.\" .. k)\n    end\n    return rawget(M, k)\n  end,\n})\n\nM.meta = {\n  desc = \"Utility functions for Snacks _(library)_\",\n}\n\nM.is_win = jit.os:find(\"Windows\")\n\nlocal uv = vim.uv or vim.loop\nlocal key_cache = {} ---@type table<string, string>\nlocal langs = {} ---@type table<string, boolean>\n\n---@alias snacks.util.hl table<string, string|vim.api.keyset.highlight>\n\nlocal hl_groups = {} ---@type table<string, vim.api.keyset.highlight>\nvim.api.nvim_create_autocmd(\"ColorScheme\", {\n  group = vim.api.nvim_create_augroup(\"snacks_util_hl\", { clear = true }),\n  callback = function()\n    for hl_group, hl in pairs(hl_groups) do\n      vim.api.nvim_set_hl(0, hl_group, hl)\n    end\n  end,\n})\n\n---@param lang string|number|nil\n---@overload fun(buf:number):string?\n---@overload fun(ft:string):string?\n---@return string?\nfunction M.get_lang(lang)\n  lang = type(lang) == \"number\" and vim.bo[lang].filetype or lang --[[@as string?]]\n  lang = lang and vim.treesitter.language.get_lang(lang) or lang\n  if lang and lang ~= \"\" and langs[lang] == nil then\n    local ok, ret = pcall(vim.treesitter.language.add, lang)\n    langs[lang] = (ok and ret) or (ok and vim.fn.has(\"nvim-0.11\") == 0)\n  end\n  return langs[lang] and lang or nil\nend\n\n--- Ensures the hl groups are always set, even after a colorscheme change.\n---@param groups snacks.util.hl\n---@param opts? { prefix?:string, default?:boolean, managed?:boolean }\nfunction M.set_hl(groups, opts)\n  opts = opts or {}\n  for hl_group, hl in pairs(groups) do\n    hl_group = opts.prefix and opts.prefix .. hl_group or hl_group\n    hl = type(hl) == \"string\" and { link = hl } or hl --[[@as vim.api.keyset.highlight]]\n    hl.default = opts.default\n    if opts.managed ~= false then\n      hl_groups[hl_group] = hl\n    end\n    vim.api.nvim_set_hl(0, hl_group, hl)\n  end\nend\n\n---@param group string|string[] hl group to get color from\n---@param prop? string property to get. Defaults to \"fg\"\nfunction M.color(group, prop)\n  prop = prop or \"fg\"\n  group = type(group) == \"table\" and group or { group }\n  ---@cast group string[]\n  for _, g in ipairs(group) do\n    local hl = vim.api.nvim_get_hl(0, { name = g, link = false, create = false })\n    if hl[prop] then\n      return string.format(\"#%06x\", hl[prop])\n    end\n  end\nend\n\n--- Set window-local options.\n---@param win number\n---@param wo vim.wo|{}|{winhighlight: string|table<string, string>}\nfunction M.wo(win, wo)\n  for k, v in pairs(wo or {}) do\n    if k == \"winhighlight\" and type(v) == \"table\" then\n      local parts = {} ---@type string[]\n      for kk, vv in pairs(v) do\n        if vv ~= \"\" then\n          parts[#parts + 1] = (\"%s:%s\"):format(kk, vv)\n        end\n      end\n      v = table.concat(parts, \",\")\n    end\n    vim.api.nvim_set_option_value(k, v, { scope = \"local\", win = win })\n  end\nend\n\n--- Set buffer-local options.\n---@param buf number\n---@param bo vim.bo|{}\nfunction M.bo(buf, bo)\n  for k, v in pairs(bo or {}) do\n    vim.api.nvim_set_option_value(k, v, { buf = buf })\n  end\nend\n\n--- Merges vim.wo.winhighlight options.\n--- Option values can be a string or a dictionary.\n---@param ... string|table<string, string>\nfunction M.winhl(...)\n  local ret = {} ---@type table<string, string>[]\n  for i = 1, select(\"#\", ...) do\n    local winhl = select(i, ...)\n    if type(winhl) == \"string\" then\n      winhl = vim.trim(winhl)\n      local parts = winhl == \"\" and {} or vim.split(winhl, \",\")\n      winhl = {}\n      for _, p in ipairs(parts) do\n        local k, v = p:match(\"^%s*(.-):(.-)%s*$\")\n        if k and v then\n          winhl[k] = v\n        end\n      end\n    end\n    ret[#ret + 1] = winhl\n  end\n  return Snacks.config.merge(unpack(ret))\nend\n\n--- Get an icon from `mini.icons` or `nvim-web-devicons`.\n---@param name string\n---@param cat? string \"file\"|\"filetype\"|\"extension\"|\"directory\"\n---@param opts? { fallback?: {dir?:string, file?:string} }\n---@return string, string?\nfunction M.icon(name, cat, opts)\n  opts = opts or {}\n  opts.fallback = opts.fallback or {}\n  local try = {\n    function()\n      return MiniIcons.get(cat or \"file\", name)\n    end,\n    function()\n      if cat == \"directory\" then\n        return opts.fallback.dir or \"󰉋 \", \"Directory\"\n      end\n      local Icons = require(\"nvim-web-devicons\")\n      if cat == \"filetype\" then\n        return Icons.get_icon_by_filetype(name, { default = false })\n      elseif cat == \"file\" then\n        local ext = name:match(\"%.(%w+)$\")\n        return Icons.get_icon(name, ext, { default = false }) --[[@as string, string]]\n      elseif cat == \"extension\" then\n        return Icons.get_icon(nil, name, { default = false }) --[[@as string, string]]\n      end\n    end,\n  }\n  for _, fn in ipairs(try) do\n    local ret = { pcall(fn) }\n    if ret[1] and ret[2] then\n      return ret[2], ret[3]\n    end\n  end\n  return opts.fallback.file or \"󰈔 \"\nend\n\n-- Encodes a string to be used as a file name.\n---@param str string\nfunction M.file_encode(str)\n  return str:gsub(\"([^%w%-_%.\\t ])\", function(c)\n    return string.format(\"_%%%02X\", string.byte(c))\n  end)\nend\n\n-- Decodes a file name to a string.\n---@param str string\nfunction M.file_decode(str)\n  return str:gsub(\"_%%(%x%x)\", function(hex)\n    return string.char(tonumber(hex, 16))\n  end)\nend\n\n---@param fg string foreground color\n---@param bg string background color\n---@param alpha number number between 0 and 1. 0 results in bg, 1 results in fg\nfunction M.blend(fg, bg, alpha)\n  local bg_rgb = { tonumber(bg:sub(2, 3), 16), tonumber(bg:sub(4, 5), 16), tonumber(bg:sub(6, 7), 16) }\n  local fg_rgb = { tonumber(fg:sub(2, 3), 16), tonumber(fg:sub(4, 5), 16), tonumber(fg:sub(6, 7), 16) }\n  local blend = function(i)\n    local ret = (alpha * fg_rgb[i] + ((1 - alpha) * bg_rgb[i]))\n    return math.floor(math.min(math.max(0, ret), 255) + 0.5)\n  end\n  return string.format(\"#%02x%02x%02x\", blend(1), blend(2), blend(3))\nend\n\nlocal transparent ---@type boolean?\n--- Check if the colorscheme is transparent.\nfunction M.is_transparent()\n  if transparent == nil then\n    transparent = M.color(\"Normal\", \"bg\") == nil\n    vim.api.nvim_create_autocmd(\"ColorScheme\", {\n      group = vim.api.nvim_create_augroup(\"snacks_util_transparent\", { clear = true }),\n      callback = function()\n        transparent = nil\n      end,\n    })\n  end\n  return transparent\nend\n\n--- Redraw the range of lines in the window.\n--- Optimized for Neovim >= 0.10\n---@param win number\n---@param from number -- 1-indexed, inclusive\n---@param to number -- 1-indexed, inclusive\nfunction M.redraw_range(win, from, to)\n  if vim.api.nvim__redraw and vim.api.nvim_win_is_valid(win) then\n    vim.api.nvim__redraw({ win = win, range = { math.floor(from - 1), math.floor(to) }, valid = true, flush = false })\n  else\n    vim.cmd([[redraw!]])\n  end\nend\n\n--- Redraw the window.\n--- Optimized for Neovim >= 0.10\n---@param win number\nfunction M.redraw(win)\n  if vim.api.nvim__redraw then\n    vim.api.nvim__redraw({ win = win, valid = false, flush = false })\n  else\n    vim.cmd([[redraw!]])\n  end\nend\n\nlocal mod_timer = assert(uv.new_timer())\nlocal mod_cb = {} ---@type table<string, fun(modname:string)[]>\n\n---@return boolean waiting\nlocal function mod_check()\n  for modname, cbs in pairs(mod_cb) do\n    if package.loaded[modname] then\n      mod_cb[modname] = nil\n      for _, cb in ipairs(cbs) do\n        cb(modname)\n      end\n    end\n  end\n  return next(mod_cb) ~= nil\nend\n\n--- Call a function when a module is loaded.\n--- The callback is called immediately if the module is already loaded.\n--- Otherwise, it is called when the module is loaded.\n---@param modname string\n---@param cb fun(modname:string)\nfunction M.on_module(modname, cb)\n  mod_cb[modname] = mod_cb[modname] or {}\n  table.insert(mod_cb[modname], cb)\n  if mod_check() then\n    mod_timer:start(\n      100,\n      100,\n      vim.schedule_wrap(function()\n        return not mod_check() and mod_timer:stop()\n      end)\n    )\n  end\nend\n\n---@param str string\nfunction M.keycode(str)\n  return vim.api.nvim_replace_termcodes(str, true, true, true)\nend\n\n--- Get a buffer or global variable.\n---@generic T\n---@param buf? number\n---@param name string\n---@param default? T\n---@return T\nfunction M.var(buf, name, default)\n  local ok, ret = pcall(function()\n    return vim.b[buf or 0][name]\n  end)\n  if ok and ret ~= nil then\n    return ret\n  end\n  ret = vim.g[name]\n  if ret ~= nil then\n    return ret\n  end\n  return default\nend\n\nlocal keys = {} ---@type table<string, fun(key:string)[]>\nlocal on_key_ns ---@type number?\n\n---@param key string\n---@param cb fun(key:string)\nfunction M.on_key(key, cb)\n  local code = M.keycode(key)\n  keys[code] = keys[code] or {}\n  table.insert(keys[code], cb)\n  on_key_ns = on_key_ns\n    or vim.on_key(function(resolved, typed)\n      for _, c in ipairs(keys[typed or resolved] or {}) do\n        pcall(c, typed)\n      end\n    end)\nend\n\n---@generic T\n---@param t T\n---@return { value?:T }|fun():T?\nfunction M.ref(t)\n  return setmetatable({ value = t }, {\n    __mode = \"v\",\n    __call = function(m)\n      return m.value\n    end,\n  })\nend\n\n---@generic T\n---@param fn T\n---@param opts? {ms?:number}\n---@return T\nfunction M.throttle(fn, opts)\n  local timer = assert(uv.new_timer())\n  local trailing, ms = false, opts and opts.ms or 20\n  local running = false\n  local function run()\n    running = true\n    if vim.in_fast_event() then\n      return vim.schedule(run)\n    end\n    fn()\n    running = false\n  end\n  return function()\n    if running or timer:is_active() then\n      trailing = true\n      return\n    end\n    trailing = false\n    run()\n    timer:start(ms, 0, function()\n      return trailing and run()\n    end)\n  end\nend\n\n---@generic T\n---@param fn T\n---@param opts? {ms?:number}\n---@return T\nfunction M.debounce(fn, opts)\n  local timer = assert(uv.new_timer())\n  local ms = opts and opts.ms or 20\n  return function()\n    timer:start(ms, 0, vim.schedule_wrap(fn))\n  end\nend\n\n---@param key string\nfunction M.normkey(key)\n  if key_cache[key] then\n    return key_cache[key]\n  end\n  local function norm(v)\n    local l = v:lower()\n    if l == \"leader\" then\n      return M.normkey(\"<leader>\")\n    elseif l == \"localleader\" then\n      return M.normkey(\"<localleader>\")\n    end\n    return vim.fn.keytrans(M.keycode((\"<%s>\"):format(v)))\n  end\n  local orig = key\n  key = key:gsub(\"<lt>\", \"<\")\n  local lower = key:lower()\n  if lower == \"<leader>\" then\n    key = vim.g.mapleader\n    key = vim.fn.keytrans((not key or key == \"\") and \"\\\\\" or key)\n  elseif lower == \"<localleader>\" then\n    key = vim.g.maplocalleader\n    key = vim.fn.keytrans((not key or key == \"\") and \"\\\\\" or key)\n  else\n    local extracted = {} ---@type string[]\n    local function extract(v)\n      v = v:sub(2, -2)\n      if v:sub(2, 2) == \"-\" and v:sub(1, 1):find(\"[aAmMcCsS]\") then\n        local m = v:sub(1, 1):upper()\n        m = m == \"A\" and \"M\" or m\n        local k = v:sub(3)\n        if #k > 1 then\n          return norm(v)\n        end\n        if m == \"C\" then\n          k = k:upper()\n        elseif m == \"S\" then\n          return k:upper()\n        end\n        return (\"<%s-%s>\"):format(m, k)\n      end\n      return norm(v)\n    end\n    local placeholder = \"_#_\"\n    ---@param v string\n    key = key:gsub(\"(%b<>)\", function(v)\n      table.insert(extracted, extract(v))\n      return placeholder\n    end)\n    key = vim.fn.keytrans(key):gsub(\"<lt>\", \"<\")\n\n    -- Restore extracted %b<> sequences\n    local i = 0\n    key = key:gsub(placeholder, function()\n      i = i + 1\n      return extracted[i] or \"\"\n    end)\n  end\n  key_cache[orig] = key\n  key_cache[key] = key\n  return key\nend\n\n---@param win? number\nfunction M.is_float(win)\n  return vim.api.nvim_win_get_config(win or 0).relative ~= \"\"\nend\n\nfunction M.spinner()\n  local spinner = { \"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\" }\n  return spinner[math.floor(uv.hrtime() / (1e6 * 80)) % #spinner + 1]\nend\n\nM.base64 = vim.base64 and vim.base64.encode\n  or function(data)\n    data = tostring(data)\n    local bit = require(\"bit\")\n    local b64chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n    local b64, len = \"\", #data\n    for i = 1, len, 3 do\n      local a, b, c = data:byte(i, i + 2)\n      local buffer = bit.bor(bit.lshift(a, 16), bit.lshift(b or 0, 8), c or 0)\n      for j = 0, 3 do\n        local index = bit.rshift(buffer, (3 - j) * 6) % 64\n        b64 = b64 .. b64chars:sub(index + 1, index + 1)\n      end\n    end\n    local padding = (3 - len % 3) % 3\n    b64 = b64:sub(1, -1 - padding) .. (\"=\"):rep(padding)\n    return b64\n  end\n\n--- Parse async when available.\n---@param parser vim.treesitter.LanguageTree\n---@param range boolean|Range|nil: Parse this range in the parser's source.\n---@param on_parse fun(err?: string, trees?: table<integer, TSTree>) Function invoked when parsing completes.\nfunction M.parse(parser, range, on_parse)\n  ---@diagnostic disable-next-line: invisible\n  local have_async = vim.fn.has(\"nvim-0.11.4\") == 1 or (vim.treesitter.languagetree or {})._async_parse ~= nil\n  if have_async then\n    parser:parse(range, on_parse)\n  else\n    parser:parse(range)\n    on_parse(nil, parser:trees())\n  end\nend\n\n---@param handle? uv.uv_handle_t|uv.uv_timer_t\nfunction M.stop(handle)\n  if handle and not handle:is_closing() then\n    if handle.stop then\n      handle:stop()\n    end\n    handle:close()\n  end\nend\n\n--- Better validation to check if path is a dir or a file\n---@param path string\n---@return \"directory\"|\"file\"\nfunction M.path_type(path)\n  local stat = uv.fs_stat(path)\n  if stat and stat.type then\n    return stat.type\n  end\n  if vim.fn.isdirectory(path) == 1 then\n    return \"directory\"\n  end\n  return \"file\"\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/util/job.lua",
    "content": "---@class vim.fn.jobstart.Opts\n---@field clear_env? boolean\n---@field cwd? string\n---@field detach? boolean\n---@field env? table<string, string>\n---@field height? number\n---@field on_exit? fun(job_id: number, exit_code: number, event_type: string)\n---@field on_stdout? fun(job_id: number, data: string[], event_type: string)\n---@field on_stderr? fun(job_id: number, data: string[], event_type: string)\n---@field overlapped? boolean\n---@field pty? boolean\n---@field rpc? boolean\n---@field stderr_buffered? boolean\n---@field stdin? \"pipe\" | \"null\"\n---@field stdout_buffered? boolean\n---@field term? boolean\n---@field width? number\n---@field sync? boolean\n\n---@class snacks.job.Opts: vim.fn.jobstart.Opts\n---@field input? string\n---@field output? string\n---@field debug? boolean\n---@field ansi? boolean\n---@field start? boolean\n---@field on_line? fun(job_id: number, text: string, line: number)\n---@field on_lines? fun(job_id: number, lines: string[])\n\nlocal M = {}\n\n---@param opts snacks.job.Opts|vim.fn.jobstart.Opts\n---@return vim.fn.jobstart.Opts\nlocal function get_opts(opts)\n  opts = vim.deepcopy(opts)\n  opts.input = nil\n  if opts.term == false then\n    opts.term = nil\n  end\n  return vim.tbl_isempty(opts) and vim.empty_dict() or opts\nend\n\n---@generic F: function\n---@param fn F\n---@param orig? F\n---@return F\nlocal function wrap(fn, orig)\n  return function(...)\n    fn(...)\n    if orig then\n      orig(...)\n    end\n  end\nend\n\n---@param cmd string | string[]\n---@param opts? vim.fn.jobstart.Opts\nlocal function jobstart(cmd, opts)\n  opts = opts or {}\n  if opts.term and vim.fn.has(\"nvim-0.11.4\") == 0 then\n    opts.term = nil\n    ---@diagnostic disable-next-line: deprecated\n    return vim.fn.termopen(cmd, get_opts(opts))\n  end\n  return vim.fn.jobstart(cmd, get_opts(opts))\nend\n\n---@class snacks.Job\n---@field buf number\n---@field cmd string | string[]\n---@field opts snacks.job.Opts\n---@field lines string[]\n---@field line number\n---@field id? number\n---@field chan? number\n---@field killed? boolean\nlocal Job = {}\nJob.__index = Job\n\n---@param buf number\n---@param cmd string | string[]\n---@param opts? snacks.job.Opts\nfunction Job.new(buf, cmd, opts)\n  local self = setmetatable({}, Job)\n  self.buf = buf\n  self.opts = opts or {}\n  self.cmd = cmd\n  self.lines = { \"\" }\n  self.line = 1\n  self:setup()\n  if self.opts.start ~= false then\n    self:start()\n  end\n  return self\nend\n\nfunction Job:setup()\n  self.opts.term = self.opts.term ~= false\n  self.opts.sync = self.opts.sync ~= false\n  if self.opts.term and self.opts.input then\n    -- NOTE: term jobs do not support input\n    self.opts.term, self.opts.ansi = false, true\n  end\n  local on_output = function(_, data)\n    self:on_output(data)\n  end\n  self.opts.on_stdout = wrap(on_output, self.opts.on_stdout)\n  self.opts.on_stderr = wrap(on_output, self.opts.on_stderr)\n  self.opts.on_exit = wrap(function(_, code)\n    self:on_exit(code)\n  end, self.opts.on_exit)\n  if not self.opts.term and not self.opts.ansi then\n    self.opts.on_line = self.opts.on_line or function(_, text, line)\n      self:on_line(text, line)\n    end\n  end\nend\n\nfunction Job:on_exit(code)\n  if not self:buf_valid() then\n    return\n  end\n  self:emit()\n  if self.opts.on_lines then\n    self.opts.on_lines(self.id, self.lines)\n  end\n  if self.opts.term then\n    self:hide_process_exited()\n  end\n\n  self:set_cursor()\n\n  if not self.killed and code ~= 0 then\n    self:error(\n      (\"Job exited with code `%s`\"):format(code),\n      (\"\\n- `vim.o.shell = %q`\\n\\nOutput:\\n%s\"):format(vim.o.shell, vim.trim(table.concat(self.lines, \"\\n\")))\n    )\n  end\nend\n\nfunction Job:set_cursor()\n  if not self:buf_valid() then\n    return\n  end\n  for _, win in ipairs(vim.fn.win_findbuf(self.buf)) do\n    vim.api.nvim_win_set_cursor(win, { 1, 0 })\n  end\nend\n\n---@param text string\n---@param line number\nfunction Job:on_line(text, line)\n  if self:buf_valid() then\n    vim.bo[self.buf].modifiable = true\n    vim.api.nvim_buf_set_lines(self.buf, line == 1 and 0 or -1, -1, true, { text })\n    vim.bo[self.buf].modifiable = false\n  end\nend\n\n---@param msg string\n---@param footer? string\nfunction Job:error(msg, footer)\n  Snacks.debug.cmd({\n    title = \"Job Error\",\n    level = vim.log.levels.ERROR,\n    header = msg,\n    footer = footer,\n    cmd = self.cmd,\n    cwd = self.opts.cwd,\n    group = true,\n  })\nend\n\nfunction Job:start()\n  if self.opts.debug then\n    vim.schedule(function()\n      Snacks.debug.cmd({\n        cmd = self.cmd,\n        cwd = self.opts.cwd,\n        group = true,\n        props = {\n          cwd = self.opts.cwd,\n          term = self.opts.term,\n          pty = self.opts.pty,\n          input = self.opts.input and \"<provided>\",\n          output = self.opts.output and \"<provided>\",\n          ansi = self.opts.ansi,\n        },\n      })\n    end)\n  end\n\n  if self.opts.output or (not self.opts.term and self.opts.ansi) then\n    self.chan = vim.api.nvim_open_term(self.buf, {})\n    if self.opts.output then\n      vim.api.nvim_chan_send(self.chan, self.opts.output)\n      return\n    end\n  end\n\n  self.id = vim.api.nvim_buf_call(self.buf, function()\n    return jobstart(self.cmd, self.opts)\n  end)\n\n  if self.id <= 0 then\n    self.id = nil\n    return self:error(\"Failed to start job\")\n  end\n\n  vim.api.nvim_create_autocmd({ \"BufWipeout\", \"BufDelete\" }, {\n    buffer = self.buf,\n    callback = function()\n      self:stop()\n    end,\n  })\n\n  if self.opts.input then\n    vim.fn.chansend(self.id, self.opts.input .. \"\\n\")\n    vim.fn.chanclose(self.id, \"stdin\")\n  end\nend\n\nfunction Job:stop()\n  if self.id then\n    self.killed = true\n    vim.fn.jobstop(self.id)\n  end\nend\n\nfunction Job:set_lines(from, to, lines)\n  if self:buf_valid() then\n    vim.bo[self.buf].modifiable = true\n    vim.api.nvim_buf_set_lines(self.buf, from, to, true, lines)\n    vim.bo[self.buf].modifiable = false\n  end\nend\n\nfunction Job:hide_process_exited()\n  local timer = assert(vim.uv.new_timer())\n  local stop = function()\n    return timer:is_active() and timer:stop() == 0 and timer:close()\n  end\n  local check = function()\n    if self:buf_valid() then\n      for i, line in ipairs(vim.api.nvim_buf_get_lines(self.buf, 0, -1, true)) do\n        if line:find(\"^%[Process exited 0%]\") then\n          self:set_lines(i - 1, i, {})\n          return stop()\n        end\n      end\n    end\n  end\n  timer:start(30, 30, vim.schedule_wrap(check))\n  vim.defer_fn(stop, 1000)\nend\n\nfunction Job:running()\n  return self.id and vim.fn.jobwait({ self.id }, 0)[1] == -1\nend\n\nfunction Job:buf_valid()\n  return self.buf and vim.api.nvim_buf_is_valid(self.buf)\nend\n\nfunction Job:emit()\n  if not self:buf_valid() then\n    return\n  end\n  while self.line < #self.lines do\n    self.lines[self.line] = self.lines[self.line]:gsub(\"\\r$\", \"\")\n    if self.opts.on_line then\n      self.opts.on_line(self.id, self.lines[self.line], self.line)\n    end\n    self.line = self.line + 1\n  end\nend\n\n---@param data string[]\nfunction Job:on_output(data)\n  if not self:buf_valid() then\n    return\n  end\n  if self.chan then\n    vim.api.nvim_chan_send(self.chan, table.concat(data, \"\\n\"))\n  end\n  self.lines[#self.lines] = self.lines[#self.lines] .. data[1]\n  vim.list_extend(self.lines, data, 2)\n  self:emit()\nend\n\nfunction Job:refresh()\n  if not self:buf_valid() then\n    return\n  end\n  -- HACK: this forces a refresh of the terminal buffer and prevents flickering\n  vim.bo[self.buf].scrollback = 9999\n  vim.bo[self.buf].scrollback = 9998\nend\n\nM.new = Job.new\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/util/lsp.lua",
    "content": "---@class snacks.lsp\nlocal M = {}\n\n---@alias snacks.lsp.handler.cb fun(buf: number, client: vim.lsp.Client):any?\n\n---@class snacks.lsp.Handler\n---@field filter vim.lsp.get_clients.Filter\n---@field cb snacks.lsp.handler.cb\n---@field done table<number, boolean>\n\nlocal _handlers = {} ---@type snacks.lsp.Handler[]\n\nlocal did_setup = false\n\n---@param filter vim.lsp.get_clients.Filter\nlocal function _handle(filter)\n  ---@param h snacks.lsp.Handler\n  local handlers = vim.tbl_filter(function(h)\n    ---@diagnostic disable-next-line: no-unknown\n    for k, v in pairs(filter) do\n      if h.filter[k] ~= nil and h.filter[k] ~= v then\n        return false\n      end\n    end\n    return true\n  end, _handlers)\n\n  if #handlers == 0 then\n    return\n  end\n\n  for _, state in ipairs(handlers) do\n    local f = vim.deepcopy(state.filter)\n    f = vim.tbl_extend(\"force\", f, filter)\n    local clients = vim.lsp.get_clients(f)\n    for _, client in ipairs(clients) do\n      for buf in pairs(client.attached_buffers) do\n        local key = (\"%d:%d\"):format(client.id, buf)\n        if not state.done[key] then\n          state.done[key] = true\n          local ok, err = pcall(state.cb, buf, client)\n          if not ok then\n            vim.schedule(function()\n              Snacks.notify.error((\"Error in handler:\\n%s\\n```lua\\n%s\\n```\"):format(err, vim.inspect(state.filter)))\n            end)\n          end\n        end\n      end\n    end\n  end\nend\n\nlocal function setup()\n  if did_setup then\n    return\n  end\n  did_setup = true\n  local register_capability = vim.lsp.handlers[\"client/registerCapability\"]\n  vim.lsp.handlers[\"client/registerCapability\"] = function(err, res, ctx)\n    ---@cast res lsp.RegistrationParams\n    local ret = register_capability(err, res, ctx) ---@type any\n    vim.schedule(function()\n      for _, m in ipairs(res.registrations or {}) do\n        _handle({ method = m.method, id = ctx.client_id })\n      end\n    end)\n    return ret\n  end\n  local group = vim.api.nvim_create_augroup(\"snacks.lsp.on_attach\", { clear = true })\n  vim.api.nvim_create_autocmd(\"LspAttach\", {\n    group = group,\n    callback = function(ev)\n      vim.schedule(function()\n        _handle({ id = ev.data.client_id, buffer = ev.buf })\n      end)\n    end,\n  })\n  vim.api.nvim_create_autocmd(\"LspDetach\", {\n    group = group,\n    callback = function(ev)\n      local key = (\"%d:%d\"):format(ev.data.client_id, ev.buf)\n      for _, state in ipairs(_handlers) do\n        state.done[key] = nil\n      end\n    end,\n  })\nend\n\n---@param filter? vim.lsp.get_clients.Filter\n---@param cb snacks.lsp.handler.cb\n---@overload fun(cb: snacks.lsp.handler.cb)\nfunction M.on(filter, cb)\n  setup()\n  filter = filter or {}\n  if type(filter) == \"function\" then\n    cb = filter\n    filter = {}\n  end\n  table.insert(_handlers, { filter = filter, cb = cb, done = {} })\n  _handle(filter)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/util/spawn.lua",
    "content": "local Async = require(\"snacks.picker.util.async\")\n\n---@class snacks.spawn\nlocal M = {}\n\nlocal uv = vim.uv or vim.loop\n\n---@class snacks.spawn.Config: uv.spawn.options,{}\n---@field cmd string\n---@field args? (string|number)[]\n---@field timeout? number\n---@field run? boolean\n---@field debug? boolean\n---@field input? string\n---@field on_stdout? fun(proc: snacks.spawn.Proc, data: string)\n---@field on_stderr? fun(proc: snacks.spawn.Proc, data: string)\n---@field on_exit? fun(proc: snacks.spawn.Proc, err: boolean)\n\n---@class snacks.spawn.Multi: snacks.spawn.Config,{}\n---@field cmd? nil\n---@field on_exit? fun(procs: snacks.spawn.Proc[], err: boolean)\n\n---@class snacks.spawn.Proc: snacks.picker.Waitable\n---@field opts snacks.spawn.Config\n---@field handle? uv.uv_process_t\n---@field stdout uv.uv_pipe_t\n---@field stderr uv.uv_pipe_t\n---@field stdin? uv.uv_pipe_t\n---@field code? number\n---@field signal? number\n---@field timer? uv.uv_timer_t\n---@field aborted? boolean\n---@field data table<uv.uv_pipe_t, string[]>\n---@field async? snacks.picker.Async\n---@field did_exit? boolean\nlocal Proc = {}\nProc.__index = Proc\n\n---@param handle uv.uv_handle_t?\nlocal function close(handle)\n  if handle and not handle:is_closing() then\n    handle:close()\n  end\nend\n\n---@param opts snacks.spawn.Config\nfunction Proc.new(opts)\n  local self = setmetatable({}, Proc)\n  self.opts = opts\n  self.code, self.signal = 0, 0\n  self.data = {}\n  if opts.run ~= false then\n    self:run()\n  end\n  return self\nend\n\nfunction Proc:running()\n  return self.handle and not self.handle:is_closing()\nend\n\n---@param signal? string|number\nfunction Proc:kill(signal)\n  close(self.stdout)\n  close(self.stderr)\n  if self:running() then\n    self.aborted = true\n    self.handle:kill(signal or \"sigterm\")\n  end\nend\n\nfunction Proc:failed()\n  if self.aborted then\n    return true\n  end\n  if self:running() then\n    return false\n  end\n  return self.code ~= 0 or self.signal ~= 0\nend\n\n---@param opts? snacks.debug.cmd|{}\nfunction Proc:debug(opts)\n  ---@type snacks.debug.cmd\n  opts = Snacks.config.merge({}, opts or {}, {\n    cmd = self.opts.cmd,\n    args = self.opts.args,\n    cwd = self.opts.cwd,\n  })\n  opts.props = opts.props or {}\n  if not self:running() then\n    opts.props.code = (\"`%d`\"):format(self.code)\n    opts.props.signal = (\"`%d`\"):format(self.signal)\n    if self.aborted then\n      opts.props.aborted = \"`true`\"\n    end\n  end\n  if self:failed() then\n    opts.level = \"error\"\n  end\n  local out = vim.trim(self:out() .. \"\\n\" .. self:err())\n  if out ~= \"\" then\n    opts.footer = \"# Output\\n```\\n\" .. out .. \"\\n```\"\n  end\n  return Snacks.debug.cmd(opts)\nend\n\nfunction Proc:setup_async()\n  self.async = Async.running()\n  if self.async then\n    self.async:on(\"abort\", function()\n      if self:running() then\n        self:kill()\n      end\n    end)\n  end\nend\n\n---@async\nfunction Proc:wait()\n  self:setup_async()\n  assert(self.async, \"Not in an async context\")\n  assert(self.async == Async.running(), \"Not in the current async context\")\n  while not self.did_exit or self:running() do\n    self.async:suspend()\n  end\n  return self\nend\n\nfunction Proc:run()\n  assert(not self.handle, \"already running\")\n  if self.aborted then\n    return self:on_exit()\n  end\n\n  self:setup_async()\n\n  self.stdout = assert(uv.new_pipe())\n  self.stderr = assert(uv.new_pipe())\n  self.stdin = self.opts.input and assert(uv.new_pipe()) or nil\n  self.data = { [self.stdout] = {}, [self.stderr] = {} }\n  if self.opts.debug then\n    vim.schedule(function()\n      self:debug()\n    end)\n  end\n  local opts = vim.tbl_deep_extend(\"force\", self.opts, {\n    stdio = { self.stdin, self.stdout, self.stderr },\n    hide = true,\n    args = vim.tbl_map(tostring, self.opts.args or {}),\n  })\n  self.handle = uv.spawn(self.opts.cmd, opts, function(code, signal)\n    self.code = code\n    self.signal = signal\n    self:on_exit()\n  end)\n  if not self.handle then\n    self.code = 1\n    self.data[self.stderr] = { \"Failed to spawn \" .. self.opts.cmd }\n    close(self.stdout)\n    close(self.stderr)\n    return self:on_exit()\n  end\n\n  if self.stdin and self.opts.input then\n    self.stdin:write(self.opts.input)\n    self.stdin:shutdown()\n    self.stdin:close()\n  end\n\n  if self.opts.timeout then\n    self.timer = assert(uv.new_timer())\n    self.timer:start(self.opts.timeout, 0, function()\n      self:kill(\"sigterm\")\n    end)\n  end\n  for _, handle in ipairs({ self.stdout, self.stderr }) do\n    handle:read_start(function(err, data)\n      assert(not err, err)\n      if data then\n        self:on_data(data, handle)\n      else\n        close(handle)\n      end\n    end)\n  end\nend\n\nfunction Proc:json()\n  return vim.json.decode(self:out())\nend\n\nfunction Proc:out()\n  return table.concat(self.data[self.stdout] or {})\nend\n\nfunction Proc:err()\n  return table.concat(self.data[self.stderr] or {})\nend\n\nfunction Proc:lines()\n  return vim.split(self:out(), \"\\n\", { plain = true })\nend\n\n---@param data string\n---@param handle uv.uv_pipe_t\nfunction Proc:on_data(data, handle)\n  table.insert(self.data[handle], data)\n  if self.opts.on_stdout and handle == self.stdout then\n    self.opts.on_stdout(self, data)\n  elseif self.opts.on_stderr and handle == self.stderr then\n    self.opts.on_stderr(self, data)\n  end\nend\n\nfunction Proc:on_exit()\n  close(self.timer)\n  close(self.handle)\n  local check = assert(uv.new_check())\n  check:start(function()\n    for _, handle in ipairs({ self.stdout, self.stderr }) do\n      if handle and not handle:is_closing() then\n        return\n      end\n    end\n    check:stop()\n    close(check)\n    close(self.stdout)\n    close(self.stderr)\n    if self.opts.on_exit then\n      self.opts.on_exit(self, self.code ~= 0 or self.signal ~= 0 or self.aborted or false)\n    end\n    self.did_exit = true\n    if self.async then\n      self.async:resume()\n    end\n  end)\nend\n\n---@param procs snacks.spawn.Proc[]\n---@param opts? snacks.spawn.Multi\nfunction M.multi(procs, opts)\n  if #procs == 0 then\n    return\n  end\n  opts = opts or {}\n  local current = 0\n\n  local function done()\n    if opts.on_exit then\n      opts.on_exit(procs, procs[current]:failed())\n    end\n  end\n\n  local function next()\n    current = current + 1\n    assert(current <= #procs, \"current > #procs\")\n    local proc = procs[current]\n    proc.opts = Snacks.config.merge(vim.deepcopy(opts), proc.opts, {\n      on_exit = function(_, err)\n        if err or current == #procs then\n          done()\n        else\n          next()\n        end\n      end,\n    })\n    proc:run()\n  end\n\n  ---@type snacks.spawn.Proc|{procs: snacks.spawn.Proc[]}\n  local ret = setmetatable({\n    procs = procs,\n    run = next,\n  }, {\n    __index = function(_, k)\n      return procs[current][k]\n    end,\n  })\n\n  if opts.run ~= false then\n    next()\n  end\n  return ret\nend\n\nM.new = Proc.new\n\n---@param cmd string[]\n---@async\nfunction M.exec(cmd)\n  return vim.trim(M.new({\n    cmd = cmd[1],\n    args = vim.list_slice(cmd, 2),\n    stdout_buffered = true,\n    stderr_buffered = true,\n  })\n    :wait()\n    :out())\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/win.lua",
    "content": "---@class snacks.win\n---@field id number\n---@field buf? number\n---@field scratch_buf? number\n---@field win? number\n---@field opts snacks.win.Config\n---@field augroup? number\n---@field backdrop? snacks.win\n---@field keys snacks.win.Keys[]\n---@field events (snacks.win.Event|{event:string|string[]})[]\n---@field meta table<string, any>\n---@field closed? boolean\n---@overload fun(opts? :snacks.win.Config|{}): snacks.win\nlocal M = setmetatable({}, {\n  __call = function(t, ...)\n    return t.new(...)\n  end,\n})\nM.__index = M\n\nM.meta = {\n  desc = \"Create and manage floating windows or splits\",\n}\n\n---@class snacks.win.Keys: vim.api.keyset.keymap\n---@field [1]? string\n---@field [2]? string|string[]|fun(self: snacks.win): string?\n---@field mode? string|string[]\n\n---@class snacks.win.Event: vim.api.keyset.create_autocmd\n---@field buf? true\n---@field win? true\n---@field callback? fun(self: snacks.win, ev:vim.api.keyset.create_autocmd.callback_args):boolean?\n\n---@class snacks.win.Backdrop\n---@field bg? string\n---@field blend? number\n---@field transparent? boolean defaults to true\n---@field win? snacks.win.Config overrides the backdrop window config\n\n---@class snacks.win.Dim\n---@field width number width of the window, without borders\n---@field height number height of the window, without borders\n---@field row number row of the window (0-indexed)\n---@field col number column of the window (0-indexed)\n---@field border? boolean whether the window has a border\n\n---@alias snacks.win.Action.fn fun(self: snacks.win):(boolean|string?)\n---@alias snacks.win.Action.spec snacks.win.Action|snacks.win.Action.fn\n---@class snacks.win.Action\n---@field action snacks.win.Action.fn\n---@field desc? string\n\n---@class snacks.win.Config: vim.api.keyset.win_config\n---@field style? string merges with config from `Snacks.config.styles[style]`\n---@field show? boolean Show the window immediately (default: true)\n---@field footer_keys? boolean|string[] Show keys footer. When string[], only show those keys with lhs (default: false)\n---@field height? number|fun(self:snacks.win):number Height of the window. Use <1 for relative height. 0 means full height. (default: 0.9)\n---@field width? number|fun(self:snacks.win):number Width of the window. Use <1 for relative width. 0 means full width. (default: 0.9)\n---@field min_height? number Minimum height of the window\n---@field max_height? number Maximum height of the window\n---@field min_width? number Minimum width of the window\n---@field max_width? number Maximum width of the window\n---@field col? number|fun(self:snacks.win):number Column of the window. Use <1 for relative column. (default: center)\n---@field row? number|fun(self:snacks.win):number Row of the window. Use <1 for relative row. (default: center)\n---@field minimal? boolean Disable a bunch of options to make the window minimal (default: true)\n---@field position? \"float\"|\"bottom\"|\"top\"|\"left\"|\"right\"|\"current\"\n---@field border? \"none\"|\"top\"|\"right\"|\"bottom\"|\"left\"|\"top_bottom\"|\"hpad\"|\"vpad\"|\"rounded\"|\"single\"|\"double\"|\"solid\"|\"shadow\"|\"bold\"|string[]|false|true\n---@field buf? number If set, use this buffer instead of creating a new one\n---@field file? string If set, use this file instead of creating a new buffer\n---@field enter? boolean Enter the window after opening (default: false)\n---@field backdrop? number|false|snacks.win.Backdrop Opacity of the backdrop (default: 60)\n---@field wo? vim.wo|{} window options\n---@field bo? vim.bo|{} buffer options\n---@field b? table<string, any> buffer local variables\n---@field w? table<string, any> window local variables\n---@field ft? string filetype to use for treesitter/syntax highlighting. Won't override existing filetype\n---@field scratch_ft? string filetype to use for scratch buffers\n---@field keys? table<string, false|string|fun(self: snacks.win)|snacks.win.Keys> Key mappings\n---@field on_buf? fun(self: snacks.win) Callback after opening the buffer\n---@field on_win? fun(self: snacks.win) Callback after opening the window\n---@field on_close? fun(self: snacks.win) Callback after closing the window\n---@field fixbuf? boolean don't allow other buffers to be opened in this window\n---@field text? string|string[]|fun():(string[]|string) Initial lines to set in the buffer\n---@field actions? table<string, snacks.win.Action.spec> Actions that can be used in key mappings\n---@field resize? boolean Automatically resize the window when the editor is resized\n---@field stack? boolean When enabled, multiple split windows with the same position will be stacked together (useful for terminals)\nlocal defaults = {\n  show = true,\n  fixbuf = true,\n  relative = \"editor\",\n  position = \"float\",\n  minimal = true,\n  wo = {\n    winhighlight = \"Normal:SnacksNormal,NormalNC:SnacksNormalNC,WinBar:SnacksWinBar,WinBarNC:SnacksWinBarNC,FloatTitle:SnacksTitle,FloatFooter:SnacksFooter,WinSeparator:SnacksWinSeparator\",\n  },\n  bo = {},\n  title_pos = \"center\",\n  keys = {\n    q = \"close\",\n  },\n  footer_pos = \"center\",\n  footer_keys = false,\n}\n\nSnacks.config.style(\"float\", {\n  position = \"float\",\n  backdrop = 60,\n  height = 0.9,\n  width = 0.9,\n  zindex = 50,\n})\n\nSnacks.config.style(\"help\", {\n  position = \"float\",\n  backdrop = false,\n  border = \"top\",\n  row = -1,\n  width = 0,\n  height = 0.3,\n})\n\nSnacks.config.style(\"split\", {\n  position = \"bottom\",\n  height = 0.4,\n  width = 0.4,\n})\n\nSnacks.config.style(\"minimal\", {\n  wo = {\n    cursorcolumn = false,\n    cursorline = false,\n    cursorlineopt = \"both\",\n    colorcolumn = \"\",\n    fillchars = \"eob: ,lastline:…\",\n    foldcolumn = \"0\",\n    list = false,\n    listchars = \"extends:…,tab:  \",\n    number = false,\n    relativenumber = false,\n    signcolumn = \"no\",\n    spell = false,\n    winbar = \"\",\n    statuscolumn = \"\",\n    wrap = false,\n    sidescrolloff = 0,\n  },\n})\n\nlocal SCROLL_UP, SCROLL_DOWN = Snacks.util.keycode(\"<c-y>\"), Snacks.util.keycode(\"<c-e>\")\n\nlocal split_commands = {\n  editor = {\n    top = \"topleft\",\n    right = \"vertical botright\",\n    bottom = \"botright\",\n    left = \"vertical topleft\",\n  },\n  win = {\n    top = \"aboveleft\",\n    right = \"vertical rightbelow\",\n    bottom = \"belowright\",\n    left = \"vertical leftabove\",\n  },\n}\n\nlocal win_opts = {\n  \"anchor\",\n  \"border\",\n  \"bufpos\",\n  \"col\",\n  \"external\",\n  \"fixed\",\n  \"focusable\",\n  \"footer\",\n  \"footer_pos\",\n  \"height\",\n  \"hide\",\n  \"noautocmd\",\n  \"relative\",\n  \"row\",\n  \"style\",\n  \"title\",\n  \"title_pos\",\n  \"width\",\n  \"win\",\n  \"zindex\",\n}\n\n---@type table<string, string[]>\nlocal borders = {\n  left = { \"\", \"\", \"\", \"\", \"\", \"\", \"\", \"│\" },\n  right = { \"\", \"\", \"\", \"│\", \"\", \"\", \"\", \"\" },\n  top = { \"\", \"─\", \"\", \"\", \"\", \"\", \"\", \"\" },\n  bottom = { \"\", \"\", \"\", \"\", \"\", \"─\", \"\", \"\" },\n  top_bottom = { \"\", \"─\", \"\", \"\", \"\", \"─\", \"\", \"\" },\n  hpad = { \"\", \"\", \"\", \" \", \"\", \"\", \"\", \" \" },\n  vpad = { \"\", \" \", \"\", \"\", \"\", \" \", \"\", \"\" },\n}\n\nSnacks.util.set_hl({\n  Backdrop = { bg = \"#000000\" },\n  Footer = \"FloatFooter\",\n  FooterDesc = \"DiagnosticInfo\",\n  FooterKey = \"DiagnosticVirtualTextInfo\",\n  Normal = \"NormalFloat\",\n  NormalNC = \"NormalFloat\",\n  Title = \"FloatTitle\",\n  WinBar = \"Title\",\n  WinBarNC = \"SnacksWinBar\",\n  WinKey = \"Keyword\",\n  WinKeySep = \"NonText\",\n  WinKeyDesc = \"Function\",\n  WinSeparator = \"WinSeparator\",\n}, { prefix = \"Snacks\", default = true })\n\nlocal id = 0\nlocal event_stack = {} ---@type string[]\n\n--@private\n---@param ...? snacks.win.Config|string|{}\n---@return snacks.win.Config\nfunction M.resolve(...)\n  local done = {} ---@type table<string, boolean>\n  local merge = {} ---@type snacks.win.Config[]\n  local stack = {}\n  for i = 1, select(\"#\", ...) do\n    local next = select(i, ...) ---@type snacks.win.Config|string?\n    if next then\n      table.insert(stack, next)\n    end\n  end\n  while #stack > 0 do\n    local next = table.remove(stack)\n    next = type(next) == \"string\" and Snacks.config.styles[next] or next\n    ---@cast next snacks.win.Config?\n    if next and type(next) == \"table\" then\n      table.insert(merge, 1, next)\n      if next.style and not done[next.style] then\n        done[next.style] = true\n        table.insert(stack, next.style)\n      end\n    end\n  end\n  local ret = #merge == 0 and {} or #merge == 1 and merge[1] or vim.tbl_deep_extend(\"force\", {}, unpack(merge))\n  ret.style = nil\n  return ret\nend\n\n---@param opts? snacks.win.Config|{}\n---@return snacks.win\nfunction M.new(opts)\n  local self = setmetatable({}, M)\n  id = id + 1\n  self.id = id\n  self.meta = {}\n  opts = M.resolve(Snacks.config.get(\"win\", defaults), opts)\n  if opts.minimal then\n    opts = M.resolve(\"minimal\", opts)\n  end\n  if opts.position == \"float\" then\n    opts = M.resolve(\"float\", opts)\n  else\n    opts = M.resolve(\"split\", opts)\n    local vertical = opts.position == \"left\" or opts.position == \"right\"\n    opts.wo.winfixheight = not vertical\n    opts.wo.winfixwidth = vertical\n  end\n  if opts.relative == \"win\" then\n    opts.win = opts.win or vim.api.nvim_get_current_win()\n  end\n\n  self.keys = {}\n  self.events = {}\n  local done = {} ---@type table<string, snacks.win.Keys>\n  for key, spec in pairs(opts.keys) do\n    if spec then\n      if type(spec) == \"string\" then\n        spec = { key, spec, desc = spec }\n      elseif type(spec) == \"function\" then\n        spec = { key, spec }\n      elseif type(spec) == \"table\" and spec[1] and not spec[2] then\n        spec = vim.deepcopy(spec) -- deepcopy just in case\n        spec[1], spec[2] = key, spec[1]\n      end\n      ---@cast spec snacks.win.Keys\n      local lhs = Snacks.util.normkey(spec[1] or \"\")\n      local mode = type(spec.mode) == \"table\" and spec.mode or { spec.mode or \"n\" }\n      ---@cast mode string[]\n      mode = #mode == 0 and { \"n\" } or mode\n      for _, m in ipairs(mode) do\n        local k = m .. \":\" .. lhs\n        if done[k] then\n          Snacks.notify.warn(\n            (\"# Duplicate key mapping for `%s` mode=%s (check case):\\n```lua\\n%s\\n```\\n```lua\\n%s\\n```\"):format(\n              lhs,\n              m,\n              vim.inspect(done[k]),\n              vim.inspect(spec)\n            )\n          )\n        end\n        done[k] = spec\n      end\n      table.insert(self.keys, spec)\n    end\n  end\n  -- last defined mapping is found first, so for `nowait` to work,\n  -- we need to sort in reverse order\n  table.sort(self.keys, function(a, b)\n    return (a[1] or \"\") > (b[1] or \"\")\n  end)\n\n  self:on(\"WinClosed\", self.on_close, { win = true })\n  self:on(\"WinResized\", function()\n    if self.backdrop and not self:is_floating() then\n      self.backdrop:close()\n      self.backdrop = nil\n    end\n  end)\n\n  -- update window size when resizing\n  self:on(\"VimResized\", self.on_resize)\n\n  ---@cast opts snacks.win.Config\n  self.opts = opts\n  if opts.show ~= false then\n    self:show()\n  end\n  return self\nend\n\nfunction M:on_resize()\n  if self.opts.resize ~= false then\n    self:update()\n  end\nend\n\n---@param actions string|string[]\nfunction M:execute(actions)\n  return self:action(actions)()\nend\n\n---@param actions string|string[]\n---@return (fun(): boolean|string?) action, string? desc\nfunction M:action(actions)\n  actions = type(actions) == \"string\" and { actions } or actions\n  ---@cast actions string[]\n  local desc = {} ---@type string[]\n  for a, name in ipairs(actions) do\n    desc[a] = name:gsub(\"_\", \" \")\n    if self.opts.actions and self.opts.actions[name] then\n      local action = self.opts.actions[name]\n      desc[a] = type(action) == \"table\" and action.desc and action.desc or desc[a]\n    end\n  end\n  return function()\n    for _, name in ipairs(actions) do\n      if self.opts.actions and self.opts.actions[name] then\n        local a = self.opts.actions[name]\n        local fn = type(a) == \"function\" and a or a.action\n        local ret = fn(self)\n        if ret then\n          return type(ret) == \"string\" and ret or nil\n        end\n      elseif self[name] then\n        self[name](self)\n        return\n      else\n        return name\n      end\n    end\n  end,\n    table.concat(desc, \", \")\nend\n\n---@param opts? {col_width?: number, key_width?: number, win?: snacks.win.Config}\nfunction M:toggle_help(opts)\n  opts = opts or {}\n  local col_width, key_width = opts.col_width or 30, opts.key_width or 10\n  for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do\n    local buf = vim.api.nvim_win_get_buf(win)\n    if vim.bo[buf].filetype == \"snacks_win_help\" then\n      vim.api.nvim_win_close(win, true)\n      return\n    end\n  end\n  local ns = vim.api.nvim_create_namespace(\"snacks.win.help\")\n  local win = M.new(M.resolve({ style = \"help\" }, opts.win or {}, {\n    show = false,\n    focusable = false,\n    zindex = self.opts.zindex + 1,\n    bo = { filetype = \"snacks_win_help\" },\n  }))\n  self:on(\"WinClosed\", function()\n    win:close()\n  end, { win = true })\n  self:on(\"BufLeave\", function()\n    win:close()\n  end, { buf = true })\n  local dim = win:dim()\n\n  -- NOTE: we use the actual buffer keymaps instead of self.keys,\n  -- since we want to show all keymaps, not just the ones we've defined on the window\n  local keys = {} ---@type vim.api.keyset.get_keymap[]\n  vim.list_extend(keys, vim.api.nvim_buf_get_keymap(self.buf, \"n\"))\n  vim.list_extend(keys, vim.api.nvim_buf_get_keymap(self.buf, \"i\"))\n  table.sort(keys, function(a, b)\n    return (a.desc or a.lhs or \"\") < (b.desc or b.lhs or \"\")\n  end)\n\n  local done = {} ---@type table<string, boolean>\n  keys = vim.tbl_filter(function(keymap)\n    local key = Snacks.util.normkey(keymap.lhs or \"\")\n    if done[key] or (keymap.desc and keymap.desc:find(\"which%-key\")) then\n      return false\n    end\n    done[key] = true\n    return true\n  end, keys)\n\n  local cols = math.floor((dim.width - 1) / col_width)\n  local rows = math.ceil(#keys / cols)\n  win.opts.height = rows\n  local help = {} ---@type {[1]:string, [2]:string}[][]\n  local row, col = 0, 1\n\n  ---@param str string\n  ---@param len number\n  ---@param align? \"left\"|\"right\"\n  local function trunc(str, len, align)\n    local w = vim.api.nvim_strwidth(str)\n    if w > len then\n      return vim.fn.strcharpart(str, 0, len - 1) .. \"…\"\n    end\n    return align == \"right\" and (string.rep(\" \", len - w) .. str) or (str .. string.rep(\" \", len - w))\n  end\n\n  for _, keymap in ipairs(keys) do\n    local key = Snacks.util.normkey(keymap.lhs or \"\")\n    row = row + 1\n    if row > rows then\n      row, col = 1, col + 1\n    end\n    help[row] = help[row] or {}\n    vim.list_extend(help[row], {\n      { trunc(key, key_width, \"right\"), \"SnacksWinKey\" },\n      { \" \" },\n      { \"➜\", \"SnacksWinKeySep\" },\n      { \" \" },\n      { trunc(keymap.desc or \"\", col_width - key_width - 3), \"SnacksWinKeyDesc\" },\n    })\n  end\n  win:show()\n  for l, line in ipairs(help) do\n    vim.api.nvim_buf_set_lines(win.buf, l - 1, l, false, { \"\" })\n    vim.api.nvim_buf_set_extmark(win.buf, ns, l - 1, 0, {\n      virt_text = line,\n      virt_text_pos = \"overlay\",\n    })\n  end\nend\n\n---@param event string|string[]\n---@param cb fun(self: snacks.win, ev:vim.api.keyset.create_autocmd.callback_args):boolean?\n---@param opts? snacks.win.Event\nfunction M:on(event, cb, opts)\n  opts = opts or {}\n  opts.callback = cb\n  table.insert(self.events, vim.tbl_extend(\"keep\", { event = event }, opts))\n  if self:valid() then\n    self:_on(event, opts)\n  end\nend\n\n---@param event string|string[]\n---@param opts snacks.win.Event\nfunction M:_on(event, opts)\n  local event_opts = {} ---@type vim.api.keyset.create_autocmd\n  local skip = { \"buf\", \"win\", \"event\" }\n  for k, v in pairs(opts) do\n    if not vim.tbl_contains(skip, k) then\n      event_opts[k] = v\n    end\n  end\n  event_opts.group = event_opts.group or self.augroup\n  event_opts.callback = function(ev)\n    table.insert(event_stack, ev.event)\n    local ok, err = pcall(opts.callback, self, ev)\n    table.remove(event_stack)\n    return not ok and error(err) or err\n  end\n  if event_opts.pattern or event_opts.buffer then\n    -- don't alter the pattern or buffer\n  elseif opts.win then\n    event_opts.pattern = self.win .. \"\"\n  elseif opts.buf then\n    event_opts.buffer = self.buf\n  end\n  vim.api.nvim_create_autocmd(event, event_opts)\nend\n\nfunction M:focus()\n  if self:valid() then\n    vim.api.nvim_set_current_win(self.win)\n  end\nend\n\nfunction M:redraw()\n  if vim.api.nvim__redraw then\n    vim.api.nvim__redraw({ win = self.win, valid = false, flush = true, cursor = false })\n  else\n    vim.cmd(\"redraw\")\n  end\nend\n\n---@param left? boolean\nfunction M:hscroll(left)\n  vim.api.nvim_win_call(self.win, function()\n    vim.cmd((\"normal! %s\"):format(left and \"zh\" or \"zl\"))\n  end)\nend\n\n---@param up? boolean\nfunction M:scroll(up)\n  vim.api.nvim_win_call(self.win, function()\n    vim.cmd((\"normal! %d%s\"):format(vim.wo[self.win].scroll, up and SCROLL_UP or SCROLL_DOWN))\n  end)\nend\n\nfunction M:destroy()\n  pcall(function()\n    self:close()\n  end)\n  self.events = {}\n  self.keys = {}\n  self.meta = {}\n  -- self.opts = {}\nend\n\n---@param opts? { buf: boolean }\nfunction M:close(opts)\n  opts = opts or {}\n  local wipe = opts.buf ~= false and self.buf == self.scratch_buf\n\n  local win = self.win\n  local buf = wipe and self.buf\n  local scratch_buf = self.scratch_buf ~= self.buf and self.scratch_buf or nil\n  self:on_close()\n\n  self.win = nil\n  if scratch_buf then\n    self.scratch_buf = nil\n  end\n  if buf then\n    self.buf = nil\n  end\n\n  local close = function()\n    local errors = {} ---@type string[]\n    if win and vim.api.nvim_win_is_valid(win) then\n      local ok, err = pcall(vim.api.nvim_win_close, win, true)\n      if not ok and (err and err:find(\"E444\")) then\n        -- last window, so creat a split and close it again\n        vim.cmd(\"silent! vsplit\")\n        pcall(vim.api.nvim_win_close, win, true)\n      elseif not ok then\n        errors[#errors + 1] = err\n      end\n    end\n    if buf and vim.api.nvim_buf_is_valid(buf) then\n      local ok, err = pcall(vim.api.nvim_buf_delete, buf, { force = true })\n      errors[#errors + 1] = not ok and err or nil\n    end\n    if scratch_buf and vim.api.nvim_buf_is_valid(scratch_buf) then\n      local ok, err = pcall(vim.api.nvim_buf_delete, scratch_buf, { force = true })\n      errors[#errors + 1] = not ok and err or nil\n    end\n    if self.augroup then\n      pcall(vim.api.nvim_del_augroup_by_id, self.augroup)\n      self.augroup = nil\n    end\n    if #errors > 0 then\n      error(table.concat(errors, \"\\n\"))\n    end\n  end\n  local retries = 0\n  local try_close ---@type fun()\n  try_close = function()\n    local ok, err = pcall(close)\n    if ok or not err then\n      return\n    end\n\n    -- command window is open\n    if err:find(\"E11\") then\n      vim.defer_fn(try_close, 200)\n      return\n    end\n\n    -- text lock\n    if err:find(\"E565\") and retries < 20 then\n      retries = retries + 1\n      vim.defer_fn(try_close, 50)\n      return\n    end\n\n    if not ok then\n      Snacks.notify.error(\"Failed to close window: \" .. err)\n    end\n  end\n  -- HACK: WinClosed is not recursive, so we need to schedule it\n  -- if we're in a WinClosed event\n  if vim.tbl_contains(event_stack, \"WinClosed\") or not pcall(close) then\n    vim.schedule(try_close)\n  end\nend\n\nfunction M:hide()\n  self:close({ buf = false })\n  return self\nend\n\nfunction M:toggle()\n  if self:valid() then\n    self:hide()\n  else\n    self:show()\n  end\n  return self\nend\n\n---@param title string|{[1]:string, [2]:string}[]\n---@param pos? \"center\"|\"left\"|\"right\"\nfunction M:set_title(title, pos)\n  if not self:has_border() then\n    return\n  end\n  if type(title) == \"string\" then\n    title = vim.trim(title)\n    if title ~= \"\" then\n      -- HACK: add extra space when last char is non word\n      -- like for icons etc\n      if not title:sub(-1):match(\"%w\") then\n        title = title .. \" \"\n      end\n      title = \" \" .. title .. \" \"\n    end\n  elseif #title == 0 then\n    title = \"\"\n  end\n  pos = pos or self.opts.title_pos or \"center\"\n  if vim.deep_equal(self.opts.title, title) and self.opts.title_pos == pos then\n    return\n  end\n  self.opts.title = title\n  self.opts.title_pos = pos\n  if not self:valid() then\n    return\n  end\n  -- Don't try to update if the relative window is invalid.\n  -- It will be fixed once a full update is done.\n  local relative_win = vim.api.nvim_win_get_config(self.win).win\n  if relative_win and not vim.api.nvim_win_is_valid(relative_win) then\n    return\n  end\n  vim.api.nvim_win_set_config(self.win, {\n    title = self.opts.title,\n    title_pos = self.opts.title_pos,\n  })\nend\n\n---@private\nfunction M:open_buf()\n  if self.buf and vim.api.nvim_buf_is_valid(self.buf) then\n    -- keep existing buffer\n    self.buf = self.buf\n  elseif self.scratch_buf and vim.api.nvim_buf_is_valid(self.scratch_buf) then\n    -- keep existing scratch buffer\n    self.buf = self.scratch_buf\n  elseif self.opts.file then\n    self.buf = vim.fn.bufadd(self.opts.file)\n    if not vim.api.nvim_buf_is_loaded(self.buf) then\n      vim.bo[self.buf].readonly = true\n      vim.bo[self.buf].swapfile = false\n      vim.fn.bufload(self.buf)\n      vim.bo[self.buf].modifiable = false\n    end\n  elseif self.opts.buf then\n    self.buf = self.opts.buf\n  else\n    self:scratch()\n  end\n  return self.buf\nend\n\nfunction M:scratch()\n  if self.buf == self.scratch_buf and self:buf_valid() then\n    return\n  end\n  self.buf = vim.api.nvim_create_buf(false, true)\n  vim.bo[self.buf].swapfile = false\n  self.scratch_buf = self.buf\n  local text = type(self.opts.text) == \"function\" and self.opts.text() or self.opts.text\n  text = type(text) == \"string\" and vim.split(text, \"\\n\") or text\n  if text then\n    ---@cast text string[]\n    vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, text)\n  end\n  if not self.opts.bo.filetype then\n    if self.opts.scratch_ft then\n      vim.bo[self.buf].filetype = self.opts.scratch_ft\n    else\n      vim.bo[self.buf].filetype = self.opts.bo.filetype or \"snacks_win\"\n    end\n    vim.bo[self.buf].syntax = \"\"\n  end\n  if self:win_valid() then\n    vim.api.nvim_win_set_buf(self.win, self.buf)\n  end\nend\n\n---@private\nfunction M:open_win()\n  local relative = self.opts.relative or \"editor\"\n  local position = self.opts.position or \"float\"\n  local enter = self.opts.enter == nil or self.opts.enter or false\n  if self.opts.focusable == false then\n    enter = false\n  end\n  local opts = self:win_opts()\n  if position == \"float\" then\n    self.win = vim.api.nvim_open_win(self.buf, enter, opts)\n  elseif position == \"current\" then\n    self.win = vim.api.nvim_get_current_win()\n    vim.api.nvim_win_set_buf(self.win, self.buf)\n  else --split\n    local parent = self.opts.win and vim.api.nvim_win_is_valid(self.opts.win) and self.opts.win or 0\n    local vertical = position == \"left\" or position == \"right\"\n    -- When stacking is enabled, find an existing window with the same relative/position\n    -- and stack the new window perpendicular to it instead of creating a new split\n    if parent == 0 and self.opts.stack then\n      for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do\n        if\n          vim.w[win].snacks_win\n          and vim.w[win].snacks_win.relative == relative\n          and vim.w[win].snacks_win.position == position\n          and vim.w[win].snacks_win.stack == true\n        then\n          parent = win\n          relative = \"win\"\n          position = vertical and \"bottom\" or \"right\"\n          vertical = not vertical\n          break\n        end\n      end\n    end\n    local cmd = split_commands[relative][position]\n    local size = vertical and opts.width or opts.height\n    local resize = (\"%sresize %s\"):format(vertical and \"vertical \" or \"\", size)\n    vim.api.nvim_win_call(parent, function()\n      vim.cmd(\"silent noswapfile \" .. cmd .. \" sbuffer \" .. self.buf .. \" | \" .. resize)\n      self.win = vim.api.nvim_get_current_win()\n    end)\n    if enter then\n      vim.api.nvim_set_current_win(self.win)\n    end\n    vim.schedule(function()\n      self:equalize()\n    end)\n  end\n  vim.w[self.win].snacks_win = {\n    id = self.id,\n    position = self.opts.position,\n    relative = self.opts.relative,\n    stack = self.opts.stack,\n  }\nend\n\n---@private\nfunction M:equalize()\n  if self:is_floating() then\n    return\n  end\n  local all = vim.tbl_filter(function(win)\n    return vim.w[win].snacks_win\n      and vim.w[win].snacks_win.relative == self.opts.relative\n      and vim.w[win].snacks_win.position == self.opts.position\n  end, vim.api.nvim_tabpage_list_wins(0))\n  if #all <= 1 then\n    return\n  end\n  local vertical = self.opts.position == \"left\" or self.opts.position == \"right\"\n  local parent_size = self:parent_size()[vertical and \"height\" or \"width\"]\n  local size = math.floor(parent_size / #all)\n  for _, win in ipairs(all) do\n    vim.api.nvim_win_call(win, function()\n      vim.cmd((\"%s resize %s\"):format(vertical and \"horizontal\" or \"vertical\", size))\n    end)\n  end\nend\n\nfunction M:update()\n  if self:valid() then\n    Snacks.util.bo(self.buf, self.opts.bo)\n    Snacks.util.wo(self.win, self.opts.wo)\n    if self:is_floating() then\n      local opts = self:win_opts()\n      opts.noautocmd = nil\n      vim.api.nvim_win_set_config(self.win, opts)\n    end\n  end\nend\n\nfunction M:on_current_tab()\n  return self:win_valid() and vim.api.nvim_get_current_tabpage() == vim.api.nvim_win_get_tabpage(self.win)\nend\n\nfunction M:show()\n  if self:valid() then\n    self:update()\n    return self\n  end\n  self.augroup = vim.api.nvim_create_augroup(\"snacks_win_\" .. self.id, { clear = true })\n\n  self:open_buf()\n\n  -- buffer local variables\n  for k, v in pairs(self.opts.b or {}) do\n    vim.b[self.buf][k] = v\n  end\n\n  -- OPTIM: prevent treesitter or syntax highlighting to attach on FileType if it's not already enabled\n  local optim_hl = not vim.b[self.buf].ts_highlight and vim.bo[self.buf].syntax == \"\"\n  vim.b[self.buf].ts_highlight = optim_hl or vim.b[self.buf].ts_highlight\n  Snacks.util.bo(self.buf, self.opts.bo)\n  vim.b[self.buf].ts_highlight = not optim_hl and vim.b[self.buf].ts_highlight or nil\n\n  if self.opts.on_buf then\n    self.opts.on_buf(self)\n  end\n\n  if self.opts.footer_keys then\n    self.opts.footer = {}\n    table.sort(self.keys, function(a, b)\n      return a[1] < b[1]\n    end)\n    local want = type(self.opts.footer_keys) == \"table\" and self.opts.footer_keys or nil\n    ---@cast want string[]|nil\n    want = want and vim.tbl_map(Snacks.util.normkey, want) or nil --[[@as string[]?]]\n    for _, key in ipairs(self.keys) do\n      local keymap = Snacks.util.normkey(key[1])\n      if want == nil or vim.tbl_contains(want, keymap) then\n        table.insert(self.opts.footer, { \" \", \"SnacksFooter\" })\n        table.insert(self.opts.footer, { \" \" .. keymap .. \" \", \"SnacksFooterKey\" })\n        table.insert(self.opts.footer, { \" \" .. (key.desc or keymap) .. \" \", \"SnacksFooterDesc\" })\n      end\n    end\n    table.insert(self.opts.footer, { \" \", \"SnacksFooter\" })\n  end\n\n  self:open_win()\n  self.closed = false\n  -- window local variables\n  for k, v in pairs(self.opts.w or {}) do\n    vim.w[self.win][k] = v\n  end\n  if Snacks.util.is_transparent() then\n    self.opts.wo.winblend = 0\n  end\n  Snacks.util.wo(self.win, self.opts.wo)\n  if self.opts.on_win then\n    self.opts.on_win(self)\n  end\n\n  -- syntax highlighting\n  local ft = self.opts.ft or vim.bo[self.buf].filetype\n  if ft and not ft:find(\"^snacks_\") and not vim.b[self.buf].ts_highlight and vim.bo[self.buf].syntax == \"\" then\n    local lang = vim.treesitter.language.get_lang(ft)\n    if not (lang and pcall(vim.treesitter.start, self.buf, lang)) then\n      vim.bo[self.buf].syntax = ft\n    end\n  end\n\n  for _, event in ipairs(self.events) do\n    self:_on(event.event, event)\n  end\n\n  -- swap buffers when opening a new buffer in the same window\n  vim.api.nvim_create_autocmd(\"BufWinEnter\", {\n    group = self.augroup,\n    nested = true,\n    callback = function()\n      return self:fixbuf()\n    end,\n  })\n\n  self:map()\n  self:drop()\n\n  return self\nend\n\nfunction M:fixbuf()\n  -- window closes, so delete the autocmd\n  if not self:win_valid() then\n    return true\n  end\n\n  if not self:buf_valid() then\n    return\n  end\n\n  if not self:on_current_tab() then\n    return\n  end\n\n  local buf = vim.api.nvim_win_get_buf(self.win)\n\n  -- same buffer\n  if buf == self.buf then\n    return\n  end\n\n  -- don't swap if fixbuf is disabled\n  if self.opts.fixbuf == false then\n    self.buf = buf\n    -- update window options\n    Snacks.util.wo(self.win, self.opts.wo)\n    return\n  end\n\n  -- another buffer was opened in this window\n  -- find another window to swap with\n  local main ---@type number?\n  for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do\n    local win_buf = vim.api.nvim_win_get_buf(win)\n    local is_float = vim.api.nvim_win_get_config(win).zindex ~= nil\n    if win ~= self.win and not is_float then\n      if vim.bo[win_buf].buftype == \"\" or vim.b[win_buf].snacks_main or vim.w[win].snacks_main then\n        main = win\n        break\n      end\n    end\n  end\n\n  if main then\n    vim.api.nvim_win_set_buf(self.win, self.buf)\n    vim.api.nvim_win_set_buf(main, buf)\n    vim.api.nvim_set_current_win(main)\n    vim.cmd.stopinsert()\n  else\n    -- no main window found, so close this window\n    vim.api.nvim_win_set_buf(self.win, self.buf)\n    vim.schedule(function()\n      vim.cmd.stopinsert()\n      vim.cmd(\"sbuffer \" .. buf)\n      if self.win and vim.api.nvim_win_is_valid(self.win) then\n        vim.api.nvim_win_close(self.win, true)\n      end\n    end)\n  end\nend\n\n---@param buf number\nfunction M:set_buf(buf)\n  assert(self:valid(), \"Window is not valid\")\n  self.buf = buf\n  vim.api.nvim_win_set_buf(self.win, buf)\n  Snacks.util.wo(self.win, self.opts.wo)\nend\n\nfunction M:map()\n  if not self:buf_valid() then\n    return\n  end\n  for _, spec in pairs(self.keys) do\n    local opts = vim.deepcopy(spec)\n    opts[1] = nil\n    opts[2] = nil\n    opts.mode = nil\n    ---@diagnostic disable-next-line: cast-type-mismatch\n    ---@cast opts vim.keymap.set.Opts\n    opts.buffer = self.buf\n    opts.nowait = true\n    local rhs = spec[2]\n    local is_action = type(rhs) == \"string\" or type(rhs) == \"table\"\n    if is_action then\n      local desc = spec.desc\n      ---@cast rhs string|string[]\n      rhs, desc = self:action(rhs)\n      opts.desc = opts.desc or desc\n    else\n      rhs = function()\n        return spec[2](self)\n      end\n    end\n    spec.desc = spec.desc or opts.desc\n    ---@cast spec snacks.win.Keys\n    vim.keymap.set(spec.mode or \"n\", spec[1], rhs, opts)\n  end\nend\n\n---@private\nfunction M:on_close()\n  -- close the backdrop\n  if self.backdrop then\n    self.backdrop:close()\n    self.backdrop = nil\n  end\n  if self.closed then\n    return\n  end\n  self.closed = true\n  if self.opts.on_close then\n    self.opts.on_close(self)\n  end\n  -- Go back to the previous window when closing,\n  -- and it's the current window\n  if vim.api.nvim_get_current_win() == self.win then\n    pcall(vim.cmd.wincmd, \"p\")\n  end\nend\n\nfunction M:add_padding()\n  local listchars = vim.split(self.opts.wo.listchars or \"\", \",\")\n  listchars = vim.tbl_filter(function(s)\n    return not s:find(\"eol:\") and s ~= \"\"\n  end, listchars)\n  table.insert(listchars, \"eol: \")\n  self.opts.wo.listchars = table.concat(listchars, \",\")\n  self.opts.wo.list = true\n  self.opts.wo.statuscolumn = \" \"\nend\n\nfunction M:is_floating()\n  return self:valid() and vim.api.nvim_win_get_config(self.win).zindex ~= nil\nend\n\n---@private\nfunction M:drop()\n  if self.backdrop then\n    self.backdrop:close()\n    self.backdrop = nil\n  end\n  local backdrop = self.opts.backdrop\n  if not backdrop then\n    return\n  end\n  backdrop = type(backdrop) == \"number\" and { blend = backdrop } or backdrop\n  backdrop = backdrop == true and {} or backdrop\n  backdrop = vim.tbl_extend(\"force\", { bg = \"#000000\", blend = 60, transparent = true }, backdrop)\n  ---@cast backdrop snacks.win.Backdrop\n\n  if\n    (Snacks.util.is_transparent() and backdrop.transparent)\n    or not vim.o.termguicolors\n    or backdrop.blend == 100\n    or not self:is_floating()\n  then\n    return\n  end\n\n  local bg, winblend = backdrop.bg or \"#000000\", backdrop.blend\n  if not backdrop.transparent then\n    if Snacks.util.is_transparent() then\n      bg = nil\n    else\n      bg = Snacks.util.blend(Snacks.util.color(\"Normal\", \"bg\"), bg, winblend / 100)\n    end\n    winblend = 0\n  end\n\n  local group = (\"SnacksBackdrop_%s\"):format(bg and bg:sub(2) or \"T\")\n  vim.api.nvim_set_hl(0, group, { bg = bg })\n\n  self.backdrop = M.new(M.resolve({\n    enter = false,\n    backdrop = false,\n    relative = \"editor\",\n    height = 0,\n    width = 0,\n    style = \"minimal\",\n    border = \"none\",\n    focusable = false,\n    zindex = self.opts.zindex - 1,\n    wo = {\n      winhighlight = \"Normal:\" .. group,\n      winblend = winblend,\n      colorcolumn = \"\",\n    },\n    bo = {\n      buftype = \"nofile\",\n      filetype = \"snacks_win_backdrop\",\n    },\n  }, backdrop.win))\nend\n\nfunction M:line(line)\n  return self:lines(line, line)[1] or \"\"\nend\n\n---@param from? number 1-indexed, inclusive\n---@param to? number 1-indexed, inclusive\nfunction M:lines(from, to)\n  return self:buf_valid() and vim.api.nvim_buf_get_lines(self.buf, from and from - 1 or 0, to or -1, false) or {}\nend\n\n---@param from? number 1-indexed, inclusive\n---@param to? number 1-indexed, inclusive\nfunction M:text(from, to)\n  return table.concat(self:lines(from, to), \"\\n\")\nend\n\n---@return { height: number, width: number }\nfunction M:parent_size()\n  if self.opts.relative == \"win\" and vim.api.nvim_win_is_valid(self.opts.win) then\n    return {\n      height = vim.api.nvim_win_get_height(self.opts.win),\n      width = vim.api.nvim_win_get_width(self.opts.win),\n    }\n  end\n  return {\n    height = vim.o.lines,\n    width = vim.o.columns,\n  }\nend\n\n---@private\nfunction M:win_opts()\n  local opts = {} ---@type vim.api.keyset.win_config\n  for _, k in ipairs(win_opts) do\n    opts[k] = self.opts[k]\n  end\n\n  local border = self:border()\n\n  opts.border = border and (borders[border] or border) or \"none\"\n\n  if opts.relative == \"cursor\" then\n    self.opts.row = self.opts.row or 0\n    self.opts.col = self.opts.col or 0\n  end\n\n  local dim = self:dim()\n  opts.height, opts.width = dim.height, dim.width\n  opts.row, opts.col = dim.row, dim.col\n\n  if vim.fn.has(\"nvim-0.10\") == 0 then\n    opts.footer, opts.footer_pos = nil, nil\n  end\n\n  if border then\n    opts.title_pos = opts.title and (opts.title_pos or \"center\") or nil\n    opts.footer_pos = opts.footer and (opts.footer_pos or \"center\") or nil\n  else\n    opts.title, opts.footer = nil, nil\n    opts.title_pos, opts.footer_pos = nil, nil\n  end\n\n  return opts\nend\n\n---@return { height: number, width: number }\nfunction M:size()\n  local opts = self:win_opts()\n  local height = opts.height\n  local width = opts.width\n  if self:has_border() then\n    height = height + 2\n    width = width + 2\n  end\n  return { height = height, width = width }\nend\n\nfunction M:has_border()\n  return self:border() ~= nil\nend\n\nfunction M.is_border(border)\n  return border and border ~= \"\" and border ~= \"none\"\nend\n\nfunction M:border()\n  if not M.is_border(self.opts.border) then\n    return\n  end\n\n  if self.opts.border == true then\n    local border ---@type string|string[]|nil\n    pcall(function()\n      border = vim.o.winborder\n      border = border:find(\",\") and vim.split(border, \",\") or border\n    end)\n    return M.is_border(border) and border or \"rounded\"\n  end\n  return self.opts.border\nend\n\n--- Calculate the size of the border\nfunction M:border_size()\n  -- The array specifies the eight\n  -- chars building up the border in a clockwise fashion\n  -- starting with the top-left corner.\n  -- { \"╔\", \"═\" ,\"╗\", \"║\", \"╝\", \"═\", \"╚\", \"║\" }\n  local border = self:border() or { \"\" }\n  border = type(border) == \"string\" and borders[border] or border\n  border = type(border) == \"string\" and { \"x\" } or border\n  assert(type(border) == \"table\", \"Invalid border type\")\n  ---@cast border string[]\n  while #border < 8 do\n    vim.list_extend(border, border)\n  end\n  -- remove border hl groups\n  border = vim.tbl_map(function(b)\n    return type(b) == \"table\" and b[1] or b\n  end, border)\n  local function size(from, to)\n    for i = from, to do\n      if border[i] ~= \"\" then\n        return 1\n      end\n    end\n    return 0\n  end\n  ---@type { top: number, right: number, bottom: number, left: number }\n  return {\n    top = size(1, 3),\n    right = size(3, 5),\n    bottom = size(5, 7),\n    left = math.max(size(7, 8), size(1, 1)),\n  }\nend\n\nfunction M:border_text_width()\n  if not self:has_border() then\n    return 0\n  end\n  local ret = 0\n  for _, t in ipairs({ \"title\", \"footer\" }) do\n    local str = self.opts[t] or {}\n    str = type(str) == \"string\" and { str } or str\n    ---@cast str (string|string[])[]\n    ret = math.max(ret, #table.concat(\n      vim.tbl_map(function(s)\n        return type(s) == \"string\" and s or s[1]\n      end, str),\n      \"\"\n    ))\n  end\n  return ret\nend\n\nfunction M:buf_valid()\n  return self.buf and vim.api.nvim_buf_is_valid(self.buf)\nend\n\nfunction M:win_valid()\n  return self.win and vim.api.nvim_win_is_valid(self.win)\nend\n\nfunction M:valid()\n  return self:win_valid() and self:buf_valid() and vim.api.nvim_win_get_buf(self.win) == self.buf\nend\n\n---@param parent? snacks.win.Dim\nfunction M:dim(parent)\n  parent = parent or self:parent_size()\n  ---@type snacks.win.Dim\n  local ret = {\n    height = 0,\n    width = 0,\n    col = 0,\n    row = 0,\n    border = self:has_border(),\n  }\n\n  ---@param s? number|fun(win:snacks.win):number? size\n  ---@param ps number parent size\n  local function size(s, ps, border_offset)\n    s = type(s) == \"function\" and s(self) or s or 0\n    ---@cast s number\n    if s == 0 then -- full size\n      return ps - border_offset\n    elseif s < 1 then -- relative size\n      return math.floor(ps * s) - border_offset\n    end\n    return s\n  end\n\n  ---@param p? number|fun(win:snacks.win):number? pos\n  ---@param s number size\n  ---@param ps number parent size\n  local function pos(p, s, ps, border_from, border_to)\n    p = type(p) == \"function\" and p(self) or p\n    ---@cast p number?\n    if self.opts.relative == \"cursor\" then\n      return p or 0\n    end\n    if not p then -- center\n      return math.floor((ps - s) / 2) - border_from\n    end\n    ---@cast p number\n    if p < 0 then -- negative position\n      return ps - s + p - border_from - border_to\n    elseif p < 1 and p > 0 then -- relative position\n      return math.floor(ps * p) + border_from\n    end\n    return p\n  end\n\n  local border = self:border_size()\n\n  ret.height = size(self.opts.height, parent.height, border.top + border.bottom)\n  ret.height = math.max(ret.height, self.opts.min_height or 0, 1)\n  ret.height = math.min(ret.height, self.opts.max_height or ret.height, parent.height)\n  ret.height = math.max(ret.height, 1)\n\n  ret.width = size(self.opts.width, parent.width, border.left + border.right)\n  ret.width = math.max(ret.width, self.opts.min_width or 0, 1)\n  ret.width = math.min(ret.width, self.opts.max_width or ret.width, parent.width)\n  ret.width = math.max(ret.width, 1)\n\n  ret.row = pos(self.opts.row, ret.height, parent.height, border.top, border.bottom)\n  ret.col = pos(self.opts.col, ret.width, parent.width, border.left, border.right)\n\n  return ret\nend\n\n--- Calculate the next available zindex for snacks windows.\n--- New windows open on top of existing ones.\n---@param opts? { zindex?: number, tab?: number|boolean, all?: boolean, max?: number }\n---@overload fun(zindex: number): number\nfunction M.zindex(opts)\n  opts = opts or {}\n  opts = type(opts) == \"number\" and { zindex = opts } or opts\n  local zindex = opts.zindex or 50\n  local max = opts.max or 100\n  local wins = opts.tab == false and vim.api.nvim_list_wins() or vim.api.nvim_tabpage_list_wins(tonumber(opts.tab) or 0)\n  for _, win in ipairs(wins) do\n    if opts.all ~= false or vim.w[win].snacks_win then\n      local other = (vim.api.nvim_win_get_config(win).zindex or 0)\n      -- ignore very high zindex windows, like notifications, completion, etc\n      if other > zindex and other < max then\n        zindex = math.max(zindex, other + 2) --[[@as number]]\n      end\n    end\n  end\n  return zindex\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/words.lua",
    "content": "---@class snacks.words\nlocal M = {}\n\nM.meta = {\n  desc = \"Auto-show LSP references and quickly navigate between them\",\n  needs_setup = true,\n}\n\n---@private\n---@alias LspWord {from:{[1]:number, [2]:number}, to:{[1]:number, [2]:number}} 1-0 indexed\n\n---@class snacks.words.Config\n---@field enabled? boolean\nlocal defaults = {\n  debounce = 200, -- time in ms to wait before updating\n  notify_jump = false, -- show a notification when jumping\n  notify_end = true, -- show a notification when reaching the end\n  foldopen = true, -- open folds after jumping\n  jumplist = true, -- set jump point before jumping\n  modes = { \"n\", \"i\", \"c\" }, -- modes to show references\n  filter = function(buf) -- what buffers to enable `snacks.words`\n    return vim.g.snacks_words ~= false and vim.b[buf].snacks_words ~= false\n  end,\n}\n\nM.enabled = false\n\nlocal config = Snacks.config.get(\"words\", defaults)\nlocal ns = vim.api.nvim_create_namespace(\"vim_lsp_references\")\nlocal ns2 = vim.api.nvim_create_namespace(\"nvim.lsp.references\")\nlocal timer = (vim.uv or vim.loop).new_timer()\n\nfunction M.enable()\n  if M.enabled then\n    return\n  end\n  M.enabled = true\n  local group = vim.api.nvim_create_augroup(\"snacks_words\", { clear = true })\n\n  vim.api.nvim_create_autocmd({ \"CursorMoved\", \"CursorMovedI\", \"ModeChanged\" }, {\n    group = group,\n    callback = function()\n      if not M.is_enabled({ modes = true }) then\n        M.clear()\n        return\n      end\n      if not ({ M.get() })[2] then\n        M.update()\n      end\n    end,\n  })\nend\n\nfunction M.disable()\n  if not M.enabled then\n    return\n  end\n  M.enabled = false\n  vim.api.nvim_del_augroup_by_name(\"snacks_words\")\n  for _, buf in ipairs(vim.api.nvim_list_bufs()) do\n    vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)\n    vim.api.nvim_buf_clear_namespace(buf, ns2, 0, -1)\n  end\nend\n\nfunction M.clear()\n  vim.lsp.buf.clear_references()\nend\n\n---@private\nfunction M.update()\n  local buf = vim.api.nvim_get_current_buf()\n  timer:start(config.debounce, 0, function()\n    vim.schedule(function()\n      if vim.api.nvim_buf_is_valid(buf) then\n        vim.api.nvim_buf_call(buf, function()\n          if not M.is_enabled({ modes = true }) then\n            return\n          end\n          vim.lsp.buf.document_highlight()\n          M.clear()\n        end)\n      end\n    end)\n  end)\nend\n\n---@param opts? number|{buf?:number, modes:boolean} if modes is true, also check if the current mode is enabled\nfunction M.is_enabled(opts)\n  if not M.enabled then\n    return false\n  end\n  opts = type(opts) == \"number\" and { buf = opts } or opts or {}\n\n  if opts.modes then\n    local mode = vim.api.nvim_get_mode().mode:lower()\n    mode = mode:gsub(\"\\22\", \"v\"):gsub(\"\\19\", \"s\")\n    mode = mode:sub(1, 2) == \"no\" and \"o\" or mode\n    mode = mode:sub(1, 1):match(\"[ncitsvo]\") or \"n\"\n    if not vim.tbl_contains(config.modes, mode) then\n      return false\n    end\n  end\n\n  local buf = opts.buf or vim.api.nvim_get_current_buf()\n  if not config.filter(buf) then\n    return false\n  end\n\n  local clients = {} ---@type vim.lsp.Client[]\n  if vim.fn.has(\"nvim-0.11\") == 1 then\n    clients = vim.lsp.get_clients({ bufnr = buf, method = \"textDocument/documentHighlight\" })\n  else\n    clients = (vim.lsp.get_clients or vim.lsp.get_active_clients)({ bufnr = buf })\n    clients = vim.tbl_filter(function(client)\n      return client.supports_method(\"textDocument/documentHighlight\", { bufnr = buf })\n    end, clients)\n  end\n\n  return #clients > 0\nend\n\n---@private\n---@return LspWord[] words, number? current\nfunction M.get()\n  local cursor = vim.api.nvim_win_get_cursor(0)\n  local current, ret = nil, {} ---@type number?, LspWord[]\n  local extmarks = {} ---@type vim.api.keyset.get_extmark_item[]\n  vim.list_extend(extmarks, vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, { details = true }))\n  vim.list_extend(extmarks, vim.api.nvim_buf_get_extmarks(0, ns2, 0, -1, { details = true }))\n  for _, extmark in ipairs(extmarks) do\n    local w = {\n      from = { extmark[2] + 1, extmark[3] },\n      to = { extmark[4].end_row + 1, extmark[4].end_col },\n    }\n    ret[#ret + 1] = w\n    if cursor[1] >= w.from[1] and cursor[1] <= w.to[1] and cursor[2] >= w.from[2] and cursor[2] <= w.to[2] then\n      current = #ret\n    end\n  end\n  return ret, current\nend\n\n---@param count? number\n---@param cycle? boolean\nfunction M.jump(count, cycle)\n  count = count or 1\n  local words, idx = M.get()\n  if not idx then\n    return\n  end\n  idx = idx + count\n  if cycle then\n    idx = (idx - 1) % #words + 1\n  end\n  local target = words[idx]\n  if target then\n    if config.jumplist then\n      vim.cmd.normal({ \"m`\", bang = true })\n    end\n    vim.api.nvim_win_set_cursor(0, target.from)\n    if config.notify_jump then\n      Snacks.notify.info((\"Reference [%d/%d]\"):format(idx, #words), { id = \"snacks.words.jump\", title = \"Words\" })\n    end\n    if config.foldopen then\n      vim.cmd.normal({ \"zv\", bang = true })\n    end\n  elseif config.notify_end then\n    Snacks.notify.warn(\"No more references\", { id = \"snacks.words.jump\", title = \"Words\" })\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/snacks/zen.lua",
    "content": "---@class snacks.zen\n---@overload fun(opts: snacks.zen.Config): snacks.win\nlocal M = setmetatable({}, {\n  __call = function(M, ...)\n    return M.zen(...)\n  end,\n})\n\nM.meta = {\n  desc = \"Zen mode • distraction-free coding\",\n}\n\n---@class snacks.zen.Config\nlocal defaults = {\n  -- You can add any `Snacks.toggle` id here.\n  -- Toggle state is restored when the window is closed.\n  -- Toggle config options are NOT merged.\n  ---@type table<string, boolean>\n  toggles = {\n    dim = true,\n    git_signs = false,\n    mini_diff_signs = false,\n    -- diagnostics = false,\n    -- inlay_hints = false,\n  },\n  center = true, -- center the window\n  show = {\n    statusline = false, -- can only be shown when using the global statusline\n    tabline = false,\n  },\n  ---@type snacks.win.Config\n  win = { style = \"zen\" },\n  --- Callback when the window is opened.\n  ---@param win snacks.win\n  on_open = function(win) end,\n  --- Callback when the window is closed.\n  ---@param win snacks.win\n  on_close = function(win) end,\n  --- Options for the `Snacks.zen.zoom()`\n  ---@type snacks.zen.Config\n  zoom = {\n    toggles = {},\n    center = false,\n    show = { statusline = true, tabline = true },\n    win = {\n      backdrop = false,\n      width = 0, -- full width\n    },\n  },\n}\n\nSnacks.config.style(\"zen\", {\n  enter = true,\n  fixbuf = false,\n  minimal = false,\n  width = 120,\n  height = 0,\n  backdrop = { transparent = true, blend = 40 },\n  keys = { q = false },\n  zindex = 40,\n  wo = {\n    winhighlight = \"NormalFloat:Normal\",\n  },\n  w = {\n    snacks_main = true,\n  },\n})\n\n-- fullscreen indicator\n-- only shown when the window is maximized\nSnacks.config.style(\"zoom_indicator\", {\n  text = \"▍ zoom  󰊓  \",\n  minimal = true,\n  enter = false,\n  focusable = false,\n  height = 1,\n  row = 0,\n  col = -1,\n  backdrop = false,\n})\n\nSnacks.util.set_hl({\n  Icon = \"DiagnosticWarn\",\n}, { prefix = \"SnacksZen\", default = true })\n\n---@param opts? {statusline: boolean, tabline: boolean}\nlocal function get_main(opts)\n  opts = opts or {}\n  local bottom = vim.o.cmdheight + (opts.statusline and vim.o.laststatus == 3 and 1 or 0)\n  local top = opts.tabline\n      and ((vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)) and 1 or 0)\n    or 0\n  ---@class snacks.zen.Main values are 0-indexed\n  local ret = {\n    width = vim.o.columns,\n    row = top,\n    height = vim.o.lines - top - bottom,\n  }\n  return ret\nend\n\nM.win = nil ---@type snacks.win?\n\n---@param opts? snacks.zen.Config\nfunction M.zen(opts)\n  local toggles = opts and opts.toggles\n  opts = Snacks.config.get(\"zen\", defaults, opts)\n  opts.toggles = toggles or opts.toggles\n\n  -- close if already open\n  if M.win and M.win:valid() then\n    M.win:close()\n    M.win = nil\n    return\n  end\n\n  local parent_win = vim.api.nvim_get_current_win()\n  local parent_zindex = vim.api.nvim_win_get_config(parent_win).zindex\n\n  local buf = vim.api.nvim_get_current_buf()\n  local win_opts = Snacks.win.resolve({ style = \"zen\" }, opts.win, { buf = buf })\n  win_opts.zindex = parent_zindex and parent_zindex + 1 or win_opts.zindex\n\n  if Snacks.util.is_transparent() and type(win_opts.backdrop) == \"table\" then\n    win_opts.backdrop.transparent = false\n  end\n\n  local zoom_indicator ---@type snacks.win?\n  local show_indicator = false\n\n  -- calculate window size\n  if win_opts.height == 0 and (opts.show.statusline or opts.show.tabline or vim.o.cmdheight > 0) then\n    local main = get_main(opts.show)\n    win_opts.row = main.row\n    win_opts.height = function()\n      return get_main(opts.show).height\n    end\n    if type(win_opts.backdrop) == \"table\" then\n      win_opts.backdrop.win = win_opts.backdrop.win or {}\n      win_opts.backdrop.win.row = win_opts.row\n      win_opts.backdrop.win.height = win_opts.height\n    end\n    if win_opts.width == 0 then\n      show_indicator = true\n    end\n  end\n\n  -- create window\n  local win = Snacks.win(win_opts)\n  if opts.center and vim.bo[buf].buftype ~= \"terminal\" then\n    vim.cmd([[norm! zz]])\n  else\n    local view = vim.api.nvim_win_call(parent_win, vim.fn.winsaveview)\n    vim.api.nvim_win_call(win.win, function()\n      vim.fn.winrestview(view)\n    end)\n  end\n  M.win = win\n\n  if show_indicator then\n    zoom_indicator = Snacks.win({\n      show = false,\n      style = \"zoom_indicator\",\n      zindex = win.opts.zindex + 1,\n      wo = { winhighlight = \"NormalFloat:SnacksZenIcon\" },\n    })\n    zoom_indicator:open_buf()\n    local lines = vim.api.nvim_buf_get_lines(zoom_indicator.buf, 0, -1, false)\n    zoom_indicator.opts.width = vim.api.nvim_strwidth(lines[1] or \"\")\n    zoom_indicator:show()\n  end\n\n  -- set toggle states\n  ---@type {toggle: snacks.toggle.Class, state: unknown}[]\n  local states = {}\n  for id, state in pairs(opts.toggles) do\n    local toggle = Snacks.toggle.get(id)\n    if toggle then\n      table.insert(states, { toggle = toggle, state = toggle:get() })\n      toggle:set(state)\n    end\n  end\n  opts.on_open(win)\n\n  -- sync cursor with the parent window\n  vim.api.nvim_create_autocmd(\"CursorMoved\", {\n    group = win.augroup,\n    callback = function()\n      if win:win_valid() and vim.api.nvim_win_is_valid(parent_win) then\n        vim.api.nvim_win_set_cursor(parent_win, vim.api.nvim_win_get_cursor(win.win))\n      end\n    end,\n  })\n\n  -- restore toggle states when window is closed\n  win:on(\"WinClosed\", function()\n    if zoom_indicator then\n      zoom_indicator:close()\n    end\n    for _, state in ipairs(states) do\n      state.toggle:set(state.state)\n    end\n    opts.on_close(win)\n  end, { win = true })\n\n  -- update the buffer of the parent window\n  -- when the zen buffer changes\n  win:on(\"BufWinEnter\", function()\n    vim.api.nvim_win_set_buf(parent_win, win.buf)\n  end)\n\n  -- close when entering another window\n  win:on(\"WinEnter\", function()\n    local w = vim.api.nvim_get_current_win()\n    if w == win.win then\n      return\n    end\n    -- exit if other window is not a floating window\n    if vim.api.nvim_win_get_config(w).relative == \"\" then\n      -- schedule so that WinClosed is properly triggered\n      vim.schedule(function()\n        win:close()\n      end)\n    end\n  end)\n  return win\nend\n\n---@param opts? snacks.zen.Config\nfunction M.zoom(opts)\n  opts = Snacks.config.get(\"zen\", defaults, opts)\n  return M.zen(opts and opts.zoom or nil)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/trouble/sources/profiler.lua",
    "content": "---@module 'trouble'\n---@diagnostic disable: inject-field\nlocal Item = require(\"trouble.item\")\n\n---@type trouble.Source\nlocal M = {}\n\n---@diagnostic disable-next-line: missing-fields\nM.config = {\n  formatters = {\n    badges = function(ctx)\n      local trace = ctx.item.item ---@type snacks.profiler.Trace\n      local badges = Snacks.profiler.ui.badges(trace, { badges = { \"time\", \"count\" } })\n      local text = Snacks.profiler.ui.format(badges)\n      return vim.tbl_map(function(t)\n        return { text = t[1], hl = t[2] }\n      end, text)\n    end,\n  },\n  modes = {\n    profiler = {\n      events = { { event = \"User\", pattern = \"SnacksProfilerLoaded\" } },\n      source = \"profiler\",\n      groups = {\n        -- { \"tag\", format = \"{todo_icon} {tag}\" },\n        -- { \"directory\" },\n        { \"loc.plugin\", format = \"{file_icon} {loc.plugin} {count}\" },\n      },\n      -- sort = { { buf = 0 }, \"filename\", \"pos\", \"name\" },\n      sort = { \"-time\" },\n      format = \"{name} {badges} {pos}\",\n    },\n  },\n}\n\nfunction M.preview(item, ctx)\n  Snacks.profiler.ui.highlight(ctx.buf, { file = item.item.loc.file })\nend\n\nfunction M.get(cb, ctx)\n  ---@type snacks.profiler.Find\n  local opts = vim.tbl_deep_extend(\n    \"force\",\n    { group = \"name\", structure = true },\n    type(ctx.opts.params) == \"table\" and ctx.opts.params or {}\n  )\n  local _, node = Snacks.profiler.find(opts)\n  local items = {} ---@type trouble.Item[]\n  local id = 0\n\n  ---@param n snacks.profiler.Node\n  local function add(n)\n    if n.trace.def then\n      id = id + 1\n      local loc = n.trace.def\n      local item = Item.new({\n        id = id,\n        pos = { n.trace.def.line, 0 },\n        text = n.trace.name,\n        filename = loc and loc.file,\n        item = n.trace,\n        source = \"profiler\",\n      })\n      items[#items + 1] = item\n      for _, child in pairs(n.children) do\n        item:add_child(add(child))\n      end\n      return item\n    end\n  end\n\n  for _, child in pairs(node.children or {}) do\n    add(child)\n  end\n  cb(items)\nend\n\nreturn M\n"
  },
  {
    "path": "plugin/snacks.lua",
    "content": "require(\"snacks\")\n"
  },
  {
    "path": "queries/css/images.scm",
    "content": "\n(declaration\n  (call_expression\n    (function_name) @fn (#eq? @fn \"url\")\n    (arguments  [(plain_value) @image.src (string_value (string_content) @image.src)]))\n) @image\n"
  },
  {
    "path": "queries/html/images.scm",
    "content": "\n(element\n  (start_tag\n    (tag_name) @tag (#eq? @tag \"img\")\n    (attribute\n    (attribute_name) @attr_name (#eq? @attr_name \"src\")\n    (quoted_attribute_value (attribute_value) @image.src)\n    )\n  )\n) @image\n\n(self_closing_tag\n  (tag_name) @tag (#eq? @tag \"img\")\n  (attribute\n    (attribute_name) @attr_name (#eq? @attr_name \"src\")\n    (quoted_attribute_value (attribute_value) @image.src)\n  )\n) @image\n\n(element\n  (start_tag (tag_name) @tag (#eq? @tag \"svg\"))\n  (#set! image.ext \"svg\")\n) @image @image.content\n"
  },
  {
    "path": "queries/javascript/images.scm",
    "content": "\n(jsx_element\n  (jsx_opening_element\n    (identifier) @tag (#any-of? @tag \"img\" \"Image\")\n    (jsx_attribute\n      (property_identifier) @attr_name (#eq? @attr_name \"src\")\n      (string (string_fragment) @image.src)\n    )\n  )\n) @image\n\n(jsx_self_closing_element\n  (identifier) @tag (#any-of? @tag \"img\" \"Image\")\n  (jsx_attribute\n    (property_identifier) @attr_name (#eq? @attr_name \"src\")\n    (string (string_fragment) @image.src)\n  )\n) @image\n"
  },
  {
    "path": "queries/latex/images.scm",
    "content": "(inline_formula\n  (#set! image.ext \"math.tex\"))\n  @image.content @image\n\n(displayed_equation\n  (#set! image.ext \"math.tex\"))\n  @image.content @image\n\n((math_environment\n  (#set! image.ext \"math.tex\"))\n  @image.content @image\n  (#not-has-ancestor? @image \"displayed_equation\" \"math_environment\"))\n\n(graphics_include\n  (_ (path) @image.src)\n) @image\n\n"
  },
  {
    "path": "queries/lua/highlights.scm",
    "content": ";; extends\n\n((identifier) @namespace.builtin\n  (#any-of? @namespace.builtin \"Snacks\" \"svim\"))\n"
  },
  {
    "path": "queries/lua/injections.scm",
    "content": "; extends\n\n((comment\n  content: (comment_content) @injection.language)\n  (#lua-match? @injection.language \"inject%s*:%s*%S+\")\n  (#gsub! @injection.language \"^%s*inject%s*:%s*(%S+).*\" \"%1\")\n  .\n  (_\n    (string\n      content: (string_content) @injection.content)))\n"
  },
  {
    "path": "queries/markdown/images.scm",
    "content": "; extends\n\n(fenced_code_block\n  (info_string (language) @lang)\n  (#eq? @lang \"math\")\n  (code_fence_content) @image.content\n  (#set! injection.language \"latex\")\n  (#set! image.ext \"math.tex\")\n) @image\n\n(fenced_code_block\n  (info_string (language) @lang)\n  (#eq? @lang \"mermaid\")\n  (code_fence_content) @image.content\n  (#set! injection.language \"mermaid\")\n  (#set! image.ext \"chart.mmd\")\n) @image\n"
  },
  {
    "path": "queries/markdown/injections.scm",
    "content": "; extends\n\n(fenced_code_block\n  (info_string (language) @lang)\n  (#eq? @lang \"math\")\n  (code_fence_content) @injection.content\n  (#set! injection.language \"latex\")\n)\n"
  },
  {
    "path": "queries/markdown_inline/images.scm",
    "content": "\n(image\n  [\n    (link_destination) @image.src\n    (image_description (shortcut_link ((link_text) @image.src)))\n  ]\n    (#gsub! @image.src \"|.*\" \"\") ; remove wikilink image options\n    (#gsub! @image.src \"^<\" \"\") ; remove bracket link\n    (#gsub! @image.src \">$\" \"\")\n  ) @image\n"
  },
  {
    "path": "queries/norg/images.scm",
    "content": "(infirm_tag\n  (tag_name) @tag (#eq? @tag \"image\")\n  (tag_parameters (tag_param) @image.src)\n) @image\n\n(inline_math\n  (#set! image.lang \"latex\")\n  (#set! image.ext \"math.tex\")\n) @image.content @image\n"
  },
  {
    "path": "queries/scss/images.scm",
    "content": "(declaration\n  (call_expression\n    (function_name) @fn (#eq? @fn \"url\")\n    (arguments  [\n      (plain_value) @image.src\n      (string_value) @image.src\n      ; Remove quotes from the image URL\n      (#gsub! @image.src \"^['\\\"]\" \"\")\n      (#gsub! @image.src \"['\\\"]$\" \"\")\n    ]))\n) @image\n"
  },
  {
    "path": "queries/svelte/images.scm",
    "content": "; inherits: html\n; extends\n"
  },
  {
    "path": "queries/tsx/images.scm",
    "content": "; inherits: javascript\n"
  },
  {
    "path": "queries/typst/images.scm",
    "content": "(call\n  (ident) @ident\n  (#eq? @ident \"image\")\n  (group (string) @image.src)\n  (#offset! @image.src 0 1 0 -1)\n) @image\n\n(math\n  (#set! image.ext \"math.typ\")\n) @image.content @image\n"
  },
  {
    "path": "queries/vue/images.scm",
    "content": "; inherits: html\n; extends\n"
  },
  {
    "path": "scripts/docs",
    "content": "#!/bin/env bash\n\nnvim -u tests/minit.lua --headless +'lua require(\"snacks.meta.docs\").build()' +qa\n"
  },
  {
    "path": "scripts/docs-post",
    "content": "#!/bin/env bash\n\nnvim -u tests/minit.lua --headless +'lua require(\"snacks.meta.docs\").fix_titles()' +qa\n"
  },
  {
    "path": "scripts/test",
    "content": "#!/usr/bin/env bash\n\nnvim -l tests/minit.lua --minitest \"$@\"\n"
  },
  {
    "path": "selene.toml",
    "content": "std=\"vim\"\n\n[lints]\nmixed_table=\"allow\"\n"
  },
  {
    "path": "stylua.toml",
    "content": "indent_type = \"Spaces\"\nindent_width = 2\ncolumn_width = 120\n[sort_requires]\nenabled = true\n\n"
  },
  {
    "path": "tests/config_spec.lua",
    "content": "---@module 'luassert'\n\nlocal function d(v)\n  return vim.inspect(v):gsub(\"%s+\", \" \")\nend\n\ndescribe(\"config\", function()\n  local tests = {\n    {\n      { 1, 2 },\n      { 3, 4 },\n      { 3, 4 },\n    },\n    {\n      { 1, 2 },\n      nil,\n      { 1, 2 },\n    },\n    {\n      { a = 1, b = 2 },\n      { c = 3 },\n      { a = 1, b = 2, c = 3 },\n    },\n    {\n      { 1, 2, a = 1 },\n      { 3, 4, b = 2 },\n      { 3, 4, b = 2 },\n    },\n    {\n      { 3, 4, b = 2 },\n      { 1, 2 },\n      { 1, 2 },\n    },\n    {\n      { 1, 2, a = 1 },\n      { b = 2 },\n      { 1, 2, b = 2, a = 1 },\n    },\n  }\n  for _, t in ipairs(tests) do\n    it(\"merges correctly \" .. d(t), function()\n      local ret = Snacks.config.merge(t[1], t[2])\n      assert.are.same(ret, t[3])\n    end)\n  end\nend)\n"
  },
  {
    "path": "tests/gitbrowse_spec.lua",
    "content": "---@module \"luassert\"\n\nlocal gitbrowse = require(\"snacks.gitbrowse\")\n\n-- stylua: ignore\nlocal git_remotes_cases = {\n  [\"https://github.com/LazyVim/LazyVim.git\"]                             = \"https://github.com/LazyVim/LazyVim\",\n  [\"https://github.com/LazyVim/LazyVim\"]                                 = \"https://github.com/LazyVim/LazyVim\",\n  [\"git@github.com:LazyVim/LazyVim\"]                                     = \"https://github.com/LazyVim/LazyVim\",\n  [\"git@ssh.dev.azure.com:v3/neovim-org/owner/repo\"]                     = \"https://dev.azure.com/neovim-org/owner/_git/repo\",\n  [\"https://folkelemaitre@bitbucket.org/samiulazim/neovim.git\"]          = \"https://bitbucket.org/samiulazim/neovim\",\n  [\"git@bitbucket.org:samiulazim/neovim.git\"]                            = \"https://bitbucket.org/samiulazim/neovim\",\n  [\"git@gitlab.com:inkscape/inkscape.git\"]                               = \"https://gitlab.com/inkscape/inkscape\",\n  [\"https://gitlab.com/inkscape/inkscape.git\"]                           = \"https://gitlab.com/inkscape/inkscape\",\n  [\"git@github.com:torvalds/linux.git\"]                                  = \"https://github.com/torvalds/linux\",\n  [\"https://github.com/torvalds/linux.git\"]                              = \"https://github.com/torvalds/linux\",\n  [\"git@bitbucket.org:team/repo.git\"]                                    = \"https://bitbucket.org/team/repo\",\n  [\"https://bitbucket.org/team/repo.git\"]                                = \"https://bitbucket.org/team/repo\",\n  [\"git@gitlab.com:example-group/example-project.git\"]                   = \"https://gitlab.com/example-group/example-project\",\n  [\"https://gitlab.com/example-group/example-project.git\"]               = \"https://gitlab.com/example-group/example-project\",\n  [\"git@ssh.dev.azure.com:v3/org/project/repo\"]                          = \"https://dev.azure.com/org/project/_git/repo\",\n  [\"https://username@dev.azure.com/org/project/_git/repo\"]               = \"https://dev.azure.com/org/project/_git/repo\",\n  [\"ssh://git@ghe.example.com:2222/org/repo.git\"]                        = \"https://ghe.example.com/org/repo\",\n  [\"https://ghe.example.com/org/repo.git\"]                               = \"https://ghe.example.com/org/repo\",\n  [\"git-codecommit.us-east-1.amazonaws.com/v1/repos/MyDemoRepo\"]         = \"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/MyDemoRepo\",\n  [\"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/MyDemoRepo\"] = \"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/MyDemoRepo\",\n  [\"ssh://git@source.developers.google.com:2022/p/project/r/repo\"]       = \"https://source.developers.google.com/p/project/r/repo\",\n  [\"https://source.developers.google.com/p/project/r/repo\"]              = \"https://source.developers.google.com/p/project/r/repo\",\n  [\"git@git.sr.ht:~user/repo\"]                                           = \"https://git.sr.ht/~user/repo\",\n  [\"https://git.sr.ht/~user/repo\"]                                       = \"https://git.sr.ht/~user/repo\",\n  [\"git@git.sr.ht:~user/another-repo\"]                                   = \"https://git.sr.ht/~user/another-repo\",\n  [\"https://git.sr.ht/~user/another-repo\"]                               = \"https://git.sr.ht/~user/another-repo\",\n}\n\ndescribe(\"util.lazygit\", function()\n  for remote, expected in pairs(git_remotes_cases) do\n    it(\"should parse git remote \" .. remote, function()\n      local url = gitbrowse.get_repo(remote)\n      assert.are.equal(expected, url)\n    end)\n  end\nend)\n"
  },
  {
    "path": "tests/image/big.md",
    "content": "- chapters 3 to 8 from the book [measure theory](https://measure.axler.net/),\n  by Sheldon Axler. [LICENSE](https://creativecommons.org/licenses/by-nc/4.0/)\n\n## Chapter 3\n\n## Integration\n\nTo remedy deficiencies of Riemann integration that were discussed in Section 1B, in the last chapter we developed measure theory as an extension of the notion of the length of an interval. Having proved the fundamental results about measures, we are now ready to use measures to develop integration with respect to a measure.\n\nAs we will see, this new method of integration fixes many of the problems with Riemann integration. In particular, we will develop good theorems for interchanging limits and integrals.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-088.jpg?height=932&width=1055&top_left_y=811&top_left_x=120)\n\nStatue in Milan of Maria Gaetana Agnesi, who in 1748 published one of the first calculus textbooks. A translation of her book into English was published in 1801. In this chapter, we develop a method of integration more powerful than methods contemplated by the pioneers of calculus.\n\n@Giovanni Dall'Orto\n\n## 3A Integration with Respect to a Measure\n\n## Integration of Nonnegative Functions\n\nWe will first define the integral of a nonnegative function with respect to a measure. Then by writing a real-valued function as the difference of two nonnegative functions, we will define the integral of a real-valued function with respect to a measure. We begin this process with the following definition.\n\n### 3.1 Definition $\\mathcal{S}$-partition\n\nSuppose $\\mathcal{S}$ is a $\\sigma$-algebra on a set $X$. An $\\mathcal{S}$-partition of $X$ is a finite collection $A_{1}, \\ldots, A_{m}$ of disjoint sets in $\\mathcal{S}$ such that $A_{1} \\cup \\cdots \\cup A_{m}=X$.\n\nThe next definition should remind you of the definition of the lower Riemann sum (see 1.3). However, now we are working with an arbitrary measure and\n\nWe adopt the convention that $0 \\cdot \\infty$ and $\\infty \\cdot 0$ should both be interpreted to be 0 . thus $X$ need not be a subset of $\\mathbf{R}$. More importantly, even in the case when $X$ is a closed interval $[a, b]$ in $\\mathbf{R}$ and $\\mu$ is Lebesgue measure on the Borel subsets of $[a, b]$, the sets $A_{1}, \\ldots, A_{m}$ in the definition below do not need to be subintervals of $[a, b]$ as they do for the lower Riemann sum-they need only be Borel sets.\n\n### 3.2 Definition lower Lebesgue sum\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $f: X \\rightarrow[0, \\infty]$ is an $\\mathcal{S}$-measurable function, and $P$ is an $\\mathcal{S}$-partition $A_{1}, \\ldots, A_{m}$ of $X$. The lower Lebesgue sum $\\mathcal{L}(f, P)$ is defined by\n\n$$\n\\mathcal{L}(f, P)=\\sum_{j=1}^{m} \\mu\\left(A_{j}\\right) \\inf _{A_{j}} f\n$$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space. We will denote the integral of an $\\mathcal{S}$ measurable function $f$ with respect to $\\mu$ by $\\int f d \\mu$. Our basic requirements for an integral are that we want $\\int \\chi_{E} d \\mu$ to equal $\\mu(E)$ for all $E \\in \\mathcal{S}$, and we want $\\int(f+g) d \\mu=\\int f d \\mu+\\int g d \\mu$. As we will see, the following definition satisfies both of those requirements (although this is not obvious). Think about why the following definition is reasonable in terms of the integral equaling the area under the graph of the function (in the special case of Lebesgue measure on an interval of $\\mathbf{R}$ ).\n\n### 3.3 Definition integral of a nonnegative function\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[0, \\infty]$ is an $\\mathcal{S}$-measurable function. The integral of $f$ with respect to $\\mu$, denoted $\\int f d \\mu$, is defined by\n\n$$\n\\int f d \\mu=\\sup \\{\\mathcal{L}(f, P): P \\text { is an } \\mathcal{S} \\text {-partition of } X\\}\n$$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[0, \\infty]$ is an $\\mathcal{S}$-measurable function. Each $\\mathcal{S}$-partition $A_{1}, \\ldots, A_{m}$ of $X$ leads to an approximation of $f$ from below by the $\\mathcal{S}$-measurable simple function $\\sum_{j=1}^{m}\\left(\\inf _{A_{j}} f\\right) \\chi_{A_{j}}$. This suggests that\n\n$$\n\\sum_{j=1}^{m} \\mu\\left(A_{j}\\right) \\inf _{A_{j}} f\n$$\n\nshould be an approximation from below of our intuitive notion of $\\int f d \\mu$. Taking the supremum of these approximations leads to our definition of $\\int f d \\mu$.\n\nThe following result gives our first example of evaluating an integral.\n\n## 3.4 integral of a characteristic function\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $E \\in \\mathcal{S}$. Then\n\n$$\n\\int \\chi_{E} d \\mu=\\mu(E) .\n$$\n\nProof If $P$ is the $\\mathcal{S}$-partition of $X$ consisting of $E$ and its complement $X \\backslash E$, then clearly $\\mathcal{L}\\left(\\chi_{E}, P\\right)=\\mu(E)$. Thus $\\int \\chi_{E} d \\mu \\geq \\mu(E)$.\n\nTo prove the inequality in the other direction, suppose $P$ is an $\\mathcal{S}$-partition $A_{1}, \\ldots, A_{m}$ of $X$. Then $\\mu\\left(A_{j}\\right) \\inf _{A_{j}} \\chi_{E}$ equals $\\mu\\left(A_{j}\\right)$ if $A_{j} \\subset E$ and equals 0 otherwise. Thus\n\n$$\n\\begin{aligned}\n\\mathcal{L}\\left(\\chi_{E}, P\\right) & =\\sum_{\\left\\{j: A_{j} \\subset E\\right\\}} \\mu\\left(A_{j}\\right) \\\\\n& =\\mu\\left(\\bigcup_{\\left\\{j: A_{j} \\subset E\\right\\}} A_{j}\\right) \\\\\n& \\leq \\mu(E) .\n\\end{aligned}\n$$\n\nThe symbol $d$ in the expression $\\int f \\mathrm{~d} \\mu$ has no independent meaning, but it often usefully separates $f$ from $\\mu$. Because the $d$ in $\\int f \\mathrm{~d} \\mu$ does not represent another object, some mathematicians prefer typesetting an upright $\\mathrm{d}$ in this situation, producing $\\int f \\mathrm{~d} \\mu$. However, the upright $\\mathrm{d}$ looks jarring to some readers who are accustomed to italicized symbols. This book takes the compromise position of using slanted d instead of math-mode italicized $d$ in integrals.\n\nThus $\\int \\chi_{E} d \\mu \\leq \\mu(E)$, completing the proof.\n\n### 3.5 Example integrals of $\\chi_{\\mathbf{Q}}$ and $\\chi_{[0,1] \\backslash \\mathbf{Q}}$\n\nSuppose $\\lambda$ is Lebesgue measure on $\\mathbf{R}$. As a special case of the result above, we have $\\int \\chi_{\\mathbf{Q}} d \\lambda=0$ (because $|\\mathbf{Q}|=0$ ). Recall that $\\chi_{\\mathbf{Q}}$ is not Riemann integrable on $[0,1]$. Thus even at this early stage in our development of integration with respect to a measure, we have fixed one of the deficiencies of Riemann integration.\n\nNote also that 3.4 implies that $\\int \\chi_{[0,1] \\backslash \\mathbf{Q}} d \\lambda=1$ (because $|[0,1] \\backslash \\mathbf{Q}|=1$ ), which is what we want. In contrast, the lower Riemann integral of $\\chi_{[0,1] \\backslash \\mathbf{Q}}$ on $[0,1]$ equals 0 , which is not what we want.\n\n### 3.6 Example integration with respect to counting measure is summation\n\nSuppose $\\mu$ is counting measure on $\\mathbf{Z}^{+}$and $b_{1}, b_{2}, \\ldots$ is a sequence of nonnegative numbers. Think of $b$ as the function from $\\mathbf{Z}^{+}$to $[0, \\infty)$ defined by $b(k)=b_{k}$. Then\n\n$$\n\\int b d \\mu=\\sum_{k=1}^{\\infty} b_{k}\n$$\n\nas you should verify.\n\nIntegration with respect to a measure can be called Lebesgue integration. The next result shows that Lebesgue integration behaves as expected on simple functions represented as linear combinations of characteristic functions of disjoint sets.\n\n## 3.7 integral of a simple function\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $E_{1}, \\ldots, E_{n}$ are disjoint sets in $\\mathcal{S}$, and $c_{1}, \\ldots, c_{n} \\in[0, \\infty]$. Then\n\n$$\n\\int\\left(\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}\\right) d \\mu=\\sum_{k=1}^{n} c_{k} \\mu\\left(E_{k}\\right)\n$$\n\nProof Without loss of generality, we can assume that $E_{1}, \\ldots, E_{n}$ is an $\\mathcal{S}$-partition of $X$ [by replacing $n$ by $n+1$ and setting $E_{n+1}=X \\backslash\\left(E_{1} \\cup \\ldots \\cup E_{n}\\right)$ and $c_{n+1}=0$ ].\n\nIf $P$ is the $\\mathcal{S}$-partition $E_{1}, \\ldots, E_{n}$ of $X$, then $\\mathcal{L}\\left(\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}{ }^{\\prime} P\\right)=\\sum_{k=1}^{n} c_{k} \\mu\\left(E_{k}\\right)$. Thus\n\n$$\n\\int\\left(\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}\\right) d \\mu \\geq \\sum_{k=1}^{n} c_{k} \\mu\\left(E_{k}\\right)\n$$\n\nTo prove the inequality in the other direction, suppose that $P$ is an $\\mathcal{S}$-partition $A_{1}, \\ldots, A_{m}$ of $X$. Then\n\n$$\n\\begin{aligned}\n\\mathcal{L}\\left(\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}, P\\right) & =\\sum_{j=1}^{m} \\mu\\left(A_{j}\\right) \\min _{\\left\\{i: A_{j} \\cap E_{i} \\neq \\varnothing\\right\\}} c_{i} \\\\\n& =\\sum_{j=1}^{m} \\sum_{k=1}^{n} \\mu\\left(A_{j} \\cap E_{k}\\right)_{\\left\\{i: A_{j} \\cap E_{i} \\neq \\varnothing\\right\\}} c_{i} \\\\\n& \\leq \\sum_{j=1}^{m} \\sum_{k=1}^{n} \\mu\\left(A_{j} \\cap E_{k}\\right) c_{k} \\\\\n& =\\sum_{k=1}^{n} c_{k} \\sum_{j=1}^{m} \\mu\\left(A_{j} \\cap E_{k}\\right) \\\\\n& =\\sum_{k=1}^{n} c_{k} \\mu\\left(E_{k}\\right) .\n\\end{aligned}\n$$\n\nThe inequality above implies that $\\int\\left(\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}\\right) d \\mu \\leq \\sum_{k=1}^{n} c_{k} \\mu\\left(E_{k}\\right)$, completing the proof.\n\nThe next easy result gives an unsurprising property of integrals.\n\n## 3.8 integration is order preserving\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f, g: X \\rightarrow[0, \\infty]$ are $\\mathcal{S}$-measurable functions such that $f(x) \\leq g(x)$ for all $x \\in X$. Then $\\int f \\mathrm{~d} \\mu \\leq \\int g d \\mu$.\n\nProof Suppose $P$ is an $\\mathcal{S}$-partition $A_{1}, \\ldots, A_{m}$ of $X$. Then\n\n$$\n\\inf _{A_{j}} f \\leq \\inf _{A_{j}} g\n$$\n\nfor each $j=1, \\ldots, m$. Thus $\\mathcal{L}(f, P) \\leq \\mathcal{L}(g, P)$. Hence $\\int f d \\mu \\leq \\int g d \\mu$.\n\n## Monotone Convergence Theorem\n\nFor the proof of the Monotone Convergence Theorem (and several other results), we will need to use the following mild restatement of the definition of the integral of a nonnegative function.\n\n## 3.9 integrals via simple functions\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[0, \\infty]$ is $\\mathcal{S}$-measurable. Then\n\n3.10 $\\int f d \\mu=\\sup \\left\\{\\sum_{j=1}^{m} c_{j} \\mu\\left(A_{j}\\right): A_{1}, \\ldots, A_{m}\\right.$ are disjoint sets in $\\mathcal{S}$,\n\n$$\n\\begin{aligned}\n& c_{1}, \\ldots, c_{m} \\in[0, \\infty), \\text { and } \\\\\n& \\left.f(x) \\geq \\sum_{j=1}^{m} c_{j} \\chi_{A_{j}}(x) \\text { for every } x \\in X\\right\\}\n\\end{aligned}\n$$\n\nProof First note that the left side of 3.10 is bigger than or equal to the right side by 3.7 and 3.8 .\n\nTo prove that the right side of 3.10 is bigger than or equal to the left side, first assume that $\\inf _{A} f<\\infty$ for every $A \\in \\mathcal{S}$ with $\\mu(A)>0$. Then for $P$ an $\\mathcal{S}$-partition $A_{1}, \\ldots, A_{m}$ of nonempty subsets of $X$, take $c_{j}=\\inf _{A_{j}} f$, which shows that $\\mathcal{L}(f, P)$ is in the set on the right side of 3.10. Thus the definition of $\\int f d \\mu$ shows that the right side of 3.10 is bigger than or equal to the left side.\n\nThe only remaining case to consider is when there exists a set $A \\in \\mathcal{S}$ such that $\\mu(A)>0$ and $\\inf _{A} f=\\infty$ [which implies that $f(x)=\\infty$ for all $x \\in A$ ]. In this case, for arbitrary $t \\in(0, \\infty)$ we can take $m=1, A_{1}=A$, and $c_{1}=t$. These choices show that the right side of 3.10 is at least $t \\mu(A)$. Because $t$ is an arbitrary positive number, this shows that the right side of 3.10 equals $\\infty$, which of course is greater than or equal to the left side, completing the proof.\n\nThe next result allows us to interchange limits and integrals in certain circumstances. We will see more theorems of this nature in the next section.\n\n### 3.11 Monotone Convergence Theorem\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $0 \\leq f_{1} \\leq f_{2} \\leq \\cdots$ is an increasing sequence of $\\mathcal{S}$-measurable functions. Define $f: X \\rightarrow[0, \\infty]$ by\n\n$$\nf(x)=\\lim _{k \\rightarrow \\infty} f_{k}(x)\n$$\n\nThen\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu\n$$\n\nProof The function $f$ is $\\mathcal{S}$-measurable by 2.53 .\n\nBecause $f_{k}(x) \\leq f(x)$ for every $x \\in X$, we have $\\int f_{k} d \\mu \\leq \\int f d \\mu$ for each $k \\in \\mathbf{Z}^{+}$(by 3.8). Thus $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu \\leq \\int f d \\mu$.\n\nTo prove the inequality in the other direction, suppose $A_{1}, \\ldots, A_{m}$ are disjoint sets in $\\mathcal{S}$ and $c_{1}, \\ldots, c_{m} \\in[0, \\infty)$ are such that\n\n$$\nf(x) \\geq \\sum_{j=1}^{m} c_{j} \\chi_{A_{j}}(x) \\quad \\text { for every } x \\in X\n$$\n\nLet $t \\in(0,1)$. For $k \\in \\mathbf{Z}^{+}$, let\n\n$$\nE_{k}=\\left\\{x \\in X: f_{k}(x) \\geq t \\sum_{j=1}^{m} c_{j} \\chi_{A_{j}}(x)\\right\\}\n$$\n\nThen $E_{1} \\subset E_{2} \\subset \\cdots$ is an increasing sequence of sets in $\\mathcal{S}$ whose union equals $X$. Thus $\\lim _{k \\rightarrow \\infty} \\mu\\left(A_{j} \\cap E_{k}\\right)=\\mu\\left(A_{j}\\right)$ for each $j \\in\\{1, \\ldots, m\\}$ (by 2.59).\n\nIf $k \\in \\mathbf{Z}^{+}$, then\n\n$$\nf_{k}(x) \\geq \\sum_{j=1}^{m} t c_{j} \\chi_{A_{j} \\cap E_{k}}(x)\n$$\n\nfor every $x \\in X$. Thus (by 3.9)\n\n$$\n\\int f_{k} d \\mu \\geq t \\sum_{j=1}^{m} c_{j} \\mu\\left(A_{j} \\cap E_{k}\\right)\n$$\n\nTaking the limit as $k \\rightarrow \\infty$ of both sides of the inequality above gives\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu \\geq t \\sum_{j=1}^{m} c_{j} \\mu\\left(A_{j}\\right)\n$$\n\nNow taking the limit as $t$ increases to 1 shows that\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu \\geq \\sum_{j=1}^{m} c_{j} \\mu\\left(A_{j}\\right)\n$$\n\nTaking the supremum of the inequality above over all $\\mathcal{S}$-partitions $A_{1}, \\ldots, A_{m}$ of $X$ and all $c_{1}, \\ldots, c_{m} \\in[0, \\infty$ ) satisfying 3.12 shows (using 3.9) that we have $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu \\geq \\int f d \\mu$, completing the proof.\n\nThe proof that the integral is additive will use the Monotone Convergence Theorem and our next result. The representation of a simple function $h: X \\rightarrow[0, \\infty]$ in the form $\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}$ is not unique. Requiring the numbers $c_{1}, \\ldots, c_{n}$ to be distinct and $E_{1}, \\ldots, E_{n}$ to be nonempty and disjoint with $E_{1} \\cup \\cdots \\cup E_{n}=X$ produces what is called the standard representation of a simple function [take $E_{k}=h^{-1}\\left(\\left\\{c_{k}\\right\\}\\right)$, where $c_{1}, \\ldots, c_{n}$ are the distinct values of $\\left.h\\right]$. The following lemma shows that all representations (including representations with sets that are not disjoint) of a simple measurable function give the same sum that we expect from integration.\n\n### 3.13 integral-type sums for simple functions\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space. Suppose $a_{1}, \\ldots, a_{m}, b_{1}, \\ldots, b_{n} \\in[0, \\infty]$ and $A_{1}, \\ldots, A_{m}, B_{1}, \\ldots, B_{n} \\in \\mathcal{S}$ are such that $\\sum_{j=1}^{m} a_{j} \\chi_{A_{j}}=\\sum_{k=1}^{n} b_{k} \\chi_{B_{k}}$. Then\n\n$$\n\\sum_{j=1}^{m} a_{j} \\mu\\left(A_{j}\\right)=\\sum_{k=1}^{n} b_{k} \\mu\\left(B_{k}\\right)\n$$\n\nProof We assume $A_{1} \\cup \\cdots \\cup A_{m}=X$ (otherwise add the term $0 \\chi_{X \\backslash\\left(A_{1} \\cup \\cdots \\cup A_{m}\\right.}$ ). Suppose $A_{1}$ and $A_{2}$ are not disjoint. Then we can write\n\n### 3.14\n\n$$\na_{1} \\chi_{A_{1}}+a_{2} \\chi_{A_{2}}=a_{1} \\chi_{A_{1} \\backslash A_{2}}+a_{2} \\chi_{A_{2} \\backslash A_{1}}+\\left(a_{1}+a_{2}\\right) \\chi_{A_{1} \\cap A_{2}},\n$$\n\nwhere the three sets appearing on the right side of the equation above are disjoint.\n\nNow $A_{1}=\\left(A_{1} \\backslash A_{2}\\right) \\cup\\left(A_{1} \\cap A_{2}\\right)$ and $A_{2}=\\left(A_{2} \\backslash A_{1}\\right) \\cup\\left(A_{1} \\cap A_{2}\\right)$; each of these unions is a disjoint union. Thus $\\mu\\left(A_{1}\\right)=\\mu\\left(A_{1} \\backslash A_{2}\\right)+\\mu\\left(A_{1} \\cap A_{2}\\right)$ and $\\mu\\left(A_{2}\\right)=\\mu\\left(A_{2} \\backslash A_{1}\\right)+\\mu\\left(A_{1} \\cap A_{2}\\right)$. Hence\n\n$$\na_{1} \\mu\\left(A_{1}\\right)+a_{2} \\mu\\left(A_{2}\\right)=a_{1} \\mu\\left(A_{1} \\backslash A_{2}\\right)+a_{2} \\mu\\left(A_{2} \\backslash A_{1}\\right)+\\left(a_{1}+a_{2}\\right) \\mu\\left(A_{1} \\cap A_{2}\\right) .\n$$\n\nThe equation above, in conjunction with 3.14 , shows that if we replace the two sets $A_{1}, A_{2}$ by the three disjoint sets $A_{1} \\backslash A_{2}, A_{2} \\backslash A_{1}, A_{1} \\cap A_{2}$ and make the appropriate adjustments to the coefficients $a_{1}, \\ldots, a_{m}$, then the value of the sum $\\sum_{j=1}^{m} a_{j} \\mu\\left(A_{j}\\right)$ is unchanged (although $m$ has increased by 1 ).\n\nRepeating this process with all pairs of subsets among $A_{1}, \\ldots, A_{m}$ that are not disjoint after each step, in a finite number of steps we can convert the initial list $A_{1}, \\ldots, A_{m}$ into a disjoint list of subsets without changing the value of $\\sum_{j=1}^{m} a_{j} \\mu\\left(A_{j}\\right)$.\n\nThe next step is to make the numbers $a_{1}, \\ldots, a_{m}$ distinct. This is done by replacing the sets corresponding to each $a_{j}$ by the union of those sets, and using finite additivity of the measure $\\mu$ to show that the value of the sum $\\sum_{j=1}^{m} a_{j} \\mu\\left(A_{j}\\right)$ does not change.\n\nFinally, drop any terms for which $A_{j}=\\varnothing$, getting the standard representation for a simple function. We have now shown that the original value of $\\sum_{j=1}^{m} a_{j} \\mu\\left(A_{j}\\right)$ is equal to the value if we use the standard representation of the simple function $\\sum_{j=1}^{m} a_{j} \\chi_{A_{j}}$. The same procedure can be used with the representation $\\sum_{k=1}^{n} b_{k} \\chi_{B_{k}}$ to show that $\\sum_{k=1}^{n} b_{k} \\mu\\left(\\chi_{B_{k}}\\right)$ equals what we would get with the standard representation. Thus the equality of the functions $\\sum_{j=1}^{m} a_{j} \\chi_{A_{j}}$ and $\\sum_{k=1}^{n} b_{k} \\chi_{B_{k}}$ implies the equality $\\sum_{j=1}^{m} a_{j} \\mu\\left(A_{j}\\right)=\\sum_{k=1}^{n} b_{k} \\mu\\left(B_{k}\\right)$.\n\nNow we can show that our definition of integration does the right thing with simple measurable functions that might not be expressed in the standard representation. The result below differs from 3.7 mainly because the sets $E_{1}, \\ldots, E_{n}$ in the result below are not required to be disjoint. Like the previous result, the next result would follow immediately from the linearity of integration if that property had already been proved.\n\nIf we had already proved that integration is linear, then we could quickly get the conclusion of the previous result by integrating both sides of the equation $\\sum_{j=1}^{m} a_{j} \\chi_{A_{j}}=\\sum_{k=1}^{n} b_{k} \\chi_{B_{k}}$ with respect to $\\mu$. However, we need the previous result to prove the next result, which is used in our proof that integration is linear.\n\n### 3.15 integral of a linear combination of characteristic functions\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $E_{1}, \\ldots, E_{n} \\in \\mathcal{S}$, and $c_{1}, \\ldots, c_{n} \\in[0, \\infty]$. Then\n\n$$\n\\int\\left(\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}\\right) d \\mu=\\sum_{k=1}^{n} c_{k} \\mu\\left(E_{k}\\right)\n$$\n\nProof The desired result follows from writing the simple function $\\sum_{k=1}^{n} c_{k} \\chi_{E_{k}}$ in the standard representation for a simple function and then using 3.7 and 3.13.\n\nNow we can prove that integration is additive on nonnegative functions.\n\n### 3.16 additivity of integration\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f, g: X \\rightarrow[0, \\infty]$ are $\\mathcal{S}$-measurable functions. Then\n\n$$\n\\int(f+g) d \\mu=\\int f d \\mu+\\int g d \\mu\n$$\n\nProof The desired result holds for simple nonnegative $\\mathcal{S}$-measurable functions (by 3.15). Thus we approximate by such functions.\n\nSpecifically, let $f_{1}, f_{2}, \\ldots$ and $g_{1}, g_{2}, \\ldots$ be increasing sequences of simple nonnegative $\\mathcal{S}$-measurable functions such that\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}(x)=f(x) \\quad \\text { and } \\quad \\lim _{k \\rightarrow \\infty} g_{k}(x)=g(x)\n$$\n\nfor all $x \\in X$ (see 2.89 for the existence of such increasing sequences). Then\n\n$$\n\\begin{aligned}\n\\int(f+g) d \\mu & =\\lim _{k \\rightarrow \\infty} \\int\\left(f_{k}+g_{k}\\right) d \\mu \\\\\n& =\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu+\\lim _{k \\rightarrow \\infty} \\int g_{k} d \\mu \\\\\n& =\\int f d \\mu+\\int g d \\mu,\n\\end{aligned}\n$$\n\nwhere the first and third equalities follow from the Monotone Convergence Theorem and the second equality holds by 3.15 .\n\nThe lower Riemann integral is not additive, even for bounded nonnegative measurable functions. For example, if $f=\\chi_{\\mathbf{Q} \\cap[0,1]}$ and $g=\\chi_{[0,1] \\backslash \\mathbf{Q}}$, then\n\n$$\nL(f,[0,1])=0 \\quad \\text { and } \\quad L(g,[0,1])=0 \\quad \\text { but } \\quad L(f+g,[0,1])=1 \\text {. }\n$$\n\nIn contrast, if $\\lambda$ is Lebesgue measure on the Borel subsets of $[0,1]$, then\n\n$$\n\\int f d \\lambda=0 \\quad \\text { and } \\quad \\int g d \\lambda=1 \\quad \\text { and } \\quad \\int(f+g) d \\lambda=1\n$$\n\nMore generally, we have just proved that $\\int(f+g) d \\mu=\\int f d \\mu+\\int g d \\mu$ for every measure $\\mu$ and for all nonnegative measurable functions $f$ and $g$. Recall that integration with respect to a measure is defined via lower Lebesgue sums in a similar fashion to the definition of the lower Riemann integral via lower Riemann sums (with the big exception of allowing measurable sets instead of just intervals in the partitions). However, we have just seen that the integral with respect to a measure (which could have been called the lower Lebesgue integral) has considerably nicer behavior (additivity!) than the lower Riemann integral.\n\n## Integration of Real-Valued Functions\n\nThe following definition gives us a standard way to write an arbitrary real-valued function as the difference of two nonnegative functions.\n\n### 3.17 Definition $f^{+} ; f^{-}$\n\nSuppose $f: X \\rightarrow[-\\infty, \\infty]$ is a function. Define functions $f^{+}$and $f^{-}$from $X$ to $[0, \\infty]$ by\n\n$$\nf^{+}(x)=\\left\\{\\begin{array}{ll}\nf(x) & \\text { if } f(x) \\geq 0, \\\\\n0 & \\text { if } f(x)<0\n\\end{array} \\quad \\text { and } \\quad f^{-}(x)= \\begin{cases}0 & \\text { if } f(x) \\geq 0 \\\\\n-f(x) & \\text { if } f(x)<0\\end{cases}\\right.\n$$\n\nNote that if $f: X \\rightarrow[-\\infty, \\infty]$ is a function, then\n\n$$\nf=f^{+}-f^{-} \\quad \\text { and } \\quad|f|=f^{+}+f^{-} .\n$$\n\nThe decomposition above allows us to extend our definition of integration to functions that take on negative as well as positive values.\n\n3.18 Definition integral of a real-valued function; $\\int f d \\mu$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[-\\infty, \\infty]$ is an $\\mathcal{S}$-measurable function such that at least one of $\\int f^{+} d \\mu$ and $\\int f^{-} d \\mu$ is finite. The integral of $f$ with respect to $\\mu$, denoted $\\int f d \\mu$, is defined by\n\n$$\n\\int f d \\mu=\\int f^{+} d \\mu-\\int f^{-} d \\mu\n$$\n\nIf $f \\geq 0$, then $f^{+}=f$ and $f^{-}=0$; thus this definition is consistent with the previous definition of the integral of a nonnegative function.\n\nThe condition $\\int|f| d \\mu<\\infty$ is equivalent to the condition $\\int f^{+} d \\mu<\\infty$ and $\\int f^{-} d \\mu<\\infty$ (because $|f|=f^{+}+f^{-}$).\n\n### 3.19 Example a function whose integral is not defined\n\nSuppose $\\lambda$ is Lebesgue measure on $\\mathbf{R}$ and $f: \\mathbf{R} \\rightarrow \\mathbf{R}$ is the function defined by\n\n$$\nf(x)= \\begin{cases}1 & \\text { if } x \\geq 0 \\\\ -1 & \\text { if } x<0\\end{cases}\n$$\n\nThen $\\int f d \\lambda$ is not defined because $\\int f^{+} d \\lambda=\\infty$ and $\\int f^{-} d \\lambda=\\infty$.\n\nThe next result says that the integral of a number times a function is exactly what we expect.\n\n### 3.20 integration is homogeneous\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[-\\infty, \\infty]$ is a function such that $\\int f d \\mu$ is defined. If $c \\in \\mathbf{R}$, then\n\n$$\n\\int c f d \\mu=c \\int f d \\mu .\n$$\n\nProof First consider the case where $f$ is a nonnegative function and $c \\geq 0$. If $P$ is an $\\mathcal{S}$-partition of $X$, then clearly $\\mathcal{L}(c f, P)=c \\mathcal{L}(f, P)$. Thus $\\int c f d \\mu=c \\int f d \\mu$.\n\nNow consider the general case where $f$ takes values in $[-\\infty, \\infty]$. Suppose $c \\geq 0$. Then\n\n$$\n\\begin{aligned}\n\\int c f d \\mu & =\\int(c f)^{+} d \\mu-\\int(c f)^{-} d \\mu \\\\\n& =\\int c f^{+} d \\mu-\\int c f^{-} d \\mu \\\\\n& =c\\left(\\int f^{+} d \\mu-\\int f^{-} d \\mu\\right) \\\\\n& =c \\int f d \\mu,\n\\end{aligned}\n$$\n\nwhere the third line follows from the first paragraph of this proof.\n\nFinally, now suppose $c<0$ (still assuming that $f$ takes values in $[-\\infty, \\infty]$ ). Then $-c>0$ and\n\n$$\n\\begin{aligned}\n\\int c f d \\mu & =\\int(c f)^{+} d \\mu-\\int(c f)^{-} d \\mu \\\\\n& =\\int(-c) f^{-} d \\mu-\\int(-c) f^{+} d \\mu \\\\\n& =(-c)\\left(\\int f^{-} d \\mu-\\int f^{+} d \\mu\\right) \\\\\n& =c \\int f d \\mu,\n\\end{aligned}\n$$\n\ncompleting the proof.\n\nNow we prove that integration with respect to a measure has the additive property required for a good theory of integration.\n\n### 3.21 additivity of integration\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f, g: X \\rightarrow \\mathbf{R}$ are $\\mathcal{S}$-measurable functions such that $\\int|f| d \\mu<\\infty$ and $\\int|g| d \\mu<\\infty$. Then\n\n$$\n\\int(f+g) d \\mu=\\int f d \\mu+\\int g d \\mu\n$$\n\nProof Clearly\n\n$$\n\\begin{aligned}\n(f+g)^{+}-(f+g)^{-} & =f+g \\\\\n& =f^{+}-f^{-}+g^{+}-g^{-}\n\\end{aligned}\n$$\n\nThus\n\n$$\n(f+g)^{+}+f^{-}+g^{-}=(f+g)^{-}+f^{+}+g^{+} .\n$$\n\nBoth sides of the equation above are sums of nonnegative functions. Thus integrating both sides with respect to $\\mu$ and using 3.16 gives\n\n$\\int(f+g)^{+} d \\mu+\\int f^{-} d \\mu+\\int g^{-} d \\mu=\\int(f+g)^{-} d \\mu+\\int f^{+} d \\mu+\\int g^{+} d \\mu$.\n\nRearranging the equation above gives\n\n$\\int(f+g)^{+} d \\mu-\\int(f+g)^{-} d \\mu=\\int f^{+} d \\mu-\\int f^{-} d \\mu+\\int g^{+} d \\mu-\\int g^{-} d \\mu$,\n\nwhere the left side is not of the form $\\infty-\\infty$ because $(f+g)^{+} \\leq f^{+}+g^{+}$and $(f+g)^{-} \\leq f^{-}+g^{-}$. The equation above can be rewritten as\n\n$$\n\\int(f+g) d \\mu=\\int f d \\mu+\\int g d \\mu\n$$\n\ncompleting the proof.\n\nGottfried Leibniz (1646-1716) invented the symbol $\\int$ to denote integration in 1675.\n\nThe next result resembles 3.8, but now the functions are allowed to be real valued.\n\n### 3.22 integration is order preserving\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f, g: X \\rightarrow \\mathbf{R}$ are $\\mathcal{S}$-measurable functions such that $\\int f d \\mu$ and $\\int g d \\mu$ are defined. Suppose also that $f(x) \\leq g(x)$ for all $x \\in X$. Then $\\int f d \\mu \\leq \\int g d \\mu$.\n\nProof The cases where $\\int f d \\mu= \\pm \\infty$ or $\\int g d \\mu= \\pm \\infty$ are left to the reader. Thus we assume that $\\int|f| d \\mu<\\infty$ and $\\int|g| d \\mu<\\infty$.\n\nThe additivity (3.21) and homogeneity ( 3.20 with $c=-1$ ) of integration imply that\n\n$$\n\\int g d \\mu-\\int f d \\mu=\\int(g-f) d \\mu\n$$\n\nThe last integral is nonnegative because $g(x)-f(x) \\geq 0$ for all $x \\in X$.\n\nThe inequality in the next result receives frequent use.\n\n### 3.23 absolute value of integral $\\leq$ integral of absolute value\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[-\\infty, \\infty]$ is a function such that $\\int f d \\mu$ is defined. Then\n\n$$\n\\left|\\int f d \\mu\\right| \\leq \\int|f| d \\mu\n$$\n\nProof Because $\\int f d \\mu$ is defined, $f$ is an $\\mathcal{S}$-measurable function and least one of $\\int f^{+} d \\mu$ and $\\int f^{-} d \\mu$ is finite. Thus\n\n$$\n\\begin{aligned}\n\\left|\\int f d \\mu\\right| & =\\left|\\int f^{+} d \\mu-\\int f^{-} d \\mu\\right| \\\\\n& \\leq \\int f^{+} d \\mu+\\int f^{-} d \\mu \\\\\n& =\\int\\left(f^{+}+f^{-}\\right) d \\mu \\\\\n& =\\int|f| d \\mu,\n\\end{aligned}\n$$\n\nas desired.\n\n## EXERCISES 3A\n\n1 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[0, \\infty]$ is an $\\mathcal{S}$-measurable function such that $\\int f d \\mu<\\infty$. Explain why\n\n$$\n\\inf _{E} f=0\n$$\n\nfor each set $E \\in \\mathcal{S}$ with $\\mu(E)=\\infty$.\n\n2 Suppose $X$ is a set, $\\mathcal{S}$ is a $\\sigma$-algebra on $X$, and $c \\in X$. Define the Dirac measure $\\delta_{c}$ on $(X, \\mathcal{S})$ by\n\n$$\n\\delta_{c}(E)= \\begin{cases}1 & \\text { if } c \\in E \\\\ 0 & \\text { if } c \\notin E\\end{cases}\n$$\n\nProve that if $f: X \\rightarrow[0, \\infty]$ is $\\mathcal{S}$-measurable, then $\\int f \\mathrm{~d} \\delta_{c}=f(c)$.\n\n[Careful: $\\{c\\}$ may not be in $\\mathcal{S}$.]\n\n3 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[0, \\infty]$ is an $\\mathcal{S}$-measurable function. Prove that\n\n$$\n\\int f d \\mu>0 \\text { if and only if } \\mu(\\{x \\in X: f(x)>0\\})>0 \\text {. }\n$$\n\n4 Give an example of a Borel measurable function $f:[0,1] \\rightarrow(0, \\infty)$ such that $L(f,[0,1])=0$.\n\n[Recall that $L(f,[0,1])$ denotes the lower Riemann integral, which was defined in Section 1A. If $\\lambda$ is Lebesgue measure on $[0,1]$, then the previous exercise states that $\\int f d \\lambda>0$ for this function $f$, which is what we expect of a positive function. Thus even though both $L(f,[0,1])$ and $\\int f d \\lambda$ are defined by taking the supremum of approximations from below, Lebesgue measure captures the right behavior for this function $f$ and the lower Riemann integral does not.]\n\n5 Verify the assertion that integration with respect to counting measure is summation (Example 3.6).\n\n6 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $f: X \\rightarrow[0, \\infty]$ is $\\mathcal{S}$-measurable, and $P$ and $P^{\\prime}$ are $\\mathcal{S}$-partitions of $X$ such that each set in $P^{\\prime}$ is contained in some set in $P$. Prove that $\\mathcal{L}(f, P) \\leq \\mathcal{L}\\left(f, P^{\\prime}\\right)$.\n\n7 Suppose $X$ is a set, $\\mathcal{S}$ is the $\\sigma$-algebra of all subsets of $X$, and $w: X \\rightarrow[0, \\infty]$ is a function. Define a measure $\\mu$ on $(X, \\mathcal{S})$ by\n\n$$\n\\mu(E)=\\sum_{x \\in E} w(x)\n$$\n\nfor $E \\subset X$. Prove that if $f: X \\rightarrow[0, \\infty]$ is a function, then\n\n$$\n\\int f d \\mu=\\sum_{x \\in X} w(x) f(x)\n$$\n\nwhere the infinite sums above are defined as the supremum of all sums over finite subsets of $E$ (first sum) or $X$ (second sum).\n\n8 Suppose $\\lambda$ denotes Lebesgue measure on R. Give an example of a sequence $f_{1}, f_{2}, \\ldots$ of simple Borel measurable functions from $\\mathbf{R}$ to $[0, \\infty)$ such that $\\lim _{k \\rightarrow \\infty} f_{k}(x)=0$ for every $x \\in \\mathbf{R}$ but $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\lambda=1$.\n\n9 Suppose $\\mu$ is a measure on a measurable space $(X, \\mathcal{S})$ and $f: X \\rightarrow[0, \\infty]$ is an $\\mathcal{S}$-measurable function. Define $v: \\mathcal{S} \\rightarrow[0, \\infty]$ by\n\n$$\nv(A)=\\int \\chi_{A} f d \\mu\n$$\n\nfor $A \\in \\mathcal{S}$. Prove that $v$ is a measure on $(X, \\mathcal{S})$.\n\n10 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f_{1}, f_{2}, \\ldots$ is a sequence of nonnegative $\\mathcal{S}$-measurable functions. Define $f: X \\rightarrow[0, \\infty]$ by $f(x)=\\sum_{k=1}^{\\infty} f_{k}(x)$. Prove that\n\n$$\n\\int f d \\mu=\\sum_{k=1}^{\\infty} \\int f_{k} d \\mu\n$$\n\n11 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f_{1}, f_{2}, \\ldots$ are $\\mathcal{S}$-measurable functions from $X$ to $\\mathbf{R}$ such that $\\sum_{k=1}^{\\infty} \\int\\left|f_{k}\\right| d \\mu<\\infty$. Prove that there exists $E \\in \\mathcal{S}$ such that $\\mu(X \\backslash E)=0$ and $\\lim _{k \\rightarrow \\infty} f_{k}(x)=0$ for every $x \\in E$.\n\n12 Show that there exists a Borel measurable function $f: \\mathbf{R} \\rightarrow(0, \\infty)$ such that $\\int \\chi_{I} f d \\lambda=\\infty$ for every nonempty open interval $I \\subset \\mathbf{R}$, where $\\lambda$ denotes Lebesgue measure on $\\mathbf{R}$.\n\n13 Give an example to show that the Monotone Convergence Theorem (3.11) can fail if the hypothesis that $f_{1}, f_{2}, \\ldots$ are nonnegative functions is dropped.\n\n14 Give an example to show that the Monotone Convergence Theorem can fail if the hypothesis of an increasing sequence of functions is replaced by a hypothesis of a decreasing sequence of functions.\n\n[This exercise shows that the Monotone Convergence Theorem should be called the Increasing Convergence Theorem. However, see Exercise 20.]\n\n15 Suppose $\\lambda$ is Lebesgue measure on $\\mathbf{R}$ and $f: \\mathbf{R} \\rightarrow[-\\infty, \\infty]$ is a Borel measurable function such that $\\int f d \\lambda$ is defined.\n\n(a) For $t \\in \\mathbf{R}$, define $f_{t}: \\mathbf{R} \\rightarrow[-\\infty, \\infty]$ by $f_{t}(x)=f(x-t)$. Prove that $\\int f_{t} d \\lambda=\\int f d \\lambda$ for all $t \\in \\mathbf{R}$.\n\n(b) For $t \\in \\mathbf{R}$, define $f_{t}: \\mathbf{R} \\rightarrow[-\\infty, \\infty]$ by $f_{t}(x)=f(t x)$. Prove that $\\int f_{t} d \\lambda=\\frac{1}{|t|} \\int f d \\lambda$ for all $t \\in \\mathbf{R} \\backslash\\{0\\}$.\n\n16 Suppose $\\mathcal{S}$ and $\\mathcal{T}$ are $\\sigma$-algebras on a set $X$ and $\\mathcal{S} \\subset \\mathcal{T}$. Suppose $\\mu_{1}$ is a measure on $(X, \\mathcal{S}), \\mu_{2}$ is a measure on $(X, \\mathcal{T})$, and $\\mu_{1}(E)=\\mu_{2}(E)$ for all $E \\in \\mathcal{S}$. Prove that if $f: X \\rightarrow[0, \\infty]$ is $\\mathcal{S}$-measurable, then $\\int f d \\mu_{1}=\\int f d \\mu_{2}$.\n\nFor $x_{1}, x_{2}, \\ldots$ a sequence in $[-\\infty, \\infty]$, define $\\underset{k \\rightarrow \\infty}{\\lim \\inf } x_{k}$ by\n\n$$\n\\liminf _{k \\rightarrow \\infty} x_{k}=\\lim _{k \\rightarrow \\infty} \\inf \\left\\{x_{k}, x_{k+1}, \\ldots\\right\\}\n$$\n\nNote that $\\inf \\left\\{x_{k}, x_{k+1}, \\ldots\\right\\}$ is an increasing function of $k$; thus the limit above on the right exists in $[-\\infty, \\infty]$.\n\n17 Suppose that $(X, \\mathcal{S}, \\mu)$ is a measure space and $f_{1}, f_{2}, \\ldots$ is a sequence of nonnegative $\\mathcal{S}$-measurable functions on $X$. Define a function $f: X \\rightarrow[0, \\infty]$ by $f(x)=\\liminf _{k \\rightarrow \\infty} f_{k}(x)$.\n\n(a) Show that $f$ is an $\\mathcal{S}$-measurable function.\n\n(b) Prove that\n\n$$\n\\int f d \\mu \\leq \\liminf _{k \\rightarrow \\infty} \\int f_{k} d \\mu\n$$\n\n(c) Give an example showing that the inequality in (b) can be a strict inequality even when $\\mu(X)<\\infty$ and the family of functions $\\left\\{f_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}$is uniformly bounded.\n\n[The result in (b) is called Fatou's Lemma. Some textbooks prove Fatou's Lemma and then use it to prove the Monotone Convergence Theorem. Here we are taking the reverse approach-you should be able to use the Monotone Convergence Theorem to give a clean proof of Fatou's Lemma.]\n\n18 Give an example of a sequence $x_{1}, x_{2}, \\ldots$ of real numbers such that\n\n$$\n\\lim _{n \\rightarrow \\infty} \\sum_{k=1}^{n} x_{k} \\text { exists in } \\mathbf{R}\n$$\n\nbut $\\int x d \\mu$ is not defined, where $\\mu$ is counting measure on $\\mathbf{Z}^{+}$and $x$ is the function from $\\mathbf{Z}^{+}$to $\\mathbf{R}$ defined by $x(k)=x_{k}$.\n\n19 Show that if $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow[0, \\infty)$ is $\\mathcal{S}$-measurable, then\n\n$$\n\\mu(X) \\inf _{X} f \\leq \\int f d \\mu \\leq \\mu(X) \\sup _{X} f\n$$\n\n20 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f_{1}, f_{2}, \\ldots$ is a monotone (meaning either increasing or decreasing) sequence of $\\mathcal{S}$-measurable functions. Define $f: X \\rightarrow[-\\infty, \\infty]$ by\n\n$$\nf(x)=\\lim _{k \\rightarrow \\infty} f_{k}(x)\n$$\n\nProve that if $\\int\\left|f_{1}\\right| d \\mu<\\infty$, then\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu\n$$\n\n21 Henri Lebesgue wrote the following about his method of integration:\n\nI have to pay a certain sum, which I have collected in my pocket. I take the bills and coins out of my pocket and give them to the creditor in the order I find them until I have reached the total sum. This is the Riemann integral. But I can proceed differently. After I have taken all the money out of my pocket I order the bills and coins according to identical values and then I pay the several heaps one after the other to the creditor. This is my integral.\n\nUse 3.15 to explain what Lebesgue meant and to explain why integration of a function with respect to a measure can be thought of as partitioning the range of the function, in contrast to Riemann integration, which depends upon partitioning the domain of the function.\n\n[The quote above is taken from page 796 of The Princeton Companion to Mathematics, edited by Timothy Gowers.]\n\n## 3B Limits of Integrals \\& Integrals of Limits\n\nThis section focuses on interchanging limits and integrals. Those tools allow us to characterize the Riemann integrable functions in terms of Lebesgue measure. We also develop some good approximation tools that will be useful in later chapters.\n\n## Bounded Convergence Theorem\n\nWe begin this section by introducing some useful notation.\n\n3.24 Definition integration on a subset; $\\int_{E} f d \\mu$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $E \\in \\mathcal{S}$. If $f: X \\rightarrow[-\\infty, \\infty]$ is an $\\mathcal{S}$-measurable function, then $\\int_{E} f \\mathrm{~d} \\mu$ is defined by\n\n$$\n\\int_{E} f d \\mu=\\int \\chi_{E} f d \\mu\n$$\n\nif the right side of the equation above is defined; otherwise $\\int_{E} f d \\mu$ is undefined.\n\nAlternatively, you can think of $\\int_{E} f d \\mu$ as $\\left.\\int f\\right|_{E} d \\mu_{E}$, where $\\mu_{E}$ is the measure obtained by restricting $\\mu$ to the elements of $\\mathcal{S}$ that are contained in $E$.\n\nNotice that according to the definition above, the notation $\\int_{X} f d \\mu$ means the same as $\\int f d \\mu$. The following easy result illustrates the use of this new notation.\n\n### 3.25 bounding an integral\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $E \\in \\mathcal{S}$, and $f: X \\rightarrow[-\\infty, \\infty]$ is a function such that $\\int_{E} f d \\mu$ is defined. Then\n\n$$\n\\left|\\int_{E} f d \\mu\\right| \\leq \\mu(E) \\sup _{E}|f|\n$$\n\nProof Let $c=\\sup _{E}|f|$. We have\n\n$$\n\\begin{aligned}\n\\left|\\int_{E} f d \\mu\\right| & =\\left|\\int \\chi_{E} f d \\mu\\right| \\\\\n& \\leq \\int \\chi_{E}|f| d \\mu \\\\\n& \\leq \\int c \\chi_{E} d \\mu \\\\\n& =c \\mu(E),\n\\end{aligned}\n$$\n\nwhere the second line comes from 3.23, the third line comes from 3.8, and the fourth line comes from 3.15.\n\nThe next result could be proved as a special case of the Dominated Convergence Theorem (3.31), which we prove later in this section. Thus you could skip the proof here. However, sometimes you get more insight by seeing an easier proof of an important special case. Thus you may want to read the easy proof of the Bounded Convergence Theorem that is presented next.\n\n### 3.26 Bounded Convergence Theorem\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space with $\\mu(X)<\\infty$. Suppose $f_{1}, f_{2}, \\ldots$ is a sequence of $\\mathcal{S}$-measurable functions from $X$ to $\\mathbf{R}$ that converges pointwise on $X$ to a function $f: X \\rightarrow \\mathbf{R}$. If there exists $c \\in(0, \\infty)$ such that\n\n$$\n\\left|f_{k}(x)\\right| \\leq c\n$$\n\nfor all $k \\in \\mathbf{Z}^{+}$and all $x \\in X$, then\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu\n$$\n\nProof The function $f$ is $\\mathcal{S}$-measurable by 2.48 .\n\nSuppose $c$ satisfies the hypothesis of this theorem. Let $\\varepsilon>0$. By Egorov's Theorem (2.85), there exists $E \\in \\mathcal{S}$ such that $\\mu(X \\backslash E)<\\frac{\\varepsilon}{4 c}$ and $f_{1}, f_{2}, \\ldots$ converges uniformly to $f$ on $E$. Now\n\n$$\n\\begin{aligned}\n\\left|\\int f_{k} d \\mu-\\int f d \\mu\\right| & =\\left|\\int_{X \\backslash E} f_{k} d \\mu-\\int_{X \\backslash E} f d \\mu+\\int_{E}\\left(f_{k}-f\\right) d \\mu\\right| \\\\\n& \\leq \\int_{X \\backslash E}\\left|f_{k}\\right| d \\mu+\\int_{X \\backslash E}|f| d \\mu+\\int_{E}\\left|f_{k}-f\\right| d \\mu \\\\\n& <\\frac{\\varepsilon}{2}+\\mu(E) \\sup _{E}\\left|f_{k}-f\\right|,\n\\end{aligned}\n$$\n\nwhere the last inequality follows from 3.25 . Because $f_{1}, f_{2}, \\ldots$ converges uniformly to $f$ on $E$ and $\\mu(E)<\\infty$, the right side of the inequality above is less than $\\varepsilon$ for $k$ sufficiently large, which completes the proof.\n\n## Sets of Measure 0 in Integration Theorems\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space. If $f, g: X \\rightarrow[-\\infty, \\infty]$ are $\\mathcal{S}$-measurable functions and\n\n$$\n\\mu(\\{x \\in X: f(x) \\neq g(x)\\})=0,\n$$\n\nthen the definition of an integral implies that $\\int f d \\mu=\\int g d \\mu$ (or both integrals are undefined). Because what happens on a set of measure 0 often does not matter, the following definition is useful.\n\n### 3.27 Definition almost every\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space. A set $E \\in \\mathcal{S}$ is said to contain $\\mu$-almost every element of $X$ if $\\mu(X \\backslash E)=0$. If the measure $\\mu$ is clear from the context, then the phrase almost every can be used (abbreviated by some authors to $a . e$.).\n\nFor example, almost every real number is irrational (with respect to the usual Lebesgue measure on $\\mathbf{R}$ ) because $|\\mathbf{Q}|=0$.\n\nTheorems about integrals can almost always be relaxed so that the hypotheses apply only almost everywhere instead of everywhere. For example, consider the Bounded Convergence Theorem (3.26), one of whose hypotheses is that\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}(x)=f(x)\n$$\n\nfor all $x \\in X$. Suppose that the hypotheses of the Bounded Convergence Theorem hold except that the equation above holds only almost everywhere, meaning there is a set $E \\in \\mathcal{S}$ such that $\\mu(X \\backslash E)=0$ and the equation above holds for all $x \\in E$. Define new functions $g_{1}, g_{2}, \\ldots$ and $g$ by\n\n$$\ng_{k}(x)=\\left\\{\\begin{array}{ll}\nf_{k}(x) & \\text { if } x \\in E, \\\\\n0 & \\text { if } x \\in X \\backslash E\n\\end{array} \\quad \\text { and } \\quad g(x)= \\begin{cases}f(x) & \\text { if } x \\in E \\\\\n0 & \\text { if } x \\in X \\backslash E\\end{cases}\\right.\n$$\n\nThen\n\n$$\n\\lim _{k \\rightarrow \\infty} g_{k}(x)=g(x)\n$$\n\nfor all $x \\in X$. Hence the Bounded Convergence Theorem implies that\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int g_{k} d \\mu=\\int g d \\mu\n$$\n\nwhich immediately implies that\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu\n$$\n\nbecause $\\int g_{k} d \\mu=\\int f_{k} d \\mu$ and $\\int g d \\mu=\\int f d \\mu$.\n\n## Dominated Convergence Theorem\n\nThe next result tells us that if a nonnegative function has a finite integral, then its integral over all small sets (in the sense of measure) is small.\n\n### 3.28 integrals on small sets are small\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $g: X \\rightarrow[0, \\infty]$ is $\\mathcal{S}$-measurable, and $\\int g d \\mu<\\infty$. Then for every $\\varepsilon>0$, there exists $\\delta>0$ such that\n\n$$\n\\int_{B} g d \\mu<\\varepsilon\n$$\n\nfor every set $B \\in \\mathcal{S}$ such that $\\mu(B)<\\delta$.\n\nProof Suppose $\\varepsilon>0$. Let $h: X \\rightarrow[0, \\infty)$ be a simple $\\mathcal{S}$-measurable function such that $0 \\leq h \\leq g$ and\n\n$$\n\\int g d \\mu-\\int h d \\mu<\\frac{\\varepsilon}{2}\n$$\n\nthe existence of a function $h$ with these properties follows from 3.9. Let\n\n$$\nH=\\max \\{h(x): x \\in X\\}\n$$\n\nand let $\\delta>0$ be such that $H \\delta<\\frac{\\varepsilon}{2}$.\n\nSuppose $B \\in \\mathcal{S}$ and $\\mu(B)<\\delta$. Then\n\n$$\n\\begin{aligned}\n\\int_{B} g d \\mu & =\\int_{B}(g-h) d \\mu+\\int_{B} h d \\mu \\\\\n& \\leq \\int(g-h) d \\mu+H \\mu(B) \\\\\n& <\\frac{\\varepsilon}{2}+H \\delta \\\\\n& <\\varepsilon,\n\\end{aligned}\n$$\n\nas desired.\n\nSome theorems, such as Egorov's Theorem (2.85) have as a hypothesis that the measure of the entire space is finite. The next result sometimes allows us to get around this hypothesis by restricting attention to a key set of finite measure.\n\n3.29 integrable functions live mostly on sets of finite measure\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $g: X \\rightarrow[0, \\infty]$ is $\\mathcal{S}$-measurable, and $\\int g d \\mu<\\infty$. Then for every $\\varepsilon>0$, there exists $E \\in \\mathcal{S}$ such that $\\mu(E)<\\infty$ and\n\n$$\n\\int_{X \\backslash E} g d \\mu<\\varepsilon\n$$\n\nProof Suppose $\\varepsilon>0$. Let $P$ be an $\\mathcal{S}$-partition $A_{1}, \\ldots, A_{m}$ of $X$ such that\n\n$$\n\\int g d \\mu<\\varepsilon+\\mathcal{L}(g, P) .\n$$\n\nLet $E$ be the union of those $A_{j}$ such that $\\inf _{A_{j}} g>0$. Then $\\mu(E)<\\infty$ (because otherwise we would have $\\mathcal{L}(g, P)=\\infty$, which contradicts the hypothesis that $\\left.\\int g d \\mu<\\infty\\right)$. Now\n\n$$\n\\begin{aligned}\n\\int_{X \\backslash E} g d \\mu & =\\int g d \\mu-\\int \\chi_{E} g d \\mu \\\\\n& <(\\varepsilon+\\mathcal{L}(g, P))-\\mathcal{L}\\left(\\chi_{E} g, P\\right) \\\\\n& =\\varepsilon,\n\\end{aligned}\n$$\n\nwhere the second line follows from 3.30 and the definition of the integral of a nonnegative function, and the last line holds because $\\inf _{A_{j}} g=0$ for each $A_{j}$ not\ncontained in $E$.\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f_{1}, f_{2}, \\ldots$ is a sequence of $\\mathcal{S}$-measurable functions on $X$ such that $\\lim _{k \\rightarrow \\infty} f_{k}(x)=f(x)$ for every (or almost every) $x \\in X$. In general, it is not true that $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu$ (see Exercises 1 and 2 ).\n\nWe already have two good theorems about interchanging limits and integrals. However, both of these theorems have restrictive hypotheses. Specifically, the Monotone Convergence Theorem (3.11) requires all the functions to be nonnegative and it requires the sequence of functions to be increasing. The Bounded Convergence Theorem (3.26) requires the measure of the whole space to be finite and it requires the sequence of functions to be uniformly bounded by a constant.\n\nThe next theorem is the grand result in this area. It does not require the sequence of functions to be nonnegative, it does not require the sequence of functions to be increasing, it does not require the measure of the whole space to be finite, and it does not require the sequence of functions to be uniformly bounded. All these hypotheses are replaced only by a requirement that the sequence of functions is pointwise bounded by a function with a finite integral.\n\nNotice that the Bounded Convergence Theorem follows immediately from the result below (take $g$ to be an appropriate constant function and use the hypothesis in the Bounded Convergence Theorem that $\\mu(X)<\\infty$ ).\n\n### 3.31 Dominated Convergence Theorem\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $f: X \\rightarrow[-\\infty, \\infty]$ is $\\mathcal{S}$-measurable, and $f_{1}, f_{2}, \\ldots$ are $\\mathcal{S}$-measurable functions from $X$ to $[-\\infty, \\infty]$ such that\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}(x)=f(x)\n$$\n\nfor almost every $x \\in X$. If there exists an $\\mathcal{S}$-measurable function $g: X \\rightarrow[0, \\infty]$ such that\n\n$$\n\\int g d \\mu<\\infty \\quad \\text { and } \\quad\\left|f_{k}(x)\\right| \\leq g(x)\n$$\n\nfor every $k \\in \\mathbf{Z}^{+}$and almost every $x \\in X$, then\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu\n$$\n\nProof Suppose $g: X \\rightarrow[0, \\infty]$ satisfies the hypotheses of this theorem. If $E \\in \\mathcal{S}$, then\n\n$$\n\\begin{aligned}\n\\left|\\int f_{k} d \\mu-\\int f d \\mu\\right| & =\\left|\\int_{X \\backslash E} f_{k} d \\mu-\\int_{X \\backslash E} f d \\mu+\\int_{E} f_{k} d \\mu-\\int_{E} f d \\mu\\right| \\\\\n& \\leq\\left|\\int_{X \\backslash E} f_{k} d \\mu\\right|+\\left|\\int_{X \\backslash E} f d \\mu\\right|+\\left|\\int_{E} f_{k} d \\mu-\\int_{E} f d \\mu\\right| \\\\\n& \\leq 2 \\int_{X \\backslash E} g d \\mu+\\left|\\int_{E} f_{k} d \\mu-\\int_{E} f d \\mu\\right| .\n\\end{aligned}\n$$\n\nCase 1: Suppose $\\mu(X)<\\infty$.\n\nLet $\\varepsilon>0$. By 3.28 , there exists $\\delta>0$ such that\n\n$$\n\\int_{B} g d \\mu<\\frac{\\varepsilon}{4}\n$$\n\nfor every set $B \\in \\mathcal{S}$ such that $\\mu(B)<\\delta$. By Egorov's Theorem (2.85), there exists a set $E \\in \\mathcal{S}$ such that $\\mu(X \\backslash E)<\\delta$ and $f_{1}, f_{2}, \\ldots$ converges uniformly to $f$ on $E$. Now 3.32 and 3.33 imply that\n\n$$\n\\left|\\int f_{k} d \\mu-\\int f d \\mu\\right|<\\frac{\\varepsilon}{2}+\\left|\\int_{E}\\left(f_{k}-f\\right) d \\mu\\right|\n$$\n\nBecause $f_{1}, f_{2}, \\ldots$ converges uniformly to $f$ on $E$ and $\\mu(E)<\\infty$, the last term on the right is less than $\\frac{\\varepsilon}{2}$ for all sufficiently large $k$. Thus $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu$, completing the proof of case 1.\n\nCase 2: Suppose $\\mu(X)=\\infty$.\n\nLet $\\varepsilon>0$. By 3.29, there exists $E \\in \\mathcal{S}$ such that $\\mu(E)<\\infty$ and\n\n$$\n\\int_{X \\backslash E} g d \\mu<\\frac{\\varepsilon}{4}\n$$\n\nThe inequality above and 3.32 imply that\n\n$$\n\\left|\\int f_{k} d \\mu-\\int f d \\mu\\right|<\\frac{\\varepsilon}{2}+\\left|\\int_{E} f_{k} d \\mu-\\int_{E} f d \\mu\\right| .\n$$\n\nBy case 1 as applied to the sequence $\\left.f_{1}\\right|_{E},\\left.f_{2}\\right|_{E}, \\ldots$, the last term on the right is less than $\\frac{\\varepsilon}{2}$ for all sufficiently large $k$. Thus $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=\\int f d \\mu$, completing the proof of case 2 .\n\n## Riemann Integrals and Lebesgue Integrals\n\nWe can now use the tools we have developed to characterize the Riemann integrable functions. In the theorem below, the left side of the last equation denotes the Riemann integral.\n\n### 3.34 Riemann integrable $\\Longleftrightarrow$ continuous almost everywhere\n\nSuppose $a<b$ and $f:[a, b] \\rightarrow \\mathbf{R}$ is a bounded function. Then $f$ is Riemann integrable if and only if\n\n$$\n\\mid\\{x \\in[a, b]: f \\text { is not continuous at } x\\} \\mid=0 \\text {. }\n$$\n\nFurthermore, if $f$ is Riemann integrable and $\\lambda$ denotes Lebesgue measure on $\\mathbf{R}$, then $f$ is Lebesgue measurable and\n\n$$\n\\int_{a}^{b} f=\\int_{[a, b]} f d \\lambda .\n$$\n\nProof Suppose $n \\in \\mathbf{Z}^{+}$. Consider the partition $P_{n}$ that divides $[a, b]$ into $2^{n}$ subintervals of equal size. Let $I_{1}, \\ldots, I_{2^{n}}$ be the corresponding closed subintervals, each of length $(b-a) / 2^{n}$. Let\n\n3.35\n\n$$\ng_{n}=\\sum_{j=1}^{2^{n}}\\left(\\inf _{I_{j}} f\\right) \\chi_{I_{j}} \\quad \\text { and } \\quad h_{n}=\\sum_{j=1}^{2^{n}}\\left(\\sup _{I_{j}} f\\right) \\chi_{I_{j}}\n$$\n\nThe lower and upper Riemann sums of $f$ for the partition $P_{n}$ are given by integrals. Specifically,\n\n$$\nL\\left(f, P_{n},[a, b]\\right)=\\int_{[a, b]} g_{n} d \\lambda \\quad \\text { and } \\quad U\\left(f, P_{n},[a, b]\\right)=\\int_{[a, b]} h_{n} d \\lambda\n$$\n\nwhere $\\lambda$ is Lebesgue measure on $\\mathbf{R}$.\n\nThe definitions of $g_{n}$ and $h_{n}$ given in 3.35 are actually just a first draft of the definitions. A slight problem arises at each point that is in two of the intervals $I_{1}, \\ldots, I_{2^{n}}$ (in other words, at endpoints of these intervals other than $a$ and $b$ ). At each of these points, change the value of $g_{n}$ to be the infimum of $f$ over the union of the two intervals that contain the point, and change the value of $h_{n}$ to be the supremum of $f$ over the union of the two intervals that contain the point. This change modifies $g_{n}$ and $h_{n}$ on only a finite number of points. Thus the integrals in 3.36 are not affected. This change is needed in order to make 3.38 true (otherwise the two sets in 3.38 might differ by at most countably many points, which would not really change the proof but which would not be as aesthetically pleasing).\n\nClearly $g_{1} \\leq g_{2} \\leq \\cdots$ is an increasing sequence of functions and $h_{1} \\geq h_{2} \\geq \\cdots$ is a decreasing sequence of functions on $[a, b]$. Define functions $f^{\\mathrm{L}}:[a, b] \\rightarrow \\mathbf{R}$ and $f^{\\mathrm{U}}:[a, b] \\rightarrow \\mathbf{R}$ by\n\n$$\nf^{\\mathrm{L}}(x)=\\lim _{n \\rightarrow \\infty} g_{n}(x) \\text { and } f^{\\mathrm{U}}(x)=\\lim _{n \\rightarrow \\infty} h_{n}(x) \\text {. }\n$$\n\nTaking the limit as $n \\rightarrow \\infty$ of both equations in 3.36 and using the Bounded Convergence Theorem (3.26) along with Exercise 7 in Section 1A, we see that $f^{L}$ and $f^{U}$ are Lebesgue measurable functions and\n\n$$\nL(f,[a, b])=\\int_{[a, b]} f^{\\mathrm{L}} d \\lambda \\quad \\text { and } \\quad U(f,[a, b])=\\int_{[a, b]} f^{\\mathrm{U}} d \\lambda\n$$\n\nNow 3.37 implies that $f$ is Riemann integrable if and only if\n\n$$\n\\int_{[a, b]}\\left(f^{\\mathrm{U}}-f^{\\mathrm{L}}\\right) d \\lambda=0\n$$\n\nBecause $f^{\\mathrm{L}}(x) \\leq f(x) \\leq f^{\\mathrm{U}}(x)$ for all $x \\in[a, b]$, the equation above holds if and only if\n\n$$\n\\left|\\left\\{x \\in[a, b]: f^{\\mathrm{U}}(x) \\neq f^{\\mathrm{L}}(x)\\right\\}\\right|=0 .\n$$\n\nThe remaining details of the proof can be completed by noting that\n\n$3.38\\left\\{x \\in[a, b]: f^{\\mathrm{U}}(x) \\neq f^{\\mathrm{L}}(x)\\right\\}=\\{x \\in[a, b]: f$ is not continuous at $x\\}$.\n\nWe previously defined the notation $\\int_{a}^{b} f$ to mean the Riemann integral of $f$. Because the Riemann integral and Lebesgue integral agree for Riemann integrable functions (see 3.34), we now redefine $\\int_{a}^{b} f$ to denote the Lebesgue integral.\n\n3.39 Definition $\\int_{a}^{b} f$\n\nSuppose $-\\infty \\leq a<b \\leq \\infty$ and $f:(a, b) \\rightarrow \\mathbf{R}$ is Lebesgue measurable. Then\n\n- $\\int_{a}^{b} f$ and $\\int_{a}^{b} f(x) d x$ mean $\\int_{(a, b)} f d \\lambda$, where $\\lambda$ is Lebesgue measure on $\\mathbf{R}$;\n- $\\int_{b}^{a} f$ is defined to be $-\\int_{a}^{b} f$.\n\nThe definition in the second bullet point above is made so that equations such as\n\n$$\n\\int_{a}^{b} f=\\int_{a}^{c} f+\\int_{c}^{b} f\n$$\n\nremain valid even if, for example, $a<b<c$.\n\n## Approximation by Nice Functions\n\nIn the next definition, the notation $\\|f\\|_{1}$ should be $\\|f\\|_{1, \\mu}$ because it depends upon the measure $\\mu$ as well as upon $f$. However, $\\mu$ is usually clear from the context. In some books, you may see the notation $\\mathcal{L}^{1}(X, \\mathcal{S}, \\mu)$ instead of $\\mathcal{L}^{1}(\\mu)$.\n\n3.40\n\n## Definition $\\|f\\|_{1} ; \\mathcal{L}^{1}(\\mu)$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space. If $f: X \\rightarrow[-\\infty, \\infty]$ is $\\mathcal{S}$-measurable, then the $\\mathcal{L}^{1}$-norm of $f$ is denoted by $\\|f\\|_{1}$ and is defined by\n\n$$\n\\|f\\|_{1}=\\int|f| d \\mu\n$$\n\nThe Lebesgue space $\\mathcal{L}^{1}(\\mu)$ is defined by\n\n$\\mathcal{L}^{1}(\\mu)=\\left\\{f: f\\right.$ is an $\\mathcal{S}$-measurable function from $X$ to $\\mathbf{R}$ and $\\left.\\|f\\|_{1}<\\infty\\right\\}$.\n\nThe terminology and notation used above are convenient even though $\\|\\cdot\\|_{1}$ might not be a genuine norm (to be defined in Chapter 6).\n\n### 3.41 Example $\\mathcal{L}^{1}(\\mu)$ functions that take on only finitely many values\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $E_{1}, \\ldots, E_{n}$ are disjoint subsets of $X$. Suppose $a_{1}, \\ldots, a_{n}$ are distinct nonzero real numbers. Then\n\n$$\na_{1} \\chi_{E_{1}}+\\cdots+a_{n} \\chi_{E_{n}} \\in \\mathcal{L}^{1}(\\mu)\n$$\n\nif and only if $E_{k} \\in \\mathcal{S}$ and $\\mu\\left(E_{k}\\right)<\\infty$ for all $k \\in\\{1, \\ldots, n\\}$. Furthermore,\n\n$$\n\\left\\|a_{1} \\chi_{E_{1}}+\\cdots+a_{n} \\chi_{E_{n}}\\right\\|_{1}=\\left|a_{1}\\right| \\mu\\left(E_{1}\\right)+\\cdots+\\left|a_{n}\\right| \\mu\\left(E_{n}\\right) .\n$$\n\n### 3.42 Example $\\ell^{1}$\n\nIf $\\mu$ is counting measure on $\\mathbf{Z}^{+}$and $x=\\left(x_{1}, x_{2}, \\ldots\\right)$ is a sequence of real numbers (thought of as a function on $\\mathbf{Z}^{+}$), then $\\|x\\|_{1}=\\sum_{k=1}^{\\infty}\\left|x_{k}\\right|$. In this case, $\\mathcal{L}^{1}(\\mu)$ is often denoted by $\\ell^{1}$ (pronounced little-el-one). In other words, $\\ell^{1}$ is the set of all sequences $\\left(x_{1}, x_{2}, \\ldots\\right)$ of real numbers such that $\\sum_{k=1}^{\\infty}\\left|x_{k}\\right|<\\infty$.\n\nThe easy proof of the following result is left to the reader.\n\n### 3.43 properties of the $\\mathcal{L}^{1}$-norm\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f, g \\in \\mathcal{L}^{1}(\\mu)$. Then\n\n- $\\|f\\|_{1} \\geq 0$\n- $\\|f\\|_{1}=0$ if and only if $f(x)=0$ for almost every $x \\in X$;\n- $\\|c f\\|_{1}=|c|\\|f\\|_{1}$ for all $c \\in \\mathbf{R}$\n- $\\|f+g\\|_{1} \\leq\\|f\\|_{1}+\\|g\\|_{1}$.\n\nThe next result states that every function in $\\mathcal{L}^{1}(\\mu)$ can be approximated in $\\mathcal{L}^{1}$ norm by measurable functions that take on only finitely many values.\n\n### 3.44 approximation by simple functions\n\nSuppose $\\mu$ is a measure and $f \\in \\mathcal{L}^{1}(\\mu)$. Then for every $\\varepsilon>0$, there exists a simple function $g \\in \\mathcal{L}^{1}(\\mu)$ such that\n\n$$\n\\|f-g\\|_{1}<\\varepsilon\n$$\n\nProof Suppose $\\varepsilon>0$. Then there exist simple functions $g_{1}, g_{2} \\in \\mathcal{L}^{1}(\\mu)$ such that $0 \\leq g_{1} \\leq f^{+}$and $0 \\leq g_{2} \\leq f^{-}$and\n\n$$\n\\int\\left(f^{+}-g_{1}\\right) d \\mu<\\frac{\\varepsilon}{2} \\quad \\text { and } \\quad \\int\\left(f^{-}-g_{2}\\right) d \\mu<\\frac{\\varepsilon}{2} \\text {, }\n$$\n\nwhere we have used 3.9 to provide the existence of $g_{1}, g_{2}$ with these properties.\n\nLet $g=g_{1}-g_{2}$. Then $g$ is a simple function in $\\mathcal{L}^{1}(\\mu)$ and\n\n$$\n\\begin{aligned}\n\\|f-g\\|_{1} & =\\left\\|\\left(f^{+}-g_{1}\\right)-\\left(f^{-}-g_{2}\\right)\\right\\|_{1} \\\\\n& =\\int\\left(f^{+}-g_{1}\\right) d \\mu+\\int\\left(f^{-}-g_{2}\\right) d \\mu \\\\\n& <\\varepsilon,\n\\end{aligned}\n$$\n\nas desired.\n\nDefinition $\\quad \\mathcal{L}^{1}(\\mathbf{R}) ;\\|f\\|_{1}$\n\n- The notation $\\mathcal{L}^{1}(\\mathbf{R})$ denotes $\\mathcal{L}^{1}(\\lambda)$, where $\\lambda$ is Lebesgue measure on either the Borel subsets of $\\mathbf{R}$ or the Lebesgue measurable subsets of $\\mathbf{R}$.\n- When working with $\\mathcal{L}^{1}(\\mathbf{R})$, the notation $\\|f\\|_{1}$ denotes the integral of the absolute value of $f$ with respect to Lebesgue measure on $\\mathbf{R}$.\n\n### 3.46 Definition step function\n\nA step function is a function $g: \\mathbf{R} \\rightarrow \\mathbf{R}$ of the form\n\n$$\ng=a_{1} \\chi_{I_{1}}+\\cdots+a_{n} \\chi_{I_{n}}\n$$\n\nwhere $I_{1}, \\ldots, I_{n}$ are intervals of $\\mathbf{R}$ and $a_{1}, \\ldots, a_{n}$ are nonzero real numbers.\n\nSuppose $g$ is a step function of the form above and the intervals $I_{1}, \\ldots, I_{n}$ are disjoint. Then\n\n$$\n\\|g\\|_{1}=\\left|a_{1}\\right|\\left|I_{1}\\right|+\\cdots+\\left|a_{n}\\right|\\left|I_{n}\\right| .\n$$\n\nIn particular, $g \\in \\mathcal{L}^{1}(\\mathbf{R})$ if and only if all the intervals $I_{1}, \\ldots, I_{n}$ are bounded.\n\nThe intervals in the definition of a step function can be open intervals, closed intervals, or half-open intervals. We will be using step functions in integrals, where the inclusion or exclusion of the endpoints of the intervals does not matter.\n\nEven though the coefficients $a_{1}, \\ldots, a_{n}$ in the definition of a step function are required to be nonzero, the function 0 that is identically 0 on $\\mathbf{R}$ is a step function. To see this, take $n=1, a_{1}=1$, and $I_{1}=\\varnothing$.\n\n### 3.47 approximation by step functions\n\nSuppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Then for every $\\varepsilon>0$, there exists a step function $g \\in \\mathcal{L}^{1}(\\mathbf{R})$ such that\n\n$$\n\\|f-g\\|_{1}<\\varepsilon\n$$\n\nProof Suppose $\\varepsilon>0$. By 3.44, there exist Borel (or Lebesgue) measurable subsets $A_{1}, \\ldots, A_{n}$ of $\\mathbf{R}$ and nonzero numbers $a_{1}, \\ldots, a_{n}$ such that $\\left|A_{k}\\right|<\\infty$ for all $k \\in$ $\\{1, \\ldots, n\\}$ and\n\n$$\n\\left\\|f-\\sum_{k=1}^{n} a_{k} \\chi_{A_{k}}\\right\\|_{1}<\\frac{\\varepsilon}{2}\n$$\n\nFor each $k \\in\\{1, \\ldots, n\\}$, there is an open subset $G_{k}$ of $\\mathbf{R}$ that contains $A_{k}$ and whose Lebesgue measure is as close as we want to $\\left|A_{k}\\right|$ [by part (e) of 2.71]. Each open subset of $\\mathbf{R}$, including each $G_{k}$, is a countable union of disjoint open intervals. Thus for each $k$, there is a set $E_{k}$ that is a finite union of bounded open intervals contained in $G_{k}$ whose Lebesgue measure is as close as we want to $\\left|G_{k}\\right|$. Hence for each $k$, there is a set $E_{k}$ that is a finite union of bounded intervals such that\n\n$$\n\\begin{aligned}\n\\left|E_{k} \\backslash A_{k}\\right|+\\left|A_{k} \\backslash E_{k}\\right| & \\leq\\left|G_{k} \\backslash A_{k}\\right|+\\left|G_{k} \\backslash E_{k}\\right| \\\\\n& <\\frac{\\varepsilon}{2\\left|a_{k}\\right| n} ;\n\\end{aligned}\n$$\n\nin other words,\n\n$$\n\\left\\|\\chi_{A_{k}}-\\chi_{E_{k}}\\right\\|_{1}<\\frac{\\varepsilon}{2\\left|a_{k}\\right| n}\n$$\n\nNow\n\n$$\n\\begin{aligned}\n\\left\\|f-\\sum_{k=1}^{n} a_{k} \\chi_{E_{k}}\\right\\|_{1} & \\leq\\left\\|f-\\sum_{k=1}^{n} a_{k} \\chi_{A_{k}}\\right\\|_{1}+\\left\\|\\sum_{k=1}^{n} a_{k} \\chi_{A_{k}}-\\sum_{k=1}^{n} a_{k} \\chi_{E_{k}}\\right\\|_{1} \\\\\n& <\\frac{\\varepsilon}{2}+\\sum_{k=1}^{n}\\left|a_{k}\\right|\\left\\|\\chi_{A_{k}}-\\chi_{E_{k}}\\right\\|_{1} \\\\\n& <\\varepsilon .\n\\end{aligned}\n$$\n\nEach $E_{k}$ is a finite union of bounded intervals. Thus the inequality above completes the proof because $\\sum_{k=1}^{n} a_{k} \\chi_{E_{k}}$ is a step function.\n\nLuzin's Theorem (2.91 and 2.93) gives a spectacular way to approximate a Borel measurable function by a continuous function. However, the following approximation theorem is usually more useful than Luzin's Theorem. For example, the next result plays a major role in the proof of the Lebesgue Differentiation Theorem (4.10).\n\n### 3.48 approximation by continuous functions\n\nSuppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Then for every $\\varepsilon>0$, there exists a continuous function $g: \\mathbf{R} \\rightarrow \\mathbf{R}$ such that\n\n$$\n\\|f-g\\|_{1}<\\varepsilon\n$$\n\nand $\\{x \\in \\mathbf{R}: g(x) \\neq 0\\}$ is a bounded set.\n\nProof For every $a_{1}, \\ldots, a_{n}, b_{1}, \\ldots, b_{n}, c_{1}, \\ldots, c_{n} \\in \\mathbf{R}$ and $g_{1}, \\ldots, g_{n} \\in \\mathcal{L}^{1}(\\mathbf{R})$, we have\n\n$$\n\\begin{aligned}\n\\left\\|f-\\sum_{k=1}^{n} a_{k} g_{k}\\right\\|_{1} & \\leq\\left\\|f-\\sum_{k=1}^{n} a_{k} \\chi_{\\left[b_{k}, c_{k}\\right]}\\right\\|_{1}+\\left\\|\\sum_{k=1}^{n} a_{k}\\left(\\chi_{\\left[b_{k}, c_{k}\\right]}-g_{k}\\right)\\right\\|_{1} \\\\\n& \\leq\\left\\|f-\\sum_{k=1}^{n} a_{k} \\chi_{\\left[b_{k}, c_{k}\\right]}\\right\\|_{1}+\\sum_{k=1}^{n}\\left|a_{k}\\right|\\left\\|\\chi_{\\left[b_{k}, c_{k}\\right]}-g_{k}\\right\\|_{1},\n\\end{aligned}\n$$\n\nwhere the inequalities above follow from 3.43. By 3.47, we can choose $a_{1}, \\ldots, a_{n}, b_{1}, \\ldots, b_{n}, c_{1}, \\ldots, c_{n} \\in \\mathbf{R}$ to make $\\left\\|f-\\sum_{k=1}^{n} a_{k} \\chi_{\\left[b_{k}, c_{k}\\right]}\\right\\|_{1}$ as small as we wish. The figure here then shows that there exist continuous functions $g_{1}, \\ldots, g_{n} \\in \\mathcal{L}^{1}(\\mathbf{R})$ that make $\\sum_{k=1}^{n}\\left|a_{k}\\right|\\left\\|\\chi_{\\left[b_{k}, c_{k}\\right]}-g_{k}\\right\\|_{1}$ as small as we wish. Now take $g=\\sum_{k=1}^{n} a_{k} g_{k}$.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-113.jpg?height=310&width=539&top_left_y=1724&top_left_x=688)\n\nThe graph of a continuous function $g_{k}$ such that $\\left\\|\\chi_{\\left[b_{k}, c_{k}\\right]}-g_{k}\\right\\|_{1}$ is small.\n\n## EXERCISES 3B\n\n1 Give an example of a sequence $f_{1}, f_{2}, \\ldots$ of functions from $\\mathbf{Z}^{+}$to $[0, \\infty)$ such that\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}(m)=0\n$$\n\nfor every $m \\in \\mathbf{Z}^{+}$but $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\mu=1$, where $\\mu$ is counting measure on $\\mathbf{Z}^{+}$.\n\n2 Give an example of a sequence $f_{1}, f_{2}, \\ldots$ of continuous functions from $\\mathbf{R}$ to $[0,1]$ such that\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}(x)=0\n$$\n\nfor every $x \\in \\mathbf{R}$ but $\\lim _{k \\rightarrow \\infty} \\int f_{k} d \\lambda=\\infty$, where $\\lambda$ is Lebesgue measure on $\\mathbf{R}$.\n\n3 Suppose $\\lambda$ is Lebesgue measure on $\\mathbf{R}$ and $f: \\mathbf{R} \\rightarrow \\mathbf{R}$ is a Borel measurable function such that $\\int|f| d \\lambda<\\infty$. Define $g: \\mathbf{R} \\rightarrow \\mathbf{R}$ by\n\n$$\ng(x)=\\int_{(-\\infty, x)} f d \\lambda\n$$\n\nProve that $g$ is uniformly continuous on $\\mathbf{R}$.\n\n4 (a) Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space with $\\mu(X)<\\infty$. Suppose that $f: X \\rightarrow[0, \\infty)$ is a bounded $\\mathcal{S}$-measurable function. Prove that\n\n$$\n\\int f d \\mu=\\inf \\left\\{\\sum_{j=1}^{m} \\mu\\left(A_{j}\\right) \\sup _{A_{j}} f: A_{1}, \\ldots, A_{m} \\text { is an } \\mathcal{S} \\text {-partition of } X\\right\\}\n$$\n\n(b) Show that the conclusion of part (a) can fail if the hypothesis that $f$ is bounded is replaced by the hypothesis that $\\int f d \\mu<\\infty$.\n\n(c) Show that the conclusion of part (a) can fail if the condition that $\\mu(X)<\\infty$ is deleted.\n\n[Part (a) of this exercise shows that if we had defined an upper Lebesgue sum, then we could have used it to define the integral. However, parts (b) and (c) show that the hypotheses that $f$ is bounded and that $\\mu(X)<\\infty$ would be needed if defining the integral via the equation above. The definition of the integral via the lower Lebesgue sum does not require these hypotheses, showing the advantage of using the approach via the lower Lebesgue sum.]\n\n5 Let $\\lambda$ denote Lebesgue measure on $\\mathbf{R}$. Suppose $f: \\mathbf{R} \\rightarrow \\mathbf{R}$ is a Borel measurable function such that $\\int|f| d \\lambda<\\infty$. Prove that\n\n$$\n\\lim _{k \\rightarrow \\infty} \\int_{[-k, k]} f d \\lambda=\\int f d \\lambda .\n$$\n\n6 Let $\\lambda$ denote Lebesgue measure on $\\mathbf{R}$. Give an example of a continuous function $f:[0, \\infty) \\rightarrow \\mathbf{R}$ such that $\\lim _{t \\rightarrow \\infty} \\int_{[0, t]} f d \\lambda$ exists (in $\\mathbf{R}$ ) but $\\int_{[0, \\infty)} f d \\lambda$ is not defined.\n\n7 Let $\\lambda$ denote Lebesgue measure on $\\mathbf{R}$. Give an example of a continuous function $f:(0,1) \\rightarrow \\mathbf{R}$ such that $\\lim _{n \\rightarrow \\infty} \\int_{\\left(\\frac{1}{n}, 1\\right)} f d \\lambda$ exists (in $\\mathbf{R}$ ) but $\\int_{(0,1)} f d \\lambda$ is not defined.\n\n8 Verify the assertion in 3.38.\n\n9 Verify the assertion in Example 3.41.\n\n10 (a) Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space such that $\\mu(X)<\\infty$. Suppose $p, r$ are positive numbers with $p<r$. Prove that if $f: X \\rightarrow[0, \\infty)$ is an $\\mathcal{S}$-measurable function such that $\\int f^{r} d \\mu<\\infty$, then $\\int f^{p} d \\mu<\\infty$.\n\n(b) Give an example to show that the result in part (a) can be false without the hypothesis that $\\mu(X)<\\infty$.\n\n11 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f \\in \\mathcal{L}^{1}(\\mu)$. Prove that\n\n$$\n\\{x \\in X: f(x) \\neq 0\\}\n$$\n\nis the countable union of sets with finite $\\mu$-measure.\n\n12 Suppose\n\n$$\nf_{k}(x)=\\frac{(1-x)^{k} \\cos \\frac{k}{x}}{\\sqrt{x}}\n$$\n\nProve that $\\lim _{k \\rightarrow \\infty} \\int_{0}^{1} f_{k}=0$.\n\n13 Give an example of a sequence of nonnegative Borel measurable functions $f_{1}, f_{2}, \\ldots$ on $[0,1]$ such that both the following conditions hold:\n\n- $\\lim _{k \\rightarrow \\infty} \\int_{0}^{1} f_{k}=0$;\n- $\\sup f_{k}(x)=\\infty$ for every $m \\in \\mathbf{Z}^{+}$and every $x \\in[0,1]$. $k \\geq m$\n\n14 Let $\\lambda$ denote Lebesgue measure on $\\mathbf{R}$.\n\n(a) Let $f(x)=1 / \\sqrt{x}$. Prove that $\\int_{[0,1]} f d \\lambda=2$.\n\n(b) Let $f(x)=1 /\\left(1+x^{2}\\right)$. Prove that $\\int_{\\mathbf{R}} f d \\lambda=\\pi$.\n\n(c) Let $f(x)=(\\sin x) / x$. Show that the integral $\\int_{(0, \\infty)} f d \\lambda$ is not defined but $\\lim _{t \\rightarrow \\infty} \\int_{(0, t)} f d \\lambda$ exists in $\\mathbf{R}$.\n\n15 Prove or give a counterexample: If $G$ is an open subset of $(0,1)$, then $\\chi_{G}$ is Riemann integrable on $[0,1]$.\n\n16 Suppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$.\n\n(a) For $t \\in \\mathbf{R}$, define $f_{t}: \\mathbf{R} \\rightarrow \\mathbf{R}$ by $f_{t}(x)=f(x-t)$. Prove that $\\lim _{t \\rightarrow 0}\\left\\|f-f_{t}\\right\\|_{1}=0$.\n\n(b) For $t>0$, define $f_{t}: \\mathbf{R} \\rightarrow \\mathbf{R}$ by $f_{t}(x)=f(t x)$. Prove that $\\lim _{t \\rightarrow 1}\\left\\|f-f_{t}\\right\\|_{1}=0$.\n\n## Chapter 4\n\n## Differentiation\n\nDoes there exist a Lebesgue measurable set that fills up exactly half of each interval? To get a feeling for this question, consider the set $E=\\left[0, \\frac{1}{8}\\right] \\cup\\left[\\frac{1}{4}, \\frac{3}{8}\\right] \\cup\\left[\\frac{1}{2}, \\frac{5}{8}\\right] \\cup\\left[\\frac{3}{4}, \\frac{7}{8}\\right]$. This set $E$ has the property that\n\n$$\n|E \\cap[0, b]|=\\frac{b}{2}\n$$\n\nfor $b=0, \\frac{1}{4}, \\frac{1}{2}, \\frac{3}{4}, 1$. Does there exist a Lebesgue measurable set $E \\subset[0,1]$, perhaps constructed in a fashion similar to the Cantor set, such that the equation above holds for all $b \\in[0,1]$ ?\n\nIn this chapter we see how to answer this question by considering differentiation issues. We begin by developing a powerful tool called the Hardy-Littlewood maximal inequality. This tool is used to prove an almost everywhere version of the Fundamental Theorem of Calculus. These results lead us to an important theorem about the density of Lebesgue measurable sets.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-116.jpg?height=561&width=1180&top_left_y=1124&top_left_x=61)\n\nTrinity College at the University of Cambridge in England. G. H. Hardy (1877-1947) and John Littlewood (1885-1977) were students and later faculty members here. If you have not already done so, you should read Hardy's remarkable book A Mathematician's Apology (do not skip the fascinating Foreword by C. $P$. Snow) and see the movie The Man Who Knew Infinity, which focuses on Hardy, Littlewood, and Srinivasa Ramanujan (1887-1920).\n\nCC-BY-SA Rafa Esteve\n\n## 4A Hardy-Littlewood Maximal Function\n\n## Markov's Inequality\n\nThe following result, called Markov's inequality, has a sweet, short proof. We will make good use of this result later in this chapter (see the proof of 4.10). Markov's inequality also leads to Chebyshev's inequality (see Exercise 2 in this section).\n\n### 4.1 Markov's inequality\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $h \\in \\mathcal{L}^{1}(\\mu)$. Then\n\n$$\n\\mu(\\{x \\in X:|h(x)| \\geq c\\}) \\leq \\frac{1}{c}\\|h\\|_{1}\n$$\n\nfor every $c>0$.\n\nProof Suppose $c>0$. Then\n\n$$\n\\begin{aligned}\n\\mu(\\{x \\in X:|h(x)| \\geq c\\}) & =\\frac{1}{c} \\int_{\\{x \\in X:|h(x)| \\geq c\\}} c d \\mu \\\\\n& \\leq \\frac{1}{c} \\int_{\\{x \\in X:|h(x)| \\geq c\\}}|h| d \\mu \\\\\n& \\leq \\frac{1}{c}\\|h\\|_{1},\n\\end{aligned}\n$$\n\nas desired.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-117.jpg?height=673&width=1158&top_left_y=1281&top_left_x=68)\n\nSt. Petersburg University along the Neva River in St. Petersburg, Russia. Andrei Markov (1856-1922) was a student and then a faculty member here. CC-BY-SA A. Savin\n\n## Vitali Covering Lemma\n\n### 4.2 Definition 3 times a bounded nonempty open interval\n\nSuppose $I$ is a bounded nonempty open interval of $\\mathbf{R}$. Then $3 * I$ denotes the open interval with the same center as $I$ and three times the length of $I$.\n\n### 4.3 Example 3 times an interval\n\nIf $I=(0,10)$, then $3 * I=(-10,20)$.\n\nThe next result is a key tool in the proof of the Hardy-Littlewood maximal inequality (4.8).\n\n### 4.4 Vitali Covering Lemma\n\nSuppose $I_{1}, \\ldots, I_{n}$ is a list of bounded nonempty open intervals of $\\mathbf{R}$. Then there exists a disjoint sublist $I_{k_{1}}, \\ldots, I_{k_{m}}$ such that\n\n$$\nI_{1} \\cup \\cdots \\cup I_{n} \\subset\\left(3 * I_{k_{1}}\\right) \\cup \\cdots \\cup\\left(3 * I_{k_{m}}\\right) .\n$$\n\n### 4.5 Example Vitali Covering Lemma\n\nSuppose $n=4$ and\n\n$$\nI_{1}=(0,10), \\quad I_{2}=(9,15), \\quad I_{3}=(14,22), \\quad I_{4}=(21,31) .\n$$\n\nThen\n\n$$\n3 * I_{1}=(-10,20), \\quad 3 * I_{2}=(3,21), \\quad 3 * I_{3}=(6,30), \\quad 3 * I_{4}=(11,41) .\n$$\n\nThus\n\n$$\nI_{1} \\cup I_{2} \\cup I_{3} \\cup I_{4} \\subset\\left(3 * I_{1}\\right) \\cup\\left(3 * I_{4}\\right)\n$$\n\nIn this example, $I_{1}, I_{4}$ is the only sublist of $I_{1}, I_{2}, I_{3}, I_{4}$ that produces the conclusion of the Vitali Covering Lemma.\n\nProof of 4.4 Let $k_{1}$ be such that\n\n$$\n\\left|I_{k_{1}}\\right|=\\max \\left\\{\\left|I_{1}\\right|, \\ldots,\\left|I_{n}\\right|\\right\\}\n$$\n\nSuppose $k_{1}, \\ldots, k_{j}$ have been chosen. Let $k_{j+1}$ be such that $\\left|I_{k_{j+1}}\\right|$ is as large as possible subject to the condition that $I_{k_{1}}, \\ldots, I_{k_{j+1}}$ are disjoint. If there is no choice of $k_{j+1}$ such that $I_{k_{1}}, \\ldots, I_{k_{j+1}}$ are disjoint, then the procedure terminates.\n\nThe technique used here is called a greedy algorithm because at each stage we select the largest remaining interval that is disjoint from the previously selected intervals.\n\nBecause we start with a finite list, the procedure must eventually terminate after some number $m$ of choices.\n\nSuppose $j \\in\\{1, \\ldots, n\\}$. To complete the proof, we must show that\n\n$$\nI_{j} \\subset\\left(3 * I_{k_{1}}\\right) \\cup \\cdots \\cup\\left(3 * I_{k_{m}}\\right)\n$$\n\nIf $j \\in\\left\\{k_{1}, \\ldots, k_{m}\\right\\}$, then the inclusion above obviously holds.\n\nThus assume that $j \\notin\\left\\{k_{1}, \\ldots, k_{m}\\right\\}$. Because the process terminated without selecting $j$, the interval $I_{j}$ is not disjoint from all of $I_{k_{1}}, \\ldots, I_{k_{m}}$. Let $I_{k_{L}}$ be the first interval on this list not disjoint from $I_{j}$; thus $I_{j}$ is disjoint from $I_{k_{1}}, \\ldots, I_{k_{L-1}}$. Because $j$ was not chosen in step $L$, we conclude that $\\left|I_{k_{L}}\\right| \\geq\\left|I_{j}\\right|$. Because $I_{k_{L}} \\cap I_{j} \\neq \\varnothing$, this last inequality implies (easy exercise) that $I_{j} \\subset 3 * I_{k_{L}}$, completing the proof.\n\n## Hardy-Littlewood Maximal Inequality\n\nNow we come to a brilliant definition that turns out to be extraordinarily useful.\n\n### 4.6 Definition Hardy-Littlewood maximal function; $h^{*}$\n\nSuppose $h: \\mathbf{R} \\rightarrow \\mathbf{R}$ is a Lebesgue measurable function. Then the HardyLittlewood maximal function of $h$ is the function $h^{*}: \\mathbf{R} \\rightarrow[0, \\infty]$ defined by\n\n$$\nh^{*}(b)=\\sup _{t>0} \\frac{1}{2 t} \\int_{b-t}^{b+t}|h|\n$$\n\nIn other words, $h^{*}(b)$ is the supremum over all bounded intervals centered at $b$ of the average of $|h|$ on those intervals.\n\n### 4.7 Example Hardy-Littlewood maximal function of $\\chi_{[0,1]}$\n\nAs usual, let $\\chi_{[0,1]}$ denote the characteristic function of the interval $[0,1]$. Then\n\n$$\n\\left(\\chi_{[0,1]}\\right)^{*}(b)= \\begin{cases}\\frac{1}{2(1-b)} & \\text { if } b \\leq 0 \\\\ 1 & \\text { if } 0<b<1 \\\\ \\frac{1}{2 b} & \\text { if } b \\geq 1\\end{cases}\n$$\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-119.jpg?height=235&width=507&top_left_y=1399&top_left_x=700)\n\nas you should verify.\n\nIf $h: \\mathbf{R} \\rightarrow \\mathbf{R}$ is Lebesgue measurable and $c \\in \\mathbf{R}$, then $\\left\\{b \\in \\mathbf{R}: h^{*}(b)>c\\right\\}$ is an open subset of $\\mathbf{R}$, as you are asked to prove in Exercise 9 in this section. Thus $h^{*}$ is a Borel measurable function.\n\nSuppose $h \\in \\mathcal{L}^{1}(\\mathbf{R})$ and $c>0$. Markov's inequality (4.1) estimates the size of the set on which $|h|$ is larger than $c$. Our next result estimates the size of the set on which $h^{*}$ is larger than $c$. The Hardy-Littlewood maximal inequality proved in the next result is a key ingredient in the proof of the Lebesgue Differentiation Theorem (4.10). Note that this next result is considerably deeper than Markov's inequality.\n\n### 4.8 Hardy-Littlewood maximal inequality\n\nSuppose $h \\in \\mathcal{L}^{1}(\\mathbf{R})$. Then\n\n$$\n\\left|\\left\\{b \\in \\mathbf{R}: h^{*}(b)>c\\right\\}\\right| \\leq \\frac{3}{c}\\|h\\|_{1}\n$$\n\nfor every $c>0$.\n\nProof Suppose $F$ is a closed bounded subset of $\\left\\{b \\in \\mathbf{R}: h^{*}(b)>c\\right\\}$. We will show that $|F| \\leq \\frac{3}{c} \\int_{-\\infty}^{\\infty}|h|$, which implies our desired result [see Exercise 24(a) in Section 2D].\n\nFor each $b \\in F$, there exists $t_{b}>0$ such that\n\n$$\n\\frac{1}{2 t_{b}} \\int_{b-t_{b}}^{b+t_{b}}|h|>c\n$$\n\nClearly\n\n$$\nF \\subset \\bigcup_{b \\in F}\\left(b-t_{b}, b+t_{b}\\right) .\n$$\n\nThe Heine-Borel Theorem (2.12) tells us that this open cover of a closed bounded set has a finite subcover. In other words, there exist $b_{1}, \\ldots, b_{n} \\in F$ such that\n\n$$\nF \\subset\\left(b_{1}-t_{b_{1}}, b_{1}+t_{b_{1}}\\right) \\cup \\cdots \\cup\\left(b_{n}-t_{b_{n}}, b_{n}+t_{b_{n}}\\right) .\n$$\n\nTo make the notation cleaner, relabel the open intervals above as $I_{1}, \\ldots, I_{n}$.\n\nNow apply the Vitali Covering Lemma (4.4) to the list $I_{1}, \\ldots, I_{n}$, producing a disjoint sublist $I_{k_{1}}, \\ldots, I_{k_{m}}$ such that\n\n$$\nI_{1} \\cup \\cdots \\cup I_{n} \\subset\\left(3 * I_{k_{1}}\\right) \\cup \\cdots \\cup\\left(3 * I_{k_{m}}\\right) .\n$$\n\nThus\n\n$$\n\\begin{aligned}\n|F| & \\leq\\left|I_{1} \\cup \\cdots \\cup I_{n}\\right| \\\\\n& \\leq\\left|\\left(3 * I_{k_{1}}\\right) \\cup \\cdots \\cup\\left(3 * I_{k_{m}}\\right)\\right| \\\\\n& \\leq\\left|3 * I_{k_{1}}\\right|+\\cdots+\\left|3 * I_{k_{m}}\\right| \\\\\n& =3\\left(\\left|I_{k_{1}}\\right|+\\cdots+\\left|I_{k_{m}}\\right|\\right) \\\\\n& <\\frac{3}{c}\\left(\\int_{I_{k_{1}}}|h|+\\cdots+\\int_{I_{k_{m}}}|h|\\right) \\\\\n& \\leq \\frac{3}{c} \\int_{-\\infty}^{\\infty}|h|,\n\\end{aligned}\n$$\n\nwhere the second-to-last inequality above comes from 4.9 (note that $\\left|I_{k_{j}}\\right|=2 t_{b}$ for the choice of $b$ corresponding to $I_{k_{j}}$ ) and the last inequality holds because $I_{k_{1}}, \\ldots, I_{k_{m}}$ are disjoint.\n\nThe last inequality completes the proof.\n\n## EXERCISES 4A\n\n1 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $h: X \\rightarrow \\mathbf{R}$ is an $\\mathcal{S}$-measurable function. Prove that\n\n$$\n\\mu(\\{x \\in X:|h(x)| \\geq c\\}) \\leq \\frac{1}{c^{p}} \\int|h|^{p} d \\mu\n$$\n\nfor all positive numbers $c$ and $p$.\n\n2 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space with $\\mu(X)=1$ and $h \\in \\mathcal{L}^{1}(\\mu)$. Prove that\n\n$$\n\\mu\\left(\\left\\{x \\in X:\\left|h(x)-\\int h d \\mu\\right| \\geq c\\right\\}\\right) \\leq \\frac{1}{c^{2}}\\left(\\int h^{2} d \\mu-\\left(\\int h d \\mu\\right)^{2}\\right)\n$$\n\nfor all $c>0$.\n\n[The result above is called Chebyshev's inequality; it plays an important role in probability theory. Pafnuty Chebyshev (1821-1894) was Markov's thesis advisor.]\n\n3 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space. Suppose $h \\in \\mathcal{L}^{1}(\\mu)$ and $\\|h\\|_{1}>0$. Prove that there is at most one number $c \\in(0, \\infty)$ such that\n\n$$\n\\mu(\\{x \\in X:|h(x)| \\geq c\\})=\\frac{1}{c}\\|h\\|_{1} .\n$$\n\n4 Show that the constant 3 in the Vitali Covering Lemma (4.4) cannot be replaced by a smaller positive constant.\n\n5 Prove the assertion left as an exercise in the last sentence of the proof of the Vitali Covering Lemma (4.4).\n\n6 Verify the formula in Example 4.7 for the Hardy-Littlewood maximal function of $\\chi_{[0,1]}$.\n\n7 Find a formula for the Hardy-Littlewood maximal function of the characteristic function of $[0,1] \\cup[2,3]$.\n\n8 Find a formula for the Hardy-Littlewood maximal function of the function $h: \\mathbf{R} \\rightarrow[0, \\infty)$ defined by\n\n$$\nh(x)= \\begin{cases}x & \\text { if } 0 \\leq x \\leq 1 \\\\ 0 & \\text { otherwise }\\end{cases}\n$$\n\n9 Suppose $h: \\mathbf{R} \\rightarrow \\mathbf{R}$ is Lebesgue measurable. Prove that\n\n$$\n\\left\\{b \\in \\mathbf{R}: h^{*}(b)>c\\right\\}\n$$\n\nis an open subset of $\\mathbf{R}$ for every $c \\in \\mathbf{R}$.\n\n10 Prove or give a counterexample: If $h: \\mathbf{R} \\rightarrow[0, \\infty)$ is an increasing function, then $h^{*}$ is an increasing function.\n\n11 Give an example of a Borel measurable function $h: \\mathbf{R} \\rightarrow[0, \\infty)$ such that $h^{*}(b)<\\infty$ for all $b \\in \\mathbf{R}$ but $\\sup \\left\\{h^{*}(b): b \\in \\mathbf{R}\\right\\}=\\infty$.\n\n12 Show that $\\left|\\left\\{b \\in \\mathbf{R}: h^{*}(b)=\\infty\\right\\}\\right|=0$ for every $h \\in \\mathcal{L}^{1}(\\mathbf{R})$.\n\n13 Show that there exists $h \\in \\mathcal{L}^{1}(\\mathbf{R})$ such that $h^{*}(b)=\\infty$ for every $b \\in \\mathbf{Q}$.\n\n14 Suppose $h \\in \\mathcal{L}^{1}(\\mathbf{R})$. Prove that\n\n$$\n\\left|\\left\\{b \\in \\mathbf{R}: h^{*}(b) \\geq c\\right\\}\\right| \\leq \\frac{3}{c}\\|h\\|_{1}\n$$\n\nfor every $c>0$.\n\n[This result slightly strengthens the Hardy-Littlewood maximal inequality (4.8) because the set on the left side above includes those $b \\in \\mathbf{R}$ such that $h^{*}(b)=c$. A much deeper strengthening comes from replacing the constant 3 in the HardyLittlewood maximal inequality with a smaller constant. In 2003, Antonios Melas answered what had been an open question about the best constant. He proved that the smallest constant that can replace 3 in the Hardy-Littlewood maximal inequality is $(11+\\sqrt{61}) / 12 \\approx 1.56752$; see Annals of Mathematics 157 (2003), 647-688.]\n\n## 4B Derivatives of Integrals\n\n## Lebesgue Differentiation Theorem\n\nThe next result states that the average amount by which a function in $\\mathcal{L}^{1}(\\mathbf{R})$ differs from its values is small almost everywhere on small intervals. The 2 in the denominator of the fraction in the result below could be deleted, but its presence makes the length of the interval of integration nicely match the denominator $2 t$.\n\nThe next result is called the Lebesgue Differentiation Theorem, even though no derivative is in sight. However, we will soon see how another version of this result deals with derivatives. The hard work takes place in the proof of this first version.\n\n### 4.10 Lebesgue Differentiation Theorem, first version\n\nSuppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Then\n\n$$\n\\lim _{t \\downarrow 0} \\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)|=0\n$$\n\nfor almost every $b \\in \\mathbf{R}$.\n\nBefore getting to the formal proof of this first version of the Lebesgue Differentiation Theorem, we pause to provide some motivation for the proof. If $b \\in \\mathbf{R}$ and $t>0$, then 3.25 gives the easy estimate\n\n$$\n\\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)| \\leq \\sup \\{|f(x)-f(b)|:|x-b| \\leq t\\}\n$$\n\nIf $f$ is continuous at $b$, then the right side of this inequality has limit 0 as $t \\downarrow 0$, proving 4.10 in the special case in which $f$ is continuous on $\\mathbf{R}$.\n\nTo prove the Lebesgue Differentiation Theorem, we will approximate an arbitrary function in $\\mathcal{L}^{1}(\\mathbf{R})$ by a continuous function (using 3.48). The previous paragraph shows that the continuous function has the desired behavior. We will use the HardyLittlewood maximal inequality (4.8) to show that the approximation produces approximately the desired behavior. Now we are ready for the formal details of the proof.\n\nProof of 4.10 Let $\\delta>0$. By 3.48, for each $k \\in \\mathbf{Z}^{+}$there exists a continuous function $h_{k}: \\mathbf{R} \\rightarrow \\mathbf{R}$ such that\n\n4.11\n\n$$\n\\left\\|f-h_{k}\\right\\|_{1}<\\frac{\\delta}{k 2^{k}} .\n$$\n\nLet\n\n$$\nB_{k}=\\left\\{b \\in \\mathbf{R}:\\left|f(b)-h_{k}(b)\\right| \\leq \\frac{1}{k} \\text { and }\\left(f-h_{k}\\right)^{*}(b) \\leq \\frac{1}{k}\\right\\} \\text {. }\n$$\n\nThen\n\n$4.12 \\mathbf{R} \\backslash B_{k}=\\left\\{b \\in \\mathbf{R}:\\left|f(b)-h_{k}(b)\\right|>\\frac{1}{k}\\right\\} \\cup\\left\\{b \\in \\mathbf{R}:\\left(f-h_{k}\\right)^{*}(b)>\\frac{1}{k}\\right\\}$.\n\nMarkov's inequality (4.1) as applied to the function $f-h_{k}$ and 4.11 imply that\n\n4.13\n\n$$\n\\left|\\left\\{b \\in \\mathbf{R}:\\left|f(b)-h_{k}(b)\\right|>\\frac{1}{k}\\right\\}\\right|<\\frac{\\delta}{2^{k}}\n$$\n\nThe Hardy-Littlewood maximal inequality (4.8) as applied to the function $f-h_{k}$ and 4.11 imply that\n\n4.14\n\n$$\n\\left|\\left\\{b \\in \\mathbf{R}:\\left(f-h_{k}\\right)^{*}(b)>\\frac{1}{k}\\right\\}\\right|<\\frac{3 \\delta}{2^{k}} .\n$$\n\nNow 4.12, 4.13, and 4.14 imply that\n\n$$\n\\left|\\mathbf{R} \\backslash B_{k}\\right|<\\frac{\\delta}{2^{k-2}}\n$$\n\nLet\n\n$$\nB=\\bigcap_{k=1}^{\\infty} B_{k}\n$$\n\nThen\n\n4.15\n\n$$\n|\\mathbf{R} \\backslash B|=\\left|\\bigcup_{k=1}^{\\infty}\\left(\\mathbf{R} \\backslash B_{k}\\right)\\right| \\leq \\sum_{k=1}^{\\infty}\\left|\\mathbf{R} \\backslash B_{k}\\right|<\\sum_{k=1}^{\\infty} \\frac{\\delta}{2^{k-2}}=4 \\delta\n$$\n\nSuppose $b \\in B$ and $t>0$. Then for each $k \\in \\mathbf{Z}^{+}$we have\n\n$$\n\\begin{aligned}\n\\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)| & \\leq \\frac{1}{2 t} \\int_{b-t}^{b+t}\\left(\\left|f-h_{k}\\right|+\\left|h_{k}-h_{k}(b)\\right|+\\left|h_{k}(b)-f(b)\\right|\\right) \\\\\n& \\leq\\left(f-h_{k}\\right)^{*}(b)+\\left(\\frac{1}{2 t} \\int_{b-t}^{b+t}\\left|h_{k}-h_{k}(b)\\right|\\right)+\\left|h_{k}(b)-f(b)\\right| \\\\\n& \\leq \\frac{2}{k}+\\frac{1}{2 t} \\int_{b-t}^{b+t}\\left|h_{k}-h_{k}(b)\\right| .\n\\end{aligned}\n$$\n\nBecause $h_{k}$ is continuous, the last term is less than $\\frac{1}{k}$ for all $t>0$ sufficiently close to 0 (how close is sufficiently close depends upon $k$ ). In other words, for each $k \\in \\mathbf{Z}^{+}$, we have\n\n$$\n\\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)|<\\frac{3}{k}\n$$\n\nfor all $t>0$ sufficiently close to 0 .\n\nHence we conclude that\n\n$$\n\\lim _{t \\downarrow 0} \\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)|=0\n$$\n\nfor all $b \\in B$.\n\nLet $A$ denote the set of numbers $a \\in \\mathbf{R}$ such that\n\n$$\n\\lim _{t \\downarrow 0} \\frac{1}{2 t} \\int_{a-t}^{a+t}|f-f(a)|\n$$\n\neither does not exist or is nonzero. We have shown that $A \\subset(\\mathbf{R} \\backslash B)$. Thus\n\n$$\n|A| \\leq|\\mathbf{R} \\backslash B|<4 \\delta\n$$\n\nwhere the last inequality comes from 4.15 . Because $\\delta$ is an arbitrary positive number, the last inequality implies that $|A|=0$, completing the proof.\n\n## Derivatives\n\nYou should remember the following definition from your calculus course.\n\n### 4.16 Definition derivative; $g^{\\prime}$; differentiable\n\nSuppose $g: I \\rightarrow \\mathbf{R}$ is a function defined on an open interval $I$ of $\\mathbf{R}$ and $b \\in I$. The derivative of $g$ at $b$, denoted $g^{\\prime}(b)$, is defined by\n\n$$\ng^{\\prime}(b)=\\lim _{t \\rightarrow 0} \\frac{g(b+t)-g(b)}{t}\n$$\n\nif the limit above exists, in which case $g$ is called differentiable at $b$.\n\nWe now turn to the Fundamental Theorem of Calculus and a powerful extension that avoids continuity. These results show that differentiation and integration can be thought of as inverse operations.\n\nYou saw the next result in your calculus class, except now the function $f$ is only required to be Lebesgue measurable (and its absolute value must have a finite Lebesgue integral). Of course, we also need to require $f$ to be continuous at the crucial point $b$ in the next result, because changing the value of $f$ at a single number would not change the function $g$.\n\n### 4.17 Fundamental Theorem of Calculus\n\nSuppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Define $g: \\mathbf{R} \\rightarrow \\mathbf{R}$ by\n\n$$\ng(x)=\\int_{-\\infty}^{x} f\n$$\n\nSuppose $b \\in \\mathbf{R}$ and $f$ is continuous at $b$. Then $g$ is differentiable at $b$ and\n\n$$\ng^{\\prime}(b)=f(b)\n$$\n\nProof If $t \\neq 0$, then\n\n$$\n\\begin{aligned}\n\\left|\\frac{g(b+t)-g(b)}{t}-f(b)\\right| & =\\left|\\frac{\\int_{-\\infty}^{b+t} f-\\int_{-\\infty}^{b} f}{t}-f(b)\\right| \\\\\n& =\\left|\\frac{\\int_{b}^{b+t} f}{t}-f(b)\\right| \\\\\n& =\\left|\\frac{\\int_{b}^{b+t}(f-f(b))}{t}\\right| \\\\\n& \\leq \\sup _{\\{x \\in \\mathbf{R}:|x-b|<|t|\\}}|f(x)-f(b)| .\n\\end{aligned}\n$$\n\nIf $\\varepsilon>0$, then by the continuity of $f$ at $b$, the last quantity is less than $\\varepsilon$ for $t$ sufficiently close to 0 . Thus $g$ is differentiable at $b$ and $g^{\\prime}(b)=f(b)$.\n\nA function in $\\mathcal{L}^{1}(\\mathbf{R})$ need not be continuous anywhere. Thus the Fundamental Theorem of Calculus (4.17) might provide no information about differentiating the integral of such a function. However, our next result states that all is well almost everywhere, even in the absence of any continuity of the function being integrated.\n\n### 4.19 Lebesgue Differentiation Theorem, second version\n\nSuppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Define $g: \\mathbf{R} \\rightarrow \\mathbf{R}$ by\n\n$$\ng(x)=\\int_{-\\infty}^{x} f\n$$\n\nThen $g^{\\prime}(b)=f(b)$ for almost every $b \\in \\mathbf{R}$.\n\nProof Suppose $t \\neq 0$. Then from 4.18 we have\n\n$$\n\\begin{aligned}\n\\left|\\frac{g(b+t)-g(b)}{t}-f(b)\\right| & =\\left|\\frac{\\int_{b}^{b+t}(f-f(b))}{t}\\right| \\\\\n& \\leq \\frac{1}{t} \\int_{b}^{b+t}|f-f(b)| \\\\\n& \\leq \\frac{1}{t} \\int_{b-t}^{b+t}|f-f(b)|\n\\end{aligned}\n$$\n\nfor all $b \\in \\mathbf{R}$. By the first version of the Lebesgue Differentiation Theorem (4.10), the last quantity has limit 0 as $t \\rightarrow 0$ for almost every $b \\in \\mathbf{R}$. Thus $g^{\\prime}(b)=f(b)$ for almost every $b \\in \\mathbf{R}$.\n\nNow we can answer the question raised on the opening page of this chapter.\n\n### 4.20 no set constitutes exactly half of each interval\n\nThere does not exist a Lebesgue measurable set $E \\subset[0,1]$ such that\n\n$$\n|E \\cap[0, b]|=\\frac{b}{2}\n$$\n\nfor all $b \\in[0,1]$\n\nProof Suppose there does exist a Lebesgue measurable set $E \\subset[0,1]$ with the property above. Define $g: \\mathbf{R} \\rightarrow \\mathbf{R}$ by\n\n$$\ng(b)=\\int_{-\\infty}^{b} \\chi_{E} .\n$$\n\nThus $g(b)=\\frac{b}{2}$ for all $b \\in[0,1]$. Hence $g^{\\prime}(b)=\\frac{1}{2}$ for all $b \\in(0,1)$.\n\nThe Lebesgue Differentiation Theorem (4.19) implies that $g^{\\prime}(b)=\\chi_{E}(b)$ for almost every $b \\in \\mathbf{R}$. However, $\\chi_{E}$ never takes on the value $\\frac{1}{2}$, which contradicts the conclusion of the previous paragraph. This contradiction completes the proof.\n\nThe next result says that a function in $\\mathcal{L}^{1}(\\mathbf{R})$ is equal almost everywhere to the limit of its average over small intervals. These two-sided results generalize more naturally to higher dimensions (take the average over balls centered at $b$ ) than the one-sided results.\n\n## $4.21 \\mathcal{L}^{1}(\\mathbf{R})$ function equals its local average almost everywhere\n\nSuppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Then\n\n$$\nf(b)=\\lim _{t \\downarrow 0} \\frac{1}{2 t} \\int_{b-t}^{b+t} f\n$$\n\nfor almost every $b \\in \\mathbf{R}$.\n\nProof Suppose $t>0$. Then\n\n$$\n\\begin{aligned}\n\\left|\\left(\\frac{1}{2 t} \\int_{b-t}^{b+t} f\\right)-f(b)\\right| & =\\left|\\frac{1}{2 t} \\int_{b-t}^{b+t}(f-f(b))\\right| \\\\\n& \\leq \\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)| .\n\\end{aligned}\n$$\n\nThe desired result now follows from the first version of the Lebesgue Differentiation Theorem (4.10).\n\nAgain, the conclusion of the result above holds at every number $b$ at which $f$ is continuous. The remarkable part of the result above is that even if $f$ is discontinuous everywhere, the conclusion holds for almost every real number $b$.\n\n## Density\n\nThe next definition captures the notion of the proportion of a set in small intervals centered at a number $b$.\n\n### 4.22 Definition density\n\nSuppose $E \\subset \\mathbf{R}$. The density of $E$ at a number $b \\in \\mathbf{R}$ is\n\n$$\n\\lim _{t \\downarrow 0} \\frac{|E \\cap(b-t, b+t)|}{2 t}\n$$\n\nif this limit exists (otherwise the density of $E$ at $b$ is undefined).\n\n4.23 Example density of an interval\n\nThe density of $[0,1]$ at $b= \\begin{cases}1 & \\text { if } b \\in(0,1), \\\\ \\frac{1}{2} & \\text { if } b=0 \\text { or } b=1 \\\\ 0 & \\text { otherwise. }\\end{cases}$\n\nThe next beautiful result shows the power of the techniques developed in this chapter.\n\n### 4.24 Lebesgue Density Theorem\n\nSuppose $E \\subset \\mathbf{R}$ is a Lebesgue measurable set. Then the density of $E$ is 1 at almost every element of $E$ and is 0 at almost every element of $\\mathbf{R} \\backslash E$.\n\nProof First suppose $|E|<\\infty$. Thus $\\chi_{E} \\in \\mathcal{L}^{1}(\\mathbf{R})$. Because\n\n$$\n\\frac{|E \\cap(b-t, b+t)|}{2 t}=\\frac{1}{2 t} \\int_{b-t}^{b+t} \\chi_{E}\n$$\n\nfor every $t>0$ and every $b \\in \\mathbf{R}$, the desired result follows immediately from 4.21.\n\nNow consider the case where $|E|=\\infty$ [which means that $\\chi_{E} \\notin \\mathcal{L}^{1}(\\mathbf{R})$ and hence 4.21 as stated cannot be used]. For $k \\in \\mathbf{Z}^{+}$, let $E_{k}=E \\cap(-k, k)$. If $|b|<k$, then the density of $E$ at $b$ equals the density of $E_{k}$ at $b$. By the previous paragraph as applied to $E_{k}$, there are sets $F_{k} \\subset E_{k}$ and $G_{k} \\subset \\mathbf{R} \\backslash E_{k}$ such that $\\left|F_{k}\\right|=\\left|G_{k}\\right|=0$ and the density of $E_{k}$ equals 1 at every element of $E_{k} \\backslash F_{k}$ and the density of $E_{k}$ equals 0 at every element of $\\left(\\mathbf{R} \\backslash E_{k}\\right) \\backslash G_{k}$.\n\nLet $F=\\bigcup_{k=1}^{\\infty} F_{k}$ and $G=\\bigcup_{k=1}^{\\infty} G_{k}$. Then $|F|=|G|=0$ and the density of $E$ is 1 at every element of $E \\backslash F$ and is 0 at every element of $(\\mathbf{R} \\backslash E) \\backslash G$.\n\nThe bad Borel set provided by the next result leads to a bad Borel measurable function. Specifically, let $E$ be the bad Borel set in 4.25. Then $\\chi_{E}$ is a Borel measurable function that is discontinuous everywhere. Furthermore, the function $\\chi_{E}$ cannot be modified on a set of measure 0 to be continuous anywhere (in contrast to the function $\\chi_{\\mathbf{Q}}$ ).\n\nThe Lebesgue Density Theorem makes the example provided by the next result somewhat surprising. Be sure to spend some time pondering why the next result does not contradict the Lebesgue Density Theorem. Also, compare the next result to 4.20.\n\nEven though the function $\\chi_{E}$ discussed in the paragraph above is continuous nowhere and every modification of this function on a set of measure 0 is also continuous nowhere, the function $g$ defined by\n\n$$\ng(b)=\\int_{0}^{b} \\chi_{E}\n$$\n\nis differentiable almost everywhere (by 4.19).\n\nThe proof of 4.25 given below is based on an idea of Walter Rudin.\n\n### 4.25 bad Borel set\n\nThere exists a Borel set $E \\subset \\mathbf{R}$ such that\n\n$$\n0<|E \\cap I|<|I|\n$$\n\nfor every nonempty bounded open interval $I$.\n\nProof We use the following fact in our construction:\n\n4.26 Suppose $G$ is a nonempty open subset of $\\mathbf{R}$. Then there exists a closed set $F \\subset G \\backslash Q$ such that $|F|>0$.\n\nTo prove 4.26, let $J$ be a closed interval contained in $G$ such that $0<|J|$. Let $r_{1}, r_{2}, \\ldots$ be a list of all the rational numbers. Let\n\n$$\nF=J \\backslash \\bigcup_{k=1}^{\\infty}\\left(r_{k}-\\frac{|J|}{2^{k+2}}, r_{k}+\\frac{|J|}{2^{k+2}}\\right)\n$$\n\nThen $F$ is a closed subset of $\\mathbf{R}$ and $F \\subset J \\backslash \\mathbf{Q} \\subset G \\backslash \\mathbf{Q}$. Also, $|J \\backslash F| \\leq \\frac{1}{2}|J|$ because $J \\backslash F \\subset \\bigcup_{k=1}^{\\infty}\\left(r_{k}-\\frac{|J|}{2^{k+2}}, r_{k}+\\frac{|J|}{2^{k+2}}\\right)$. Thus\n\n$$\n|F|=|J|-|J \\backslash F| \\geq \\frac{1}{2}|J|>0\n$$\n\ncompleting the proof of 4.26 .\n\nTo construct the set $E$ with the desired properties, let $I_{1}, I_{2}, \\ldots$ be a sequence consisting of all nonempty bounded open intervals of $\\mathbf{R}$ with rational endpoints. Let $F_{0}=\\widehat{F}_{0}=\\varnothing$, and inductively construct sequences $F_{1}, F_{2}, \\ldots$ and $\\widehat{F}_{1}, \\widehat{F}_{2}, \\ldots$ of closed subsets of $\\mathbf{R}$ as follows: Suppose $n \\in \\mathbf{Z}^{+}$and $F_{0}, \\ldots, F_{n-1}$ and $\\widehat{F}_{0}, \\ldots, \\widehat{F}_{n-1}$ have been chosen as closed sets that contain no rational numbers. Thus\n\n$$\nI_{n} \\backslash\\left(\\widehat{F_{0}} \\cup \\ldots \\cup \\widehat{F}_{n-1}\\right)\n$$\n\nis a nonempty open set (nonempty because it contains all rational numbers in $I_{n}$ ). Applying 4.26 to the open set above, we see that there is a closed set $F_{n}$ contained in the set above such that $F_{n}$ contains no rational numbers and $\\left|F_{n}\\right|>0$. Applying 4.26 again, but this time to the open set\n\n$$\nI_{n} \\backslash\\left(F_{0} \\cup \\ldots \\cup F_{n}\\right)\n$$\n\nwhich is nonempty because it contains all rational numbers in $I_{n}$, we see that there is a closed set $\\widehat{F}_{n}$ contained in the set above such that $\\widehat{F}_{n}$ contains no rational numbers and $\\left|\\widehat{F}_{n}\\right|>0$.\n\nNow let\n\n$$\nE=\\bigcup_{k=1}^{\\infty} F_{k}\n$$\n\nOur construction implies that $F_{k} \\cap \\widehat{F}_{n}=\\varnothing$ for all $k, n \\in \\mathbf{Z}^{+}$. Thus $E \\cap \\widehat{F}_{n}=\\varnothing$ for all $n \\in \\mathbf{Z}^{+}$. Hence $\\widehat{F}_{n} \\subset I_{n} \\backslash E$ for all $n \\in \\mathbf{Z}^{+}$.\n\nSuppose $I$ is a nonempty bounded open interval. Then $I_{n} \\subset I$ for some $n \\in \\mathbf{Z}^{+}$. Thus\n\n$$\n0<\\left|F_{n}\\right| \\leq\\left|E \\cap I_{n}\\right| \\leq|E \\cap I|\n$$\n\nAlso,\n\n$$\n|E \\cap I|=|I|-|I \\backslash E| \\leq|I|-\\left|I_{n} \\backslash E\\right| \\leq|I|-\\left|\\widehat{F}_{n}\\right|<|I|,\n$$\n\ncompleting the proof.\n\n## EXERCISES 4B\n\nFor $f \\in \\mathcal{L}^{1}(\\mathrm{R})$ and $I$ an interval of $\\mathrm{R}$ with $0<|I|<\\infty$, let $f_{I}$ denote the average of $f$ on I. In other words, $f_{I}=\\frac{1}{|I|} \\int_{I} f$.\n\n1 Suppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Prove that\n\n$$\n\\lim _{t \\downarrow 0} \\frac{1}{2 t} \\int_{b-t}^{b+t}\\left|f-f_{[b-t, b+t]}\\right|=0\n$$\n\nfor almost every $b \\in \\mathbf{R}$.\n\n2 Suppose $f \\in \\mathcal{L}^{1}(\\mathbf{R})$. Prove that\n\n$$\n\\lim _{t \\downarrow 0} \\sup \\left\\{\\frac{1}{|I|} \\int_{I}\\left|f-f_{I}\\right|: I \\text { is an interval of length } t \\text { containing } b\\right\\}=0\n$$\n\nfor almost every $b \\in \\mathbf{R}$.\n\n3 Suppose $f: \\mathbf{R} \\rightarrow \\mathbf{R}$ is a Lebesgue measurable function such that $f^{2} \\in \\mathcal{L}^{1}(\\mathbf{R})$. Prove that\n\n$$\n\\lim _{t \\downarrow 0} \\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)|^{2}=0\n$$\n\nfor almost every $b \\in \\mathbf{R}$.\n\n4 Prove that the Lebesgue Differentiation Theorem (4.19) still holds if the hypothesis that $\\int_{-\\infty}^{\\infty}|f|<\\infty$ is weakened to the requirement that $\\int_{-\\infty}^{x}|f|<\\infty$ for all $x \\in \\mathbf{R}$.\n\n5 Suppose $f: \\mathbf{R} \\rightarrow \\mathbf{R}$ is a Lebesgue measurable function. Prove that\n\n$$\n|f(b)| \\leq f^{*}(b)\n$$\n\nfor almost every $b \\in \\mathbf{R}$.\n\n6 Prove that if $h \\in \\mathcal{L}^{1}(\\mathbf{R})$ and $\\int_{-\\infty}^{s} h=0$ for all $s \\in \\mathbf{R}$, then $h(s)=0$ for almost every $s \\in \\mathbf{R}$.\n\n7 Give an example of a Borel subset of $\\mathbf{R}$ whose density at 0 is not defined.\n\n8 Give an example of a Borel subset of $\\mathbf{R}$ whose density at 0 is $\\frac{1}{3}$.\n\n9 Prove that if $t \\in[0,1]$, then there exists a Borel set $E \\subset \\mathbf{R}$ such that the density of $E$ at 0 is $t$.\n\n10 Suppose $E$ is a Lebesgue measurable subset of $\\mathbf{R}$ such that the density of $E$ equals 1 at every element of $E$ and equals 0 at every element of $\\mathbf{R} \\backslash E$. Prove that $E=\\varnothing$ or $E=\\mathbf{R}$.\n\n## Chapter 5\n\n## Product Measures\n\nLebesgue measure on $\\mathbf{R}$ generalizes the notion of the length of an interval. In this chapter, we see how two-dimensional Lebesgue measure on $\\mathbf{R}^{2}$ generalizes the notion of the area of a rectangle. More generally, we construct new measures that are the products of two measures.\n\nOnce these new measures have been constructed, the question arises of how to compute integrals with respect to these new measures. Beautiful theorems proved in the first decade of the twentieth century allow us to compute integrals with respect to product measures as iterated integrals involving the two measures that produced the product. Furthermore, we will see that under reasonable conditions we can switch the order of an iterated integral.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-131.jpg?height=639&width=1167&top_left_y=934&top_left_x=64)\n\nMain building of Scuola Normale Superiore di Pisa, the university in Pisa, Italy, where Guido Fubini (1879-1943) received his PhD in 1900. In 1907 Fubini proved that under reasonable conditions, an integral with respect to a product measure can be computed as an iterated integral and that the order of integration can be switched. Leonida Tonelli (1885-1943) also taught for many years in Pisa; he also proved a crucial theorem about interchanging the order of integration in an iterated integral. CC-BY-SA Lucarelli\n\n## 5A Products of Measure Spaces\n\n## Products of $\\sigma$-Algebras\n\nOur first step in constructing product measures is to construct the product of two $\\sigma$-algebras. We begin with the following definition.\n\n### 5.1 Definition rectangle\n\nSuppose $X$ and $Y$ are sets. A rectangle in $X \\times Y$ is a set of the form $A \\times B$, where $A \\subset X$ and $B \\subset Y$.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-132.jpg?height=397&width=509&top_left_y=690&top_left_x=699)\n\nNow we can define the product of two $\\sigma$-algebras.\n\n5.2 Definition product of two $\\sigma$-algebras; $\\mathcal{S} \\otimes \\mathcal{T}$; measurable rectangle\n\nSuppose $(X, \\mathcal{S})$ and $(Y, \\mathcal{T})$ are measurable spaces. Then\n\n- the product $\\mathcal{S} \\otimes \\mathcal{T}$ is defined to be the smallest $\\sigma$-algebra on $X \\times Y$ that contains\n\n$$\n\\{A \\times B: A \\in \\mathcal{S}, B \\in \\mathcal{T}\\}\n$$\n\n- a measurable rectangle in $\\mathcal{S} \\otimes \\mathcal{T}$ is a set of the form $A \\times B$, where $A \\in \\mathcal{S}$ and $B \\in \\mathcal{T}$.\n\nUsing the terminology introduced in the second bullet point above, we can say that $\\mathcal{S} \\otimes \\mathcal{T}$ is the smallest $\\sigma$-algebra containing all the measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$. Exercise 1 in this section asks you to show that the measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$ are the only rectangles in\n\nThe notation $\\mathcal{S} \\times \\mathcal{T}$ is not used because $\\mathcal{S}$ and $\\mathcal{T}$ are sets (of sets), and thus the notation $\\mathcal{S} \\times \\mathcal{T}$ already is defined to mean the set of all ordered pairs of the form $(A, B)$, where $A \\in \\mathcal{S}$ and $B \\in \\mathcal{T}$. $X \\times Y$ that are in $\\mathcal{S} \\otimes \\mathcal{T}$.\n\nThe notion of cross sections plays a crucial role in our development of product measures. First, we define cross sections of sets, and then we define cross sections of functions.\n\n5.3 Definition cross sections of sets; $[E]_{a}$ and $[E]^{b}$\n\nSuppose $X$ and $Y$ are sets and $E \\subset X \\times Y$. Then for $a \\in X$ and $b \\in Y$, the cross sections $[E]_{a}$ and $[E]^{b}$ are defined by\n\n$$\n[E]_{a}=\\{y \\in Y:(a, y) \\in E\\} \\quad \\text { and } \\quad[E]^{b}=\\{x \\in X:(x, b) \\in E\\}\n$$\n\n5.4 Example cross sections of a subset of $X \\times Y$\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-133.jpg?height=344&width=1124&top_left_y=578&top_left_x=100)\n\n### 5.5 Example cross sections of rectangles\n\nSuppose $X$ and $Y$ are sets and $A \\subset X$ and $B \\subset Y$. If $a \\in X$ and $b \\in Y$, then\n\n$$\n[A \\times B]_{a}=\\left\\{\\begin{array}{ll}\nB & \\text { if } a \\in A, \\\\\n\\varnothing & \\text { if } a \\notin A\n\\end{array} \\quad \\text { and } \\quad[A \\times B]^{b}= \\begin{cases}A & \\text { if } b \\in B \\\\\n\\varnothing & \\text { if } b \\notin B\\end{cases}\\right.\n$$\n\nas you should verify.\n\nThe next result shows that cross sections preserve measurability.\n\n## 5.6 cross sections of measurable sets are measurable\n\nSuppose $\\mathcal{S}$ is a $\\sigma$-algebra on $X$ and $\\mathcal{T}$ is a $\\sigma$-algebra on $Y$. If $E \\in \\mathcal{S} \\otimes \\mathcal{T}$, then\n\n$$\n[E]_{a} \\in \\mathcal{T} \\text { for every } a \\in X \\quad \\text { and } \\quad[E]^{b} \\in \\mathcal{S} \\text { for every } b \\in Y\n$$\n\nProof Let $\\mathcal{E}$ denote the collection of subsets $E$ of $X \\times Y$ for which the conclusion of this result holds. Then $A \\times B \\in \\mathcal{E}$ for all $A \\in \\mathcal{S}$ and all $B \\in \\mathcal{T}$ (by Example 5.5).\n\nThe collection $\\mathcal{E}$ is closed under complementation and countable unions because\n\n$$\n[(X \\times Y) \\backslash E]_{a}=Y \\backslash[E]_{a}\n$$\n\nand\n\n$$\n\\left[E_{1} \\cup E_{2} \\cup \\cdots\\right]_{a}=\\left[E_{1}\\right]_{a} \\cup\\left[E_{2}\\right]_{a} \\cup \\cdots\n$$\n\nfor all subsets $E, E_{1}, E_{2}, \\ldots$ of $X \\times Y$ and all $a \\in X$, as you should verify, with similar statements holding for cross sections with respect to all $b \\in Y$.\n\nBecause $\\mathcal{E}$ is a $\\sigma$-algebra containing all the measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$, we conclude that $\\mathcal{E}$ contains $\\mathcal{S} \\otimes \\mathcal{T}$.\n\nNow we define cross sections of functions.\n\n5.7 Definition cross sections of functions; $[f]_{a}$ and $[f]^{b}$\n\nSuppose $X$ and $Y$ are sets and $f: X \\times Y \\rightarrow \\mathbf{R}$ is a function. Then for $a \\in X$ and $b \\in Y$, the cross section functions $[f]_{a}: Y \\rightarrow \\mathbf{R}$ and $[f]^{b}: X \\rightarrow \\mathbf{R}$ are defined by\n\n$$\n[f]_{a}(y)=f(a, y) \\text { for } y \\in Y \\quad \\text { and }[f]^{b}(x)=f(x, b) \\text { for } x \\in X\n$$\n\n### 5.8 Example cross sections\n\n- Suppose $f: \\mathbf{R} \\times \\mathbf{R} \\rightarrow \\mathbf{R}$ is defined by $f(x, y)=5 x^{2}+y^{3}$. Then\n\n$$\n[f]_{2}(y)=20+y^{3} \\text { and }[f]^{3}(x)=5 x^{2}+27\n$$\n\nfor all $y \\in \\mathbf{R}$ and all $x \\in \\mathbf{R}$, as you should verify.\n\n- Suppose $X$ and $Y$ are sets and $A \\subset X$ and $B \\subset Y$. If $a \\in X$ and $b \\in Y$, then\n\n$$\n\\left[\\chi_{A \\times B}\\right]_{a}=\\chi_{A}(a) \\chi_{B} \\quad \\text { and } \\quad\\left[\\chi_{A \\times B}\\right]^{b}=\\chi_{B}(b) \\chi_{A} \\text {, }\n$$\n\nas you should verify.\n\nThe next result shows that cross sections preserve measurability, this time in the context of functions rather than sets.\n\n## 5.9 cross sections of measurable functions are measurable\n\nSuppose $\\mathcal{S}$ is a $\\sigma$-algebra on $X$ and $\\mathcal{T}$ is a $\\sigma$-algebra on $Y$. Suppose $f: X \\times Y \\rightarrow \\mathbf{R}$ is an $\\mathcal{S} \\otimes \\mathcal{T}$-measurable function. Then\n\n$[f]_{a}$ is a $\\mathcal{T}$-measurable function on $Y$ for every $a \\in X$\n\nand\n\n$$\n[f]^{b} \\text { is an } \\mathcal{S} \\text {-measurable function on } X \\text { for every } b \\in Y \\text {. }\n$$\n\nProof Suppose $D$ is a Borel subset of $\\mathbf{R}$ and $a \\in X$. If $y \\in Y$, then\n\n$$\n\\begin{aligned}\ny \\in\\left([f]_{a}\\right)^{-1}(D) & \\Longleftrightarrow[f]_{a}(y) \\in D \\\\\n& \\Longleftrightarrow f(a, y) \\in D \\\\\n& \\Longleftrightarrow(a, y) \\in f^{-1}(D) \\\\\n& \\Longleftrightarrow y \\in\\left[f^{-1}(D)\\right]_{a} .\n\\end{aligned}\n$$\n\nThus\n\n$$\n\\left([f]_{a}\\right)^{-1}(D)=\\left[f^{-1}(D)\\right]_{a} .\n$$\n\nBecause $f$ is an $\\mathcal{S} \\otimes \\mathcal{T}$-measurable function, $f^{-1}(D) \\in \\mathcal{S} \\otimes \\mathcal{T}$. Thus the equation above and $5.6 \\mathrm{imply}$ that $\\left([f]_{a}\\right)^{-1}(D) \\in \\mathcal{T}$. Hence $[f]_{a}$ is a $\\mathcal{T}$-measurable function.\n\nThe same ideas show that $[f]^{b}$ is an $\\mathcal{S}$-measurable function for every $b \\in Y$.\n\n## Monotone Class Theorem\n\nThe following standard two-step technique often works to prove that every set in a $\\sigma$-algebra has a certain property:\n\n1. show that every set in a collection of sets that generates the $\\sigma$-algebra has the property;\n2. show that the collection of sets that has the property is a $\\sigma$-algebra.\n\nFor example, the proof of 5.6 used the technique above-first we showed that every measurable rectangle in $\\mathcal{S} \\otimes \\mathcal{T}$ has the desired property, then we showed that the collection of sets that has the desired property is a $\\sigma$-algebra (this completed the proof because $\\mathcal{S} \\otimes \\mathcal{T}$ is the smallest $\\sigma$-algebra containing the measurable rectangles).\n\nThe technique outlined above should be used when possible. However, in some situations there seems to be no reasonable way to verify that the collection of sets with the desired property is a $\\sigma$-algebra. We will encounter this situation in the next subsection. To deal with it, we need to introduce another technique that involves what are called monotone classes.\n\nThe following definition will be used in our main theorem about monotone classes.\n\n### 5.10 Definition algebra\n\nSuppose $W$ is a set and $\\mathcal{A}$ is a set of subsets of $W$. Then $\\mathcal{A}$ is called an algebra on $W$ if the following three conditions are satisfied:\n\n- $\\varnothing \\in \\mathcal{A}$;\n- if $E \\in \\mathcal{A}$, then $W \\backslash E \\in \\mathcal{A}$;\n- if $E$ and $F$ are elements of $\\mathcal{A}$, then $E \\cup F \\in \\mathcal{A}$.\n\nThus an algebra is closed under complementation and under finite unions; a $\\sigma$-algebra is closed under complementation and countable unions.\n\n### 5.11 Example collection of finite unions of intervals is an algebra\n\nSuppose $\\mathcal{A}$ is the collection of all finite unions of intervals of $\\mathbf{R}$. Here we are including all intervals - open intervals, closed intervals, bounded intervals, unbounded intervals, sets consisting of only a single point, and intervals that are neither open nor closed because they contain one endpoint but not the other endpoint.\n\nClearly $\\mathcal{A}$ is closed under finite unions. You should also verify that $\\mathcal{A}$ is closed under complementation. Thus $\\mathcal{A}$ is an algebra on $\\mathbf{R}$.\n\n### 5.12 Example collection of countable unions of intervals is not an algebra\n\nSuppose $\\mathcal{A}$ is the collection of all countable unions of intervals of $\\mathbf{R}$.\n\nClearly $\\mathcal{A}$ is closed under finite unions (and also under countable unions). You should verify that $\\mathcal{A}$ is not closed under complementation. Thus $\\mathcal{A}$ is neither an algebra nor a $\\sigma$-algebra on $\\mathbf{R}$.\n\nThe following result provides an example of an algebra that we will exploit.\n\n### 5.13 the set of finite unions of measurable rectangles is an algebra\n\nSuppose $(X, \\mathcal{S})$ and $(Y, \\mathcal{T})$ are measurable spaces. Then\n\n(a) the set of finite unions of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$ is an algebra on $X \\times Y$;\n\n(b) every finite union of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$ can be written as a finite union of disjoint measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$.\n\nProof Let $\\mathcal{A}$ denote the set of finite unions of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$. Obviously $\\mathcal{A}$ is closed under finite unions.\n\nThe collection $\\mathcal{A}$ is also closed under finite intersections. To verify this claim, note that if $A_{1}, \\ldots, A_{n}, C_{1}, \\ldots, C_{m} \\in \\mathcal{S}$ and $B_{1}, \\ldots, B_{n}, D_{1}, \\ldots, D_{m} \\in \\mathcal{T}$, then\n\n$$\n\\begin{aligned}\n& \\left(\\left(A_{1} \\times B_{1}\\right) \\cup \\cdots \\cup\\left(A_{n} \\times B_{n}\\right)\\right) \\cap\\left(\\left(C_{1} \\times D_{1}\\right) \\cup \\cdots \\cup\\left(C_{m} \\times D_{m}\\right)\\right) \\\\\n& =\\bigcup_{j=1}^{n} \\bigcup_{k=1}^{m}\\left(\\left(A_{j} \\times B_{j}\\right) \\cap\\left(C_{k} \\times D_{k}\\right)\\right) \\\\\n& =\\bigcup_{j=1}^{n} \\bigcup_{k=1}^{m}\\left(\\left(A_{j} \\cap C_{k}\\right) \\times\\left(B_{j} \\cap D_{k}\\right)\\right), \\quad A \\times B\n\\end{aligned}\n$$\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-136.jpg?height=250&width=489&top_left_y=902&top_left_x=683)\n\nIntersection of two rectangles is a rectangle.\n\nwhich implies that $\\mathcal{A}$ is closed under finite intersections.\n\nIf $A \\in \\mathcal{S}$ and $B \\in \\mathcal{T}$, then\n\n$$\n(X \\times Y) \\backslash(A \\times B)=((X \\backslash A) \\times Y) \\cup(X \\times(Y \\backslash B))\n$$\n\nHence the complement of each measurable rectangle in $\\mathcal{S} \\otimes \\mathcal{T}$ is in $\\mathcal{A}$. Thus the complement of a finite union of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$ is in $\\mathcal{A}$ (use De Morgan's Laws and the result in the previous paragraph that $\\mathcal{A}$ is closed under finite intersections). In other words, $\\mathcal{A}$ is closed under complementation, completing the proof of (a).\n\nTo prove (b), note that if $A \\times B$ and $C \\times D$ are measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$, then (as can be verified in the figure above)\n\n$$\n5.14(A \\times B) \\cup(C \\times D)=(A \\times B) \\cup(C \\times(D \\backslash B)) \\cup((C \\backslash A) \\times(B \\cap D)) .\n$$\n\nThe equation above writes the union of two measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$ as the union of three disjoint measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$.\n\nNow consider any finite union of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$. If this is not a disjoint union, then choose any nondisjoint pair of measurable rectangles in the union and replace those two measurable rectangles with the union of three disjoint measurable rectangles as in 5.14. Iterate this process until obtaining a disjoint union of measurable rectangles.\n\nNow we define a monotone class as a collection of sets that is closed under countable increasing unions and under countable decreasing intersections.\n\n### 5.15 Definition monotone class\n\nSuppose $W$ is a set and $\\mathcal{M}$ is a set of subsets of $W$. Then $\\mathcal{M}$ is called a monotone class on $W$ if the following two conditions are satisfied:\n\n- If $E_{1} \\subset E_{2} \\subset \\cdots$ is an increasing sequence of sets in $\\mathcal{M}$, then $\\bigcup_{k=1}^{\\infty} E_{k} \\in \\mathcal{M}$;\n- If $E_{1} \\supset E_{2} \\supset \\cdots$ is a decreasing sequence of sets in $\\mathcal{M}$, then $\\bigcap_{k=1}^{\\infty} E_{k} \\in \\mathcal{M}$.\n\nClearly every $\\sigma$-algebra is a monotone class. However, some monotone classes are not closed under even finite unions, as shown by the next example.\n\n### 5.16 Example a monotone class that is not an algebra\n\nSuppose $\\mathcal{A}$ is the collection of all intervals of $\\mathbf{R}$. Then $\\mathcal{A}$ is closed under countable increasing unions and countable decreasing intersections. Thus $\\mathcal{A}$ is a monotone class on $\\mathbf{R}$. However, $\\mathcal{A}$ is not closed under finite unions, and $\\mathcal{A}$ is not closed under complementation. Thus $\\mathcal{A}$ is neither an algebra nor a $\\sigma$-algebra on $\\mathbf{R}$.\n\nIf $\\mathcal{A}$ is a collection of subsets of some set $W$, then the intersection of all monotone classes on $W$ that contain $\\mathcal{A}$ is a monotone class that contains $\\mathcal{A}$. Thus this intersection is the smallest monotone class on $W$ that contains $\\mathcal{A}$.\n\nThe next result provides a useful tool when the standard technique for showing that every set in a $\\sigma$-algebra has a certain property does not work.\n\n### 5.17 Monotone Class Theorem\n\nSuppose $\\mathcal{A}$ is an algebra on a set $W$. Then the smallest $\\sigma$-algebra containing $\\mathcal{A}$ is the smallest monotone class containing $\\mathcal{A}$.\n\nProof Let $\\mathcal{M}$ denote the smallest monotone class containing $\\mathcal{A}$. Because every $\\sigma$ algebra is a monotone class, $\\mathcal{M}$ is contained in the smallest $\\sigma$-algebra containing $\\mathcal{A}$.\n\nTo prove the inclusion in the other direction, first suppose $A \\in \\mathcal{A}$. Let\n\n$$\n\\mathcal{E}=\\{E \\in \\mathcal{M}: A \\cup E \\in \\mathcal{M}\\}\n$$\n\nThen $\\mathcal{A} \\subset \\mathcal{E}$ (because the union of two sets in $\\mathcal{A}$ is in $\\mathcal{A}$ ). A moment's thought shows that $\\mathcal{E}$ is a monotone class. Thus the smallest monotone class that contains $\\mathcal{A}$ is contained in $\\mathcal{E}$, meaning that $\\mathcal{M} \\subset \\mathcal{E}$. Hence we have proved that $A \\cup E \\in \\mathcal{M}$ for every $E \\in \\mathcal{M}$.\n\nNow let\n\n$$\n\\mathcal{D}=\\{D \\in \\mathcal{M}: D \\cup E \\in \\mathcal{M} \\text { for all } E \\in \\mathcal{M}\\}\n$$\n\nThe previous paragraph shows that $\\mathcal{A} \\subset \\mathcal{D}$. A moment's thought again shows that $\\mathcal{D}$ is a monotone class. Thus, as in the previous paragraph, we conclude that $\\mathcal{M} \\subset \\mathcal{D}$. Hence we have proved that $D \\cup E \\in \\mathcal{M}$ for all $D, E \\in \\mathcal{M}$.\n\nThe paragraph above shows that the monotone class $\\mathcal{M}$ is closed under finite unions. Now if $E_{1}, E_{2}, \\ldots \\in \\mathcal{M}$, then\n\n$$\nE_{1} \\cup E_{2} \\cup E_{3} \\cup \\cdots=E_{1} \\cup\\left(E_{1} \\cup E_{2}\\right) \\cup\\left(E_{1} \\cup E_{2} \\cup E_{3}\\right) \\cup \\cdots,\n$$\n\nwhich is an increasing union of a sequence of sets in $\\mathcal{M}$ (by the previous paragraph). We conclude that $\\mathcal{M}$ is closed under countable unions.\n\nFinally, let\n\n$$\n\\mathcal{M}^{\\prime}=\\{E \\in \\mathcal{M}: W \\backslash E \\in \\mathcal{M}\\}\n$$\n\nThen $\\mathcal{A} \\subset \\mathcal{M}^{\\prime}$ (because $\\mathcal{A}$ is closed under complementation). Once again, you should verify that $\\mathcal{M}^{\\prime}$ is a monotone class. Thus $\\mathcal{M} \\subset \\mathcal{M}^{\\prime}$. We conclude that $\\mathcal{M}$ is closed under complementation.\n\nThe two previous paragraphs show that $\\mathcal{M}$ is closed under countable unions and under complementation. Thus $\\mathcal{M}$ is a $\\sigma$-algebra that contains $\\mathcal{A}$. Hence $\\mathcal{M}$ contains the smallest $\\sigma$-algebra containing $\\mathcal{A}$, completing the proof.\n\n## Products of Measures\n\nThe following definitions will be useful.\n\n### 5.18 Definition finite measure; $\\sigma$-finite measure\n\n- A measure $\\mu$ on a measurable space $(X, \\mathcal{S})$ is called finite if $\\mu(X)<\\infty$.\n- A measure is called $\\sigma$-finite if the whole space can be written as the countable union of sets with finite measure.\n- More precisely, a measure $\\mu$ on a measurable space $(X, \\mathcal{S})$ is called $\\sigma$-finite if there exists a sequence $X_{1}, X_{2}, \\ldots$ of sets in $\\mathcal{S}$ such that\n\n$$\nX=\\bigcup_{k=1}^{\\infty} X_{k} \\quad \\text { and } \\quad \\mu\\left(X_{k}\\right)<\\infty \\text { for every } k \\in \\mathbf{Z}^{+} \\text {. }\n$$\n\n### 5.19 Example finite and $\\sigma$-finite measures\n\n- Lebesgue measure on the interval $[0,1]$ is a finite measure.\n- Lebesgue measure on $\\mathbf{R}$ is not a finite measure but is a $\\sigma$-finite measure.\n- Counting measure on $\\mathbf{R}$ is not a $\\sigma$-finite measure (because the countable union of finite sets is a countable set).\n\nThe next result will allow us to define the product of two $\\sigma$-finite measures.\n\n### 5.20 measure of cross section is a measurable function\n\nSuppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are $\\sigma$-finite measure spaces. If $E \\in \\mathcal{S} \\otimes \\mathcal{T}$, then\n\n(a) $x \\mapsto v\\left([E]_{x}\\right)$ is an $\\mathcal{S}$-measurable function on $X$;\n\n(b) $y \\mapsto \\mu\\left([E]^{y}\\right)$ is a $\\mathcal{T}$-measurable function on $Y$.\n\nProof We will prove (a). If $E \\in \\mathcal{S} \\otimes \\mathcal{T}$, then $[E]_{x} \\in \\mathcal{T}$ for every $x \\in X$ (by 5.6); thus the function $x \\mapsto v\\left([E]_{x}\\right)$ is well defined on $X$.\n\nWe first consider the case where $v$ is a finite measure. Let\n\n$$\n\\mathcal{M}=\\left\\{E \\in \\mathcal{S} \\otimes \\mathcal{T}: x \\mapsto v\\left([E]_{x}\\right) \\text { is an } \\mathcal{S} \\text {-measurable function on } X\\right\\}\n$$\n\nWe need to prove that $\\mathcal{M}=\\mathcal{S} \\otimes \\mathcal{T}$.\n\nIf $A \\in \\mathcal{S}$ and $B \\in \\mathcal{T}$, then $v\\left([A \\times B]_{x}\\right)=v(B) \\chi_{A}(x)$ for every $x \\in X$ (by Example 5.5). Thus the function $x \\mapsto v\\left([A \\times B]_{x}\\right)$ equals the function $v(B) \\chi_{A}$ (as a function on $X$ ), which is an $\\mathcal{S}$-measurable function on $X$. Hence $\\mathcal{M}$ contains all the measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$.\n\nLet $\\mathcal{A}$ denote the set of finite unions of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$. Suppose $E \\in \\mathcal{A}$. Then by 5.13 (b), $E$ is a union of disjoint measurable rectangles $E_{1}, \\ldots, E_{n}$. Thus\n\n$$\n\\begin{aligned}\nv\\left([E]_{x}\\right) & =v\\left(\\left[E_{1} \\cup \\cdots \\cup E_{n}\\right]_{x}\\right) \\\\\n& =v\\left(\\left[E_{1}\\right]_{x} \\cup \\cdots \\cup\\left[E_{n}\\right]_{x}\\right) \\\\\n& =v\\left(\\left[E_{1}\\right]_{x}\\right)+\\cdots+v\\left(\\left[E_{n}\\right]_{x}\\right)\n\\end{aligned}\n$$\n\nwhere the last equality holds because $v$ is a measure and $\\left[E_{1}\\right]_{x}, \\ldots,\\left[E_{n}\\right]_{x}$ are disjoint. The equation above, when combined with the conclusion of the previous paragraph, shows that $x \\mapsto v\\left([E]_{x}\\right)$ is a finite sum of $\\mathcal{S}$-measurable functions and thus is an $\\mathcal{S}$-measurable function. Hence $E \\in \\mathcal{M}$. We have now shown that $\\mathcal{A} \\subset \\mathcal{M}$.\n\nOur next goal is to show that $\\mathcal{M}$ is a monotone class on $X \\times Y$. To do this, first suppose $E_{1} \\subset E_{2} \\subset \\cdots$ is an increasing sequence of sets in $\\mathcal{M}$. Then\n\n$$\n\\begin{aligned}\nv\\left(\\left[\\bigcup_{k=1}^{\\infty} E_{k}\\right]_{x}\\right) & =v\\left(\\bigcup_{k=1}^{\\infty}\\left(\\left[E_{k}\\right]_{x}\\right)\\right) \\\\\n& =\\lim _{k \\rightarrow \\infty} v\\left(\\left[E_{k}\\right]_{x}\\right)\n\\end{aligned}\n$$\n\nwhere we have used 2.59. Because the pointwise limit of $\\mathcal{S}$-measurable functions is $\\mathcal{S}$-measurable (by 2.48), the equation above shows that $x \\mapsto v\\left(\\left[\\bigcup_{k=1}^{\\infty} E_{k}\\right]_{x}\\right)$ is an $\\mathcal{S}$-measurable function. Hence $\\bigcup_{k=1}^{\\infty} E_{k} \\in \\mathcal{M}$. We have now shown that $\\mathcal{M}$ is closed under countable increasing unions.\n\nNow suppose $E_{1} \\supset E_{2} \\supset \\cdots$ is a decreasing sequence of sets in $\\mathcal{M}$. Then\n\n$$\n\\begin{aligned}\nv\\left(\\left[\\bigcap_{k=1}^{\\infty} E_{k}\\right]_{x}\\right) & =v\\left(\\bigcap_{k=1}^{\\infty}\\left(\\left[E_{k}\\right]_{x}\\right)\\right) \\\\\n& =\\lim _{k \\rightarrow \\infty} v\\left(\\left[E_{k}\\right]_{x}\\right)\n\\end{aligned}\n$$\n\nwhere we have used 2.60 (this is where we use the assumption that $v$ is a finite measure). Because the pointwise limit of $\\mathcal{S}$-measurable functions is $\\mathcal{S}$-measurable (by 2.48), the equation above shows that $x \\mapsto v\\left(\\left[\\bigcap_{k=1}^{\\infty} E_{k}\\right]_{x}\\right)$ is an $\\mathcal{S}$-measurable function. Hence $\\bigcap_{k=1}^{\\infty} E_{k} \\in \\mathcal{M}$. We have now shown that $\\mathcal{M}$ is closed under countable decreasing intersections.\n\nWe have shown that $\\mathcal{M}$ is a monotone class that contains the algebra $\\mathcal{A}$ of all finite unions of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$ [by 5.13(a), $\\mathcal{A}$ is indeed an algebra]. The Monotone Class Theorem (5.17) implies that $\\mathcal{M}$ contains the smallest $\\sigma$-algebra containing $\\mathcal{A}$. In other words, $\\mathcal{M}$ contains $\\mathcal{S} \\otimes \\mathcal{T}$. This conclusion completes the proof of (a) in the case where $v$ is a finite measure.\n\nNow consider the case where $v$ is a $\\sigma$-finite measure. Thus there exists a sequence $Y_{1}, Y_{2}, \\ldots$ of sets in $\\mathcal{T}$ such that $\\bigcup_{k=1}^{\\infty} Y_{k}=Y$ and $v\\left(Y_{k}\\right)<\\infty$ for each $k \\in \\mathbf{Z}^{+}$. Replacing each $Y_{k}$ by $Y_{1} \\cup \\cdots \\cup Y_{k}$, we can assume that $Y_{1} \\subset Y_{2} \\subset \\cdots$. If $E \\in \\mathcal{S} \\otimes \\mathcal{T}$, then\n\n$$\nv\\left([E]_{x}\\right)=\\lim _{k \\rightarrow \\infty} v\\left(\\left[E \\cap\\left(X \\times Y_{k}\\right)\\right]_{x}\\right)\n$$\n\nThe function $x \\mapsto v\\left(\\left[E \\cap\\left(X \\times Y_{k}\\right)\\right]_{x}\\right)$ is an $\\mathcal{S}$-measurable function on $X$, as follows by considering the finite measure obtained by restricting $v$ to the $\\sigma$-algebra on $Y_{k}$ consisting of sets in $\\mathcal{T}$ that are contained in $Y_{k}$. The equation above now implies that $x \\mapsto v\\left([E]_{x}\\right)$ is an $\\mathcal{S}$-measurable function on $X$, completing the proof of (a).\n\nThe proof of (b) is similar.\n\n### 5.21 Definition integration notation\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $g: X \\rightarrow[-\\infty, \\infty]$ is a function. The notation\n\n$$\n\\int g(x) d \\mu(x) \\text { means } \\int g d \\mu\n$$\n\nwhere $d \\mu(x)$ indicates that variables other than $x$ should be treated as constants.\n\n### 5.22 Example integrals\n\nIf $\\lambda$ is Lebesgue measure on $[0,4]$, then\n\n$$\n\\int_{[0,4]}\\left(x^{2}+y\\right) d \\lambda(y)=4 x^{2}+8 \\quad \\text { and } \\quad \\int_{[0,4]}\\left(x^{2}+y\\right) d \\lambda(x)=\\frac{64}{3}+4 y \\text {. }\n$$\n\nThe intent in the next definition is that $\\int_{X} \\int_{Y} f(x, y) d v(y) d \\mu(x)$ is defined only when the inner integral and then the outer integral both make sense.\n\n### 5.23 Definition iterated integrals\n\nSuppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are measure spaces and $f: X \\times Y \\rightarrow \\mathbf{R}$ is a function. Then\n\n$$\n\\int_{X} \\int_{Y} f(x, y) d \\nu(y) d \\mu(x) \\quad \\text { means } \\quad \\int_{X}\\left(\\int_{Y} f(x, y) d \\nu(y)\\right) d \\mu(x)\n$$\n\nIn other words, to compute $\\int_{X} \\int_{Y} f(x, y) d v(y) d \\mu(x)$, first (temporarily) fix $x \\in$ $X$ and compute $\\int_{Y} f(x, y) d v(y)$ [if this integral makes sense]. Then compute the integral with respect to $\\mu$ of the function $x \\mapsto \\int_{Y} f(x, y) d v(y)$ [if this integral makes sense].\n\n### 5.24 Example iterated integrals\n\nIf $\\lambda$ is Lebesgue measure on $[0,4]$, then\n\n$$\n\\begin{aligned}\n\\int_{[0,4]} \\int_{[0,4]}\\left(x^{2}+y\\right) d \\lambda(y) d \\lambda(x) & =\\int_{[0,4]}\\left(4 x^{2}+8\\right) d \\lambda(x) \\\\\n& =\\frac{352}{3}\n\\end{aligned}\n$$\n\nand\n\n$$\n\\begin{aligned}\n\\int_{[0,4]} \\int_{[0,4]}\\left(x^{2}+y\\right) d \\lambda(x) d \\lambda(y) & =\\int_{[0,4]}\\left(\\frac{64}{3}+4 y\\right) d \\lambda(y) \\\\\n& =\\frac{352}{3} .\n\\end{aligned}\n$$\n\nThe two iterated integrals in this example turned out to both equal $\\frac{352}{3}$, even though they do not look alike in the intermediate step of the evaluation. As we will see in the next section, this equality of integrals when changing the order of integration is not a coincidence.\n\nThe definition of $(\\mu \\times v)(E)$ given below makes sense because the inner integral below equals $v\\left([E]_{x}\\right)$, which makes sense by 5.6 (or use 5.9), and then the outer integral makes sense by 5.20 (a).\n\nThe restriction in the definition below to $\\sigma$-finite measures is not bothersome because the main results we seek are not valid without this hypothesis (see Example 5.30 in the next section).\n\n### 5.25 Definition product of two measures; $\\mu \\times v$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are $\\sigma$-finite measure spaces. For $E \\in \\mathcal{S} \\otimes \\mathcal{T}$, define $(\\mu \\times v)(E)$ by\n\n$$\n(\\mu \\times v)(E)=\\int_{X} \\int_{Y} \\chi_{E}(x, y) d \\nu(y) d \\mu(x)\n$$\n\n### 5.26 Example measure of a rectangle\n\nSuppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are $\\sigma$-finite measure spaces. If $A \\in \\mathcal{S}$ and $B \\in \\mathcal{T}$, then\n\n$$\n\\begin{aligned}\n(\\mu \\times v)(A \\times B) & =\\int_{X} \\int_{Y} \\chi_{A \\times B}(x, y) d v(y) d \\mu(x) \\\\\n& =\\int_{X} v(B) \\chi_{A}(x) d \\mu(x) \\\\\n& =\\mu(A) v(B) .\n\\end{aligned}\n$$\n\nThus product measure of a measurable rectangle is the product of the measures of the corresponding sets.\n\nFor $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v) \\sigma$-finite measure spaces, we defined the product $\\mu \\times v$ to be a function from $\\mathcal{S} \\otimes \\mathcal{T}$ to $[0, \\infty]$ (see 5.25). Now we show that this function is a measure.\n\n### 5.27 product of two measures is a measure\n\nSuppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are $\\sigma$-finite measure spaces. Then $\\mu \\times v$ is a measure on $(X \\times Y, \\mathcal{S} \\otimes \\mathcal{T})$.\n\nProof Clearly $(\\mu \\times v)(\\varnothing)=0$.\n\nSuppose $E_{1}, E_{2}, \\ldots$ is a disjoint sequence of sets in $\\mathcal{S} \\otimes \\mathcal{T}$. Then\n\n$$\n\\begin{aligned}\n(\\mu \\times v)\\left(\\bigcup_{k=1}^{\\infty} E_{k}\\right) & =\\int_{X} v\\left(\\left[\\bigcup_{k=1}^{\\infty} E_{k}\\right]_{x}\\right) d \\mu(x) \\\\\n& =\\int_{X} v\\left(\\bigcup_{k=1}^{\\infty}\\left(\\left[E_{k}\\right]_{x}\\right)\\right) d \\mu(x) \\\\\n& =\\int_{X}\\left(\\sum_{k=1}^{\\infty} v\\left(\\left[E_{k}\\right]_{x}\\right)\\right) d \\mu(x) \\\\\n& =\\sum_{k=1}^{\\infty} \\int_{X} v\\left(\\left[E_{k}\\right]_{x}\\right) d \\mu(x) \\\\\n& =\\sum_{k=1}^{\\infty}(\\mu \\times v)\\left(E_{k}\\right),\n\\end{aligned}\n$$\n\nwhere the fourth equality follows from the Monotone Convergence Theorem (3.11; or see Exercise 10 in Section 3A). The equation above shows that $\\mu \\times v$ satisfies the countable additivity condition required for a measure.\n\n## EXERCISES 5A\n\n1 Suppose $(X, \\mathcal{S})$ and $(Y, \\mathcal{T})$ are measurable spaces. Prove that if $A$ is a nonempty subset of $X$ and $B$ is a nonempty subset of $Y$ such that $A \\times B \\in$ $\\mathcal{S} \\otimes \\mathcal{T}$, then $A \\in \\mathcal{S}$ and $B \\in \\mathcal{T}$.\n\n2 Suppose $(X, \\mathcal{S})$ is a measurable space. Prove that if $E \\in \\mathcal{S} \\otimes \\mathcal{S}$, then\n\n$$\n\\{x \\in X:(x, x) \\in E\\} \\in \\mathcal{S} .\n$$\n\n3 Let $\\mathcal{B}$ denote the $\\sigma$-algebra of Borel subsets of $\\mathbf{R}$. Show that there exists a set $E \\subset \\mathbf{R} \\times \\mathbf{R}$ such that $[E]_{a} \\in \\mathcal{B}$ and $[E]^{a} \\in \\mathcal{B}$ for every $a \\in \\mathbf{R}$, but $E \\notin \\mathcal{B} \\otimes \\mathcal{B}$.\n\n4 Suppose $(X, \\mathcal{S})$ and $(Y, \\mathcal{T})$ are measurable spaces. Prove that if $f: X \\rightarrow \\mathbf{R}$ is $\\mathcal{S}$-measurable and $g: Y \\rightarrow \\mathbf{R}$ is $\\mathcal{T}$-measurable and $h: X \\times Y \\rightarrow \\mathbf{R}$ is defined by $h(x, y)=f(x) g(y)$, then $h$ is $(\\mathcal{S} \\otimes \\mathcal{T})$-measurable.\n\n5 Verify the assertion in Example 5.11 that the collection of finite unions of intervals of $\\mathbf{R}$ is closed under complementation.\n\n6 Verify the assertion in Example 5.12 that the collection of countable unions of intervals of $\\mathbf{R}$ is not closed under complementation.\n\n7 Suppose $\\mathcal{A}$ is a nonempty collection of subsets of a set $W$. Show that $\\mathcal{A}$ is an algebra on $W$ if and only if $\\mathcal{A}$ is closed under finite intersections and under complementation.\n\n8 Suppose $\\mu$ is a measure on a measurable space $(X, \\mathcal{S})$. Prove that the following are equivalent:\n\n(a) The measure $\\mu$ is $\\sigma$-finite.\n\n(b) There exists an increasing sequence $X_{1} \\subset X_{2} \\subset \\cdots$ of sets in $\\mathcal{S}$ such that $X=\\bigcup_{k=1}^{\\infty} X_{k}$ and $\\mu\\left(X_{k}\\right)<\\infty$ for every $k \\in \\mathbf{Z}^{+}$.\n\n(c) There exists a disjoint sequence $X_{1}, X_{2}, X_{3}, \\ldots$ of sets in $\\mathcal{S}$ such that $X=\\bigcup_{k=1}^{\\infty} X_{k}$ and $\\mu\\left(X_{k}\\right)<\\infty$ for every $k \\in \\mathbf{Z}^{+}$.\n\n9 Suppose $\\mu$ and $v$ are $\\sigma$-finite measures. Prove that $\\mu \\times v$ is a $\\sigma$-finite measure.\n\n10 Suppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are $\\sigma$-finite measure spaces. Prove that if $\\omega$ is a measure on $\\mathcal{S} \\otimes \\mathcal{T}$ such that $\\omega(A \\times B)=\\mu(A) v(B)$ for all $A \\in \\mathcal{S}$ and all $B \\in \\mathcal{T}$, then $\\omega=\\mu \\times v$.\n\n[The exercise above means that $\\mu \\times v$ is the unique measure on $\\mathcal{S} \\otimes \\mathcal{T}$ that behaves as we expect on measurable rectangles.]\n\n## 5B Iterated Integrals\n\n## Tonelli's Theorem\n\nRelook at Example 5.24 in the previous section and notice that the value of the iterated integral was unchanged when we switched the order of integration, even though switching the order of integration led to different intermediate results. Our next result states that the order of integration can be switched if the function being integrated is nonnegative and the measures are $\\sigma$-finite.\n\n### 5.28 Tonelli's Theorem\n\nSuppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are $\\sigma$-finite measure spaces. Suppose $f: X \\times Y \\rightarrow[0, \\infty]$ is $\\mathcal{S} \\otimes \\mathcal{T}$-measurable. Then\n\n$$\n\\begin{aligned}\nx & \\mapsto \\int_{Y} f(x, y) d \\nu(y) \\text { is an } \\mathcal{S} \\text {-measurable function on } X, \\\\\ny & \\mapsto \\int_{X} f(x, y) d \\mu(x) \\text { is a } \\mathcal{T} \\text {-measurable function on } Y,\n\\end{aligned}\n$$\n\nand\n\n$$\n\\int_{X \\times Y} f d(\\mu \\times v)=\\int_{X} \\int_{Y} f(x, y) d \\nu(y) d \\mu(x)=\\int_{Y} \\int_{X} f(x, y) d \\mu(x) d \\nu(y) .\n$$\n\nProof We begin by considering the special case where $f=\\chi_{E}$ for some $E \\in \\mathcal{S} \\otimes \\mathcal{T}$. In this case,\n\n$$\n\\int_{Y} \\chi_{E}(x, y) d v(y)=v\\left([E]_{x}\\right) \\text { for every } x \\in X\n$$\n\nand\n\n$$\n\\int_{X} \\chi_{E}(x, y) d \\mu(x)=\\mu\\left([E]^{y}\\right) \\text { for every } y \\in Y\n$$\n\nThus (a) and (b) hold in this case by 5.20.\n\nFirst assume that $\\mu$ and $v$ are finite measures. Let\n\n$\\mathcal{M}=\\left\\{E \\in \\mathcal{S} \\otimes \\mathcal{T}: \\int_{X} \\int_{Y} \\chi_{E}(x, y) d v(y) d \\mu(x)=\\int_{Y} \\int_{X} \\chi_{E}(x, y) d \\mu(x) d \\nu(y)\\right\\}$.\n\nIf $A \\in \\mathcal{S}$ and $B \\in \\mathcal{T}$, then $A \\times B \\in \\mathcal{M}$ because both sides of the equation defining $\\mathcal{M}$ equal $\\mu(A) v(B)$.\n\nLet $\\mathcal{A}$ denote the set of finite unions of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$. Then 5.13(b) implies that every element of $\\mathcal{A}$ is a disjoint union of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$. The previous paragraph now implies $\\mathcal{A} \\subset \\mathcal{M}$.\n\nThe Monotone Convergence Theorem (3.11) implies that $\\mathcal{M}$ is closed under countable increasing unions. The Bounded Convergence Theorem (3.26) implies that $\\mathcal{M}$ is closed under countable decreasing intersections (this is where we use the assumption that $\\mu$ and $v$ are finite measures).\n\nWe have shown that $\\mathcal{M}$ is a monotone class that contains the algebra $\\mathcal{A}$ of all finite unions of measurable rectangles in $\\mathcal{S} \\otimes \\mathcal{T}$ [by 5.13(a), $\\mathcal{A}$ is indeed an algebra].\n\nThe Monotone Class Theorem (5.17) implies that $\\mathcal{M}$ contains the smallest $\\sigma$-algebra containing $\\mathcal{A}$. In other words, $\\mathcal{M}$ contains $\\mathcal{S} \\otimes \\mathcal{T}$. Thus\n\n$$\n\\int_{X} \\int_{Y} \\chi_{E}(x, y) d v(y) d \\mu(x)=\\int_{Y} \\int_{X} \\chi_{E}(x, y) d \\mu(x) d v(y)\n$$\n\nfor every $E \\in \\mathcal{S} \\otimes \\mathcal{T}$.\n\nNow relax the assumption that $\\mu$ and $v$ are finite measures. Write $X$ as an increasing union of sets $X_{1} \\subset X_{2} \\subset \\cdots$ in $\\mathcal{S}$ with finite measure, and write $Y$ as an increasing union of sets $Y_{1} \\subset Y_{2} \\subset \\cdots$ in $\\mathcal{T}$ with finite measure. Suppose $E \\in \\mathcal{S} \\otimes \\mathcal{T}$. Applying the finite-measure case to the situation where the measures and the $\\sigma$-algebras are restricted to $X_{j}$ and $Y_{k}$, we can conclude that 5.29 holds with $E$ replaced by $E \\cap\\left(X_{j} \\times Y_{k}\\right)$ for all $j, k \\in \\mathbf{Z}^{+}$. Fix $k \\in \\mathbf{Z}^{+}$and use the Monotone Convergence Theorem (3.11) to conclude that 5.29 holds with $E$ replaced by $E \\cap\\left(X \\times Y_{k}\\right)$ for all $k \\in \\mathbf{Z}^{+}$. One more use of the Monotone Convergence Theorem then shows that\n\n$$\n\\int_{X \\times Y} \\chi_{E} d(\\mu \\times v)=\\int_{X} \\int_{Y} \\chi_{E}(x, y) d v(y) d \\mu(x)=\\int_{Y} \\int_{X} \\chi_{E}(x, y) d \\mu(x) d v(y)\n$$\n\nfor all $E \\in \\mathcal{S} \\otimes \\mathcal{T}$, where the first equality above comes from the definition of $(\\mu \\times v)(E)($ see 5.25$)$.\n\nNow we turn from characteristic functions to the general case of an $\\mathcal{S} \\otimes \\mathcal{T}$ measurable function $f: X \\times Y \\rightarrow[0, \\infty]$. Define a sequence $f_{1}, f_{2}, \\ldots$ of simple $\\mathcal{S} \\otimes \\mathcal{T}$-measurable functions from $X \\times Y$ to $[0, \\infty)$ by\n\n$f_{k}(x, y)= \\begin{cases}\\frac{m}{2^{k}} & \\text { if } f(x, y)<k \\text { and } m \\text { is the integer with } f(x, y) \\in\\left[\\frac{m}{2^{k}}, \\frac{m+1}{2^{k}}\\right), \\\\ k & \\text { if } f(x, y) \\geq k .\\end{cases}$\n\nNote that\n\n$$\n0 \\leq f_{1}(x, y) \\leq f_{2}(x, y) \\leq f_{3}(x, y) \\leq \\cdots \\quad \\text { and } \\quad \\lim _{k \\rightarrow \\infty} f_{k}(x, y)=f(x, y)\n$$\n\nfor all $(x, y) \\in X \\times Y$.\n\nEach $f_{k}$ is a finite sum of functions of the form $c \\chi_{E}$, where $c \\in \\mathbf{R}$ and $E \\in \\mathcal{S} \\otimes \\mathcal{T}$. Thus the conclusions of this theorem hold for each function $f_{k}$.\n\nThe Monotone Convergence Theorem implies that\n\n$$\n\\int_{Y} f(x, y) d v(y)=\\lim _{k \\rightarrow \\infty} \\int_{Y} f_{k}(x, y) d v(y)\n$$\n\nfor every $x \\in X$. Thus the function $x \\mapsto \\int_{Y} f(x, y) d v(y)$ is the pointwise limit on $X$ of a sequence of $\\mathcal{S}$-measurable functions. Hence (a) holds, as does (b) for similar reasons.\n\nThe last line in the statement of this theorem holds for each $f_{k}$. The Monotone Convergence Theorem now implies that the last line in the statement of this theorem holds for $f$, completing the proof.\n\nSee Exercise 1 in this section for an example (with finite measures) showing that Tonelli's Theorem can fail without the hypothesis that the function being integrated is nonnegative. The next example shows that the hypothesis of $\\sigma$-finite measures also cannot be eliminated.\n\n### 5.30 Example Tonelli's Theorem can fail without the hypothesis of $\\sigma$-finite\n\nSuppose $\\mathcal{B}$ is the $\\sigma$-algebra of Borel subsets of $[0,1], \\lambda$ is Lebesgue measure on $([0,1], \\mathcal{B})$, and $\\mu$ is counting measure on $([0,1], \\mathcal{B})$. Let $D$ denote the diagonal of $[0,1] \\times[0,1]$; in other words,\n\n$$\nD=\\{(x, x): x \\in[0,1]\\} .\n$$\n\nThen\n\n$$\n\\int_{[0,1]} \\int_{[0,1]} \\chi_{D}(x, y) d \\mu(y) d \\lambda(x)=\\int_{[0,1]} 1 d \\lambda=1\n$$\n\nbut\n\n$$\n\\int_{[0,1]} \\int_{[0,1]} \\chi_{D}(x, y) d \\lambda(x) d \\mu(y)=\\int_{[0,1]} 0 d \\mu=0 .\n$$\n\nThe following useful corollary of Tonelli's Theorem states that we can switch the order of summation in a double-sum of nonnegative numbers. Exercise 2 asks you to find a double-sum of real numbers in which switching the order of summation changes the value of the double sum.\n\n### 5.31 double sums of nonnegative numbers\n\nIf $\\left\\{x_{j, k}: j, k \\in \\mathbf{Z}^{+}\\right\\}$is a doubly indexed collection of nonnegative numbers, then\n\n$$\n\\sum_{j=1}^{\\infty} \\sum_{k=1}^{\\infty} x_{j, k}=\\sum_{k=1}^{\\infty} \\sum_{j=1}^{\\infty} x_{j, k}\n$$\n\nProof Apply Tonelli's Theorem (5.28) to $\\mu \\times \\mu$, where $\\mu$ is counting measure on $\\mathbf{Z}^{+}$.\n\n## Fubini's Theorem\n\nOur next goal is Fubini's Theorem, which has the same conclusions as Tonelli's Theorem but has a different hypothesis. Tonelli's Theorem requires the function being integrated to be nonnegative. $\\mathrm{Fu}-$ bini's Theorem instead requires the integral of the absolute value of the function to be finite. When using Fubini's Theorem to evaluate the integral of $f$, you will usually first use Tonelli's Theorem as applied to $|f|$ to verify the hypothesis of Fubini's Theorem.\n\nHistorically, Fubini's Theorem (proved in 1907) came before Tonelli's Theorem (proved in 1909). However, presenting Tonelli's Theorem first, as is done here, seems to lead to simpler proofs and better understanding. The hard work here went into proving Tonelli's Theorem, thus our proof of Fubini's Theorem consists mainly of bookkeeping details.\n\nAs you will see in the proof of Fubini's Theorem, the function in 5.32(a) is defined only for almost every $x \\in X$ and the function in 5.32(b) is defined only for almost every $y \\in Y$. For convenience, you can think of these functions as equaling 0 on the sets of measure 0 on which they are otherwise undefined.\n\n### 5.32 Fubini's Theorem\n\nSuppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, \\nu)$ are $\\sigma$-finite measure spaces. Suppose $f: X \\times Y \\rightarrow[-\\infty, \\infty]$ is $\\mathcal{S} \\otimes \\mathcal{T}$-measurable and $\\int_{X \\times Y}|f| d(\\mu \\times v)<\\infty$. Then\n\n$$\n\\int_{Y}|f(x, y)| d v(y)<\\infty \\text { for almost every } x \\in X\n$$\n\nand\n\n$$\n\\int_{X}|f(x, y)| d \\mu(x)<\\infty \\text { for almost every } y \\in Y \\text {. }\n$$\n\nFurthermore,\n\n$$\n\\begin{aligned}\nx & \\mapsto \\int_{Y} f(x, y) d v(y) \\text { is an } \\mathcal{S} \\text {-measurable function on } X, \\\\\ny & \\mapsto \\int_{X} f(x, y) d \\mu(x) \\text { is a } \\mathcal{T} \\text {-measurable function on } Y,\n\\end{aligned}\n$$\n\nand\n\n$$\n\\int_{X \\times Y} f d(\\mu \\times v)=\\int_{X} \\int_{Y} f(x, y) d v(y) d \\mu(x)=\\int_{Y} \\int_{X} f(x, y) d \\mu(x) d v(y) .\n$$\n\nProof Tonelli's Theorem (5.28) applied to the nonnegative function $|f|$ implies that $x \\mapsto \\int_{Y}|f(x, y)| d v(y)$ is an $\\mathcal{S}$-measurable function on $X$. Hence\n\n$$\n\\left\\{x \\in X: \\int_{Y}|f(x, y)| d v(y)=\\infty\\right\\} \\in \\mathcal{S}\n$$\n\nTonelli's Theorem applied to $|f|$ also tells us that\n\n$$\n\\int_{X} \\int_{Y}|f(x, y)| d v(y) d \\mu(x)<\\infty\n$$\n\nbecause the iterated integral above equals $\\int_{X \\times Y}|f| d(\\mu \\times v)$. The inequality above implies that\n\n$$\n\\mu\\left(\\left\\{x \\in X: \\int_{Y}|f(x, y)| d v(y)=\\infty\\right\\}\\right)=0 .\n$$\n\nRecall that $f^{+}$and $f^{-}$are nonnegative $\\mathcal{S} \\otimes \\mathcal{T}$-measurable functions such that $|f|=f^{+}+f^{-}$and $f=f^{+}-f^{-}$(see 3.17). Applying Tonelli's Theorem to $f^{+}$ and $f^{-}$, we see that\n\n$$\nx \\mapsto \\int_{Y} f^{+}(x, y) d v(y) \\quad \\text { and } \\quad x \\mapsto \\int_{Y} f^{-}(x, y) d v(y)\n$$\n\nare $\\mathcal{S}$-measurable functions from $X$ to $[0, \\infty]$. Because $f^{+} \\leq|f|$ and $f^{-} \\leq|f|$, the sets $\\left\\{x \\in X: \\int_{Y} f^{+}(x, y) d v(y)=\\infty\\right\\}$ and $\\left\\{x \\in X: \\int_{Y} f^{-}(x, y) d v(y)=\\infty\\right\\}$ have $\\mu$-measure 0 . Thus the intersection of these two sets, which is the set of $x \\in X$ such that $\\int_{Y} f(x, y) d \\nu(y)$ is not defined, also has $\\mu$-measure 0 .\n\nSubtracting the second function in 5.33 from the first function in 5.33 , we see that the function that we define to be 0 for those $x \\in X$ where we encounter $\\infty-\\infty$ (a set of $\\mu$-measure 0 , as noted above) and that equals $\\int_{Y} f(x, y) d v(y)$ elsewhere is an $\\mathcal{S}$-measurable function on $X$.\n\nNow\n\n$$\n\\begin{aligned}\n\\int_{X \\times Y} f d(\\mu \\times v) & =\\int_{X \\times Y} f^{+} d(\\mu \\times v)-\\int_{X \\times Y} f^{-} d(\\mu \\times v) \\\\\n& =\\int_{X} \\int_{Y} f^{+}(x, y) d v(y) d \\mu(x)-\\int_{X} \\int_{Y} f^{-}(x, y) d v(y) d \\mu(x) \\\\\n& =\\int_{X} \\int_{Y}\\left(f^{+}(x, y)-f^{-}(x, y)\\right) d v(y) d \\mu(x) \\\\\n& =\\int_{X} \\int_{Y} f(x, y) d v(y) d \\mu(x),\n\\end{aligned}\n$$\n\nwhere the first line above comes from the definition of the integral of a function that is not nonnegative (note that neither of the two terms on the right side of the first line equals $\\infty$ because $\\int_{X \\times Y}|f| d(\\mu \\times v)<\\infty$ ) and the second line comes from applying Tonelli's Theorem to $f^{+}$and $f^{-}$.\n\nWe have now proved all aspects of Fubini's Theorem that involve integrating first over $Y$. The same procedure provides proofs for the aspects of Fubini's theorem that involve integrating first over $X$.\n\n## Area Under Graph\n\n### 5.34 Definition region under the graph; $U_{f}$\n\nSuppose $X$ is a set and $f: X \\rightarrow[0, \\infty]$ is a function. Then the region under the graph of $f$, denoted $U_{f}$, is defined by\n\n$$\nU_{f}=\\{(x, t) \\in X \\times(0, \\infty): 0<t<f(x)\\} .\n$$\n\nR\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-148.jpg?height=327&width=539&top_left_y=1806&top_left_x=117)\n\nThe figure indicates why we call $U_{f}$ the region under the graph of $f$, even in cases when $X$ is not a subset of $\\mathbf{R}$. Similarly, the informal term area in the next paragraph should remind you of the area in the figure, even though we are really dealing with the measure of $U_{f}$ in a product space.\n\nThe first equality in the result below can be thought of as recovering Riemann's conception of the integral as the area under the graph (although now in a much more general context with arbitrary $\\sigma$-finite measures). The second equality in the result below can be thought of as reinforcing Lebesgue's conception of computing the area under a curve by integrating in the direction perpendicular to Riemann's.\n\n### 5.35 area under the graph of a function equals the integral\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a $\\sigma$-finite measure space and $f: X \\rightarrow[0, \\infty]$ is an $\\mathcal{S}$-measurable function. Let $\\mathcal{B}$ denote the $\\sigma$-algebra of Borel subsets of $(0, \\infty)$, and let $\\lambda$ denote Lebesgue measure on $((0, \\infty), \\mathcal{B})$. Then $U_{f} \\in \\mathcal{S} \\otimes \\mathcal{B}$ and\n\n$$\n(\\mu \\times \\lambda)\\left(U_{f}\\right)=\\int_{X} f d \\mu=\\int_{(0, \\infty)} \\mu(\\{x \\in X: t<f(x)\\}) d \\lambda(t)\n$$\n\nProof For $k \\in \\mathbf{Z}^{+}$, let\n\n$$\nE_{k}=\\bigcup_{m=0}^{k^{2}-1}\\left(f^{-1}\\left(\\left[\\frac{m}{k}, \\frac{m+1}{k}\\right)\\right) \\times\\left(0, \\frac{m}{k}\\right)\\right) \\quad \\text { and } \\quad F_{k}=f^{-1}([k, \\infty]) \\times(0, k) \\text {. }\n$$\n\nThen $E_{k}$ is a finite union of $\\mathcal{S} \\otimes \\mathcal{B}$-measurable rectangles and $F_{k}$ is an $\\mathcal{S} \\otimes \\mathcal{B}$ measurable rectangle. Because\n\n$$\nU_{f}=\\bigcup_{k=1}^{\\infty}\\left(E_{k} \\cup F_{k}\\right)\n$$\n\nwe conclude that $U_{f} \\in \\mathcal{S} \\otimes \\mathcal{B}$.\n\nNow the definition of the product measure $\\mu \\times \\lambda$ implies that\n\n$$\n\\begin{aligned}\n(\\mu \\times \\lambda)\\left(U_{f}\\right) & =\\int_{X} \\int_{(0, \\infty)} \\chi_{U_{f}}(x, t) d \\lambda(t) d \\mu(x) \\\\\n& =\\int_{X} f(x) d \\mu(x),\n\\end{aligned}\n$$\n\nwhich completes the proof of the first equality in the conclusion of this theorem.\n\nTonelli's Theorem (5.28) tells us that we can interchange the order of integration in the double integral above, getting\n\n$$\n\\begin{aligned}\n(\\mu \\times \\lambda)\\left(U_{f}\\right) & =\\int_{(0, \\infty)} \\int_{X} \\chi_{U_{f}}(x, t) d \\mu(x) d \\lambda(t) \\\\\n& =\\int_{(0, \\infty)} \\mu(\\{x \\in X: t<f(x)\\}) d \\lambda(t)\n\\end{aligned}\n$$\n\nwhich completes the proof of the second equality in the conclusion of this theorem.\n\nMarkov's inequality (4.1) implies that if $f$ and $\\mu$ are as in the result above, then\n\n$$\n\\mu(\\{x \\in X: f(x)>t\\}) \\leq \\frac{\\int_{X} f d \\mu}{t}\n$$\n\nfor all $t>0$. Thus if $\\int_{X} f d \\mu<\\infty$, then the result above should be considered to be somewhat stronger than Markov's inequality (because $\\int_{(0, \\infty)} \\frac{1}{t} d \\lambda(t)=\\infty$ ).\n\n## EXERCISES 5B\n\n1 (a) Let $\\lambda$ denote Lebesgue measure on $[0,1]$. Show that\n\n$$\n\\int_{[0,1]} \\int_{[0,1]} \\frac{x^{2}-y^{2}}{\\left(x^{2}+y^{2}\\right)^{2}} d \\lambda(y) d \\lambda(x)=\\frac{\\pi}{4}\n$$\n\nand\n\n$$\n\\int_{[0,1]} \\int_{[0,1]} \\frac{x^{2}-y^{2}}{\\left(x^{2}+y^{2}\\right)^{2}} d \\lambda(x) d \\lambda(y)=-\\frac{\\pi}{4}\n$$\n\n(b) Explain why (a) violates neither Tonelli's Theorem nor Fubini's Theorem.\n\n2 (a) Give an example of a doubly indexed collection $\\left\\{x_{m, n}: m, n \\in \\mathbf{Z}^{+}\\right\\}$of real numbers such that\n\n$$\n\\sum_{m=1}^{\\infty} \\sum_{n=1}^{\\infty} x_{m, n}=0 \\quad \\text { and } \\quad \\sum_{n=1}^{\\infty} \\sum_{m=1}^{\\infty} x_{m, n}=\\infty\n$$\n\n(b) Explain why (a) violates neither Tonelli's Theorem nor Fubini's Theorem.\n\n3 Suppose $(X, \\mathcal{S})$ is a measurable space and $f: X \\rightarrow[0, \\infty]$ is a function. Let $\\mathcal{B}$ denote the $\\sigma$-algebra of Borel subsets of $(0, \\infty)$. Prove that $U_{f} \\in \\mathcal{S} \\otimes \\mathcal{B}$ if and only if $f$ is an $\\mathcal{S}$-measurable function.\n\n4 Suppose $(X, \\mathcal{S})$ is a measurable space and $f: X \\rightarrow \\mathbf{R}$ is a function. Let $\\operatorname{graph}(f) \\subset X \\times \\mathbf{R}$ denote the graph of $f:$\n\n$$\n\\operatorname{graph}(f)=\\{(x, f(x)): x \\in X\\} \\text {. }\n$$\n\nLet $\\mathcal{B}$ denote the $\\sigma$-algebra of Borel subsets of $\\mathbf{R}$. Prove that graph $(f) \\in \\mathcal{S} \\otimes \\mathcal{B}$ if $f$ is an $\\mathcal{S}$-measurable function.\n\n## $5 C$ Lebesgue Integration on $\\mathbf{R}^{n}$\n\nThroughout this section, assume that $m$ and $n$ are positive integers. Thus, for example, 5.36 should include the hypothesis that $m$ and $n$ are positive integers, but theorems and definitions become easier to state without explicitly repeating this hypothesis.\n\n## Borel Subsets of $\\mathbf{R}^{n}$\n\nWe begin with a quick review of notation and key concepts concerning $\\mathbf{R}^{n}$.\n\nRecall that $\\mathbf{R}^{n}$ is the set of all $n$-tuples of real numbers:\n\n$$\n\\mathbf{R}^{n}=\\left\\{\\left(x_{1}, \\ldots, x_{n}\\right): x_{1}, \\ldots, x_{n} \\in \\mathbf{R}\\right\\}\n$$\n\nThe function $\\|\\cdot\\|_{\\infty}$ from $\\mathbf{R}^{n}$ to $[0, \\infty)$ is defined by\n\n$$\n\\left\\|\\left(x_{1}, \\ldots, x_{n}\\right)\\right\\|_{\\infty}=\\max \\left\\{\\left|x_{1}\\right|, \\ldots,\\left|x_{n}\\right|\\right\\}\n$$\n\nFor $x \\in \\mathbf{R}^{n}$ and $\\delta>0$, the open cube $B(x, \\delta)$ with side length $2 \\delta$ is defined by\n\n$$\nB(x, \\delta)=\\left\\{y \\in \\mathbf{R}^{n}:\\|y-x\\|_{\\infty}<\\delta\\right\\} .\n$$\n\nIf $n=1$, then an open cube is simply a bounded open interval. If $n=2$, then an open cube might more appropriately be called an open square. However, using the cube terminology for all dimensions has the advantage of not requiring a different word for different dimensions.\n\nA subset $G$ of $\\mathbf{R}^{n}$ is called open if for every $x \\in G$, there exists $\\delta>0$ such that $B(x, \\delta) \\subset G$. Equivalently, a subset $G$ of $\\mathbf{R}^{n}$ is called open if every element of $G$ is contained in an open cube that is contained in $G$.\n\nThe union of every collection (finite or infinite) of open subsets of $\\mathbf{R}^{n}$ is an open subset of $\\mathbf{R}^{n}$. Also, the intersection of every finite collection of open subsets of $\\mathbf{R}^{n}$ is an open subset of $\\mathbf{R}^{n}$.\n\nA subset of $\\mathbf{R}^{n}$ is called closed if its complement in $\\mathbf{R}^{n}$ is open. A set $A \\subset \\mathbf{R}^{n}$ is called bounded if $\\sup \\left\\{\\|a\\|_{\\infty}: a \\in A\\right\\}<\\infty$.\n\nWe adopt the following common convention:\n\n$$\n\\mathbf{R}^{m} \\times \\mathbf{R}^{n} \\text { is identified with } \\mathbf{R}^{m+n} \\text {. }\n$$\n\nTo understand the necessity of this convention, note that $\\mathbf{R}^{2} \\times \\mathbf{R} \\neq \\mathbf{R}^{3}$ because $\\mathbf{R}^{2} \\times \\mathbf{R}$ and $\\mathbf{R}^{3}$ contain different kinds of objects. Specifically, an element of $\\mathbf{R}^{2} \\times \\mathbf{R}$ is an ordered pair, the first of which is an element of $\\mathbf{R}^{2}$ and the second of which is an element of $\\mathbf{R}$; thus an element of $\\mathbf{R}^{2} \\times \\mathbf{R}$ looks like $\\left(\\left(x_{1}, x_{2}\\right), x_{3}\\right)$. An element of $\\mathbf{R}^{3}$ is an ordered triple of real numbers that looks like $\\left(x_{1}, x_{2}, x_{3}\\right)$. However, we can identify $\\left(\\left(x_{1}, x_{2}\\right), x_{3}\\right)$ with $\\left(x_{1}, x_{2}, x_{3}\\right)$ in the obvious way. Thus we say that $\\mathbf{R}^{2} \\times \\mathbf{R}$ \"equals\" $\\mathbf{R}^{3}$. More generally, we make the natural identification of $\\mathbf{R}^{m} \\times \\mathbf{R}^{n}$ with $\\mathbf{R}^{m+n}$.\n\nTo check that you understand the identification discussed above, make sure that you see why $B(x, \\delta) \\times B(y, \\delta)=B((x, y), \\delta)$ for all $x \\in \\mathbf{R}^{m}, y \\in \\mathbf{R}^{n}$, and $\\delta>0$.\n\nWe can now prove that the product of two open sets is an open set.\n\n### 5.36 product of open sets is open\n\nSuppose $G_{1}$ is an open subset of $\\mathbf{R}^{m}$ and $G_{2}$ is an open subset of $\\mathbf{R}^{n}$. Then $G_{1} \\times G_{2}$ is an open subset of $\\mathbf{R}^{m+n}$.\n\nProof Suppose $(x, y) \\in G_{1} \\times G_{2}$. Then there exists an open cube $D$ in $\\mathbf{R}^{m}$ centered at $x$ and an open cube $E$ in $\\mathbf{R}^{n}$ centered at $y$ such that $D \\subset G_{1}$ and $E \\subset G_{2}$. By reducing the size of either $D$ or $E$, we can assume that the cubes $D$ and $E$ have the same side length. Thus $D \\times E$ is an open cube in $\\mathbf{R}^{m+n}$ centered at $(x, y)$ that is contained in $G_{1} \\times G_{2}$.\n\nWe have shown that an arbitrary point in $G_{1} \\times G_{2}$ is the center of an open cube contained in $G_{1} \\times G_{2}$. Hence $G_{1} \\times G_{2}$ is an open subset of $\\mathbf{R}^{m+n}$.\n\nWhen $n=1$, the definition below of a Borel subset of $\\mathbf{R}^{1}$ agrees with our previous definition (2.29) of a Borel subset of $\\mathbf{R}$.\n\n### 5.37 Definition Borel set; $\\mathcal{B}_{n}$\n\n- A Borel subset of $\\mathbf{R}^{n}$ is an element of the smallest $\\sigma$-algebra on $\\mathbf{R}^{n}$ containing all open subsets of $\\mathbf{R}^{n}$.\n- The $\\sigma$-algebra of Borel subsets of $\\mathbf{R}^{n}$ is denoted by $\\mathcal{B}_{n}$.\n\nRecall that a subset of $\\mathbf{R}$ is open if and only if it is a countable disjoint union of open intervals. Part (a) in the result below provides a similar result in $\\mathbf{R}^{n}$, although we must give up the disjoint aspect.\n\n### 5.38 open sets are countable unions of open cubes\n\n(a) A subset of $\\mathbf{R}^{n}$ is open in $\\mathbf{R}^{n}$ if and only if it is a countable union of open cubes in $\\mathbf{R}^{n}$.\n\n(b) $\\mathcal{B}_{n}$ is the smallest $\\sigma$-algebra on $\\mathbf{R}^{n}$ containing all the open cubes in $\\mathbf{R}^{n}$.\n\nProof We will prove (a), which clearly implies (b).\n\nThe proof that a countable union of open cubes is open is left as an exercise for the reader (actually, arbitrary unions of open cubes are open).\n\nTo prove the other direction, suppose $G$ is an open subset of $\\mathbf{R}^{n}$. For each $x \\in G$, there is an open cube centered at $x$ that is contained in $G$. Thus there is a smaller cube $C_{x}$ such that $x \\in C_{x} \\subset G$ and all coordinates of the center of $C_{x}$ are rational numbers and the side length of $C_{x}$ is a rational number. Now\n\n$$\nG=\\bigcup_{x \\in G} C_{x} .\n$$\n\nHowever, there are only countably many distinct cubes whose center has all rational coordinates and whose side length is rational. Thus $G$ is the countable union of open cubes.\n\nThe next result tells us that the collection of Borel sets from various dimensions fit together nicely.\n\n### 5.39 product of the Borel subsets of $\\mathbf{R}^{m}$ and the Borel subsets of $\\mathbf{R}^{n}$\n\n$\\mathcal{B}_{m} \\otimes \\mathcal{B}_{n}=\\mathcal{B}_{m+n}$\n\nProof Suppose $E$ is an open cube in $\\mathbf{R}^{m+n}$. Thus $E$ is the product of an open cube in $\\mathbf{R}^{m}$ and an open cube in $\\mathbf{R}^{n}$. Hence $E \\in \\mathcal{B}_{m} \\otimes \\mathcal{B}_{n}$. Thus the smallest $\\sigma$-algebra containing all the open cubes in $\\mathbf{R}^{m+n}$ is contained in $\\mathcal{B}_{m} \\otimes \\mathcal{B}_{n}$. Now 5.38(b) implies that $\\mathcal{B}_{m+n} \\subset \\mathcal{B}_{m} \\otimes \\mathcal{B}_{n}$.\n\nTo prove the set inclusion in the other direction, temporarily fix an open set $G$ in $\\mathbf{R}^{n}$. Let\n\n$$\n\\mathcal{E}=\\left\\{A \\subset \\mathbf{R}^{m}: A \\times G \\in \\mathcal{B}_{m+n}\\right\\} .\n$$\n\nThen $\\mathcal{E}$ contains every open subset of $\\mathbf{R}^{m}$ (as follows from 5.36). Also, $\\mathcal{E}$ is closed under countable unions because\n\n$$\n\\left(\\bigcup_{k=1}^{\\infty} A_{k}\\right) \\times G=\\bigcup_{k=1}^{\\infty}\\left(A_{k} \\times G\\right)\n$$\n\nFurthermore, $\\mathcal{E}$ is closed under complementation because\n\n$$\n\\left(\\mathbf{R}^{m} \\backslash A\\right) \\times G=\\left(\\left(\\mathbf{R}^{m} \\times \\mathbf{R}^{n}\\right) \\backslash(A \\times G)\\right) \\cap\\left(\\mathbf{R}^{m} \\times G\\right)\n$$\n\nThus $\\mathcal{E}$ is a $\\sigma$-algebra on $\\mathbf{R}^{m}$ that contains all open subsets of $\\mathbf{R}^{m}$, which implies that $\\mathcal{B}_{m} \\subset \\mathcal{E}$. In other words, we have proved that if $A \\in \\mathcal{B}_{m}$ and $G$ is an open subset of $\\mathbf{R}^{n}$, then $A \\times G \\in \\mathcal{B}_{m+n}$.\n\nNow temporarily fix a Borel subset $A$ of $\\mathbf{R}^{m}$. Let\n\n$$\n\\mathcal{F}=\\left\\{B \\subset \\mathbf{R}^{n}: A \\times B \\in \\mathcal{B}_{m+n}\\right\\}\n$$\n\nThe conclusion of the previous paragraph shows that $\\mathcal{F}$ contains every open subset of $\\mathbf{R}^{n}$. As in the previous paragraph, we also see that $\\mathcal{F}$ is a $\\sigma$-algebra. Hence $\\mathcal{B}_{n} \\subset \\mathcal{F}$. In other words, we have proved that if $A \\in \\mathcal{B}_{m}$ and $B \\in \\mathcal{B}_{n}$, then $A \\times B \\in \\mathcal{B}_{m+n}$. Thus $\\mathcal{B}_{m} \\otimes \\mathcal{B}_{n} \\subset \\mathcal{B}_{m+n}$, completing the proof.\n\nThe previous result implies a nice associative property. Specifically, if $m, n$, and $p$ are positive integers, then two applications of 5.39 give\n\n$$\n\\left(\\mathcal{B}_{m} \\otimes \\mathcal{B}_{n}\\right) \\otimes \\mathcal{B}_{p}=\\mathcal{B}_{m+n} \\otimes \\mathcal{B}_{p}=\\mathcal{B}_{m+n+p}\n$$\n\nSimilarly, two more applications of 5.39 give\n\n$$\n\\mathcal{B}_{m} \\otimes\\left(\\mathcal{B}_{n} \\otimes \\mathcal{B}_{p}\\right)=\\mathcal{B}_{m} \\otimes \\mathcal{B}_{n+p}=\\mathcal{B}_{m+n+p}\n$$\n\nThus $\\left(\\mathcal{B}_{m} \\otimes \\mathcal{B}_{n}\\right) \\otimes \\mathcal{B}_{p}=\\mathcal{B}_{m} \\otimes\\left(\\mathcal{B}_{n} \\otimes \\mathcal{B}_{p}\\right)$; hence we can dispense with parentheses when taking products of more than two Borel $\\sigma$-algebras. More generally, we could have defined $\\mathcal{B}_{m} \\otimes \\mathcal{B}_{n} \\otimes \\mathcal{B}_{p}$ directly as the smallest $\\sigma$-algebra on $\\mathbf{R}^{m+n+p}$ containing $\\left\\{A \\times B \\times C: A \\in \\mathcal{B}_{m}, B \\in \\mathcal{B}_{n}, C \\in \\mathcal{B}_{p}\\right\\}$ and obtained the same $\\sigma$-algebra (see Exercise 3 in this section).\n\n## Lebesgue Measure on $\\mathbf{R}^{n}$\n\n### 5.40 Definition Lebesgue measure; $\\lambda_{n}$\n\nLebesgue measure on $\\mathbf{R}^{n}$ is denoted by $\\lambda_{n}$ and is defined inductively by\n\n$$\n\\lambda_{n}=\\lambda_{n-1} \\times \\lambda_{1},\n$$\n\nwhere $\\lambda_{1}$ is Lebesgue measure on $\\left(\\mathbf{R}, \\mathcal{B}_{1}\\right)$.\n\nBecause $\\mathcal{B}_{n}=\\mathcal{B}_{n-1} \\otimes \\mathcal{B}_{1}$ (by 5.39), the measure $\\lambda_{n}$ is defined on the Borel subsets of $\\mathbf{R}^{n}$. Thinking of a typical point in $\\mathbf{R}^{n}$ as $(x, y)$, where $x \\in \\mathbf{R}^{n-1}$ and $y \\in \\mathbf{R}$, we can use the definition of the product of two measures (5.25) to write\n\n$$\n\\lambda_{n}(E)=\\int_{\\mathbf{R}^{n-1}} \\int_{\\mathbf{R}} \\chi_{E}(x, y) d \\lambda_{1}(y) d \\lambda_{n-1}(x)\n$$\n\nfor $E \\in \\mathcal{B}_{n}$. Of course, we could use Tonelli's Theorem (5.28) to interchange the order of integration in the equation above.\n\nBecause Lebesgue measure is the most commonly used measure, mathematicians often dispense with explicitly displaying the measure and just use a variable name. In other words, if no measure is explicitly displayed in an integral and the context indicates no other measure, then you should assume that the measure involved is Lebesgue measure in the appropriate dimension. For example, the result of interchanging the order of integration in the equation above could be written as\n\n$$\n\\lambda_{n}(E)=\\int_{\\mathbf{R}} \\int_{\\mathbf{R}^{n-1}} \\chi_{E}(x, y) d x d y\n$$\n\nfor $E \\in \\mathcal{B}_{n}$; here $d x$ means $d \\lambda_{n-1}(x)$ and $d y$ means $d \\lambda_{1}(y)$.\n\nIn the equations above giving formulas for $\\lambda_{n}(E)$, the integral over $\\mathbf{R}^{n-1}$ could be rewritten as an iterated integral over $\\mathbf{R}^{n-2}$ and $\\mathbf{R}$, and that process could be repeated until reaching iterated integrals only over $\\mathbf{R}$. Tonelli's Theorem could then be used repeatedly to swap the order of pairs of those integrated integrals, leading to iterated integrals in any order.\n\nSimilar comments apply to integrating functions on $\\mathbf{R}^{n}$ other than characteristic functions. For example, if $f: \\mathbf{R}^{3} \\rightarrow \\mathbf{R}$ is a $\\mathcal{B}_{3}$-measurable function such that either $f \\geq 0$ or $\\int_{\\mathbf{R}^{3}}|f| d \\lambda_{3}<\\infty$, then by either Tonelli's Theorem or Fubini's Theorem we have\n\n$$\n\\int_{\\mathbf{R}^{3}} f d \\lambda_{3}=\\int_{\\mathbf{R}} \\int_{\\mathbf{R}} \\int_{\\mathbf{R}} f\\left(x_{1}, x_{2}, x_{3}\\right) d x_{j} d x_{k} d x_{m},\n$$\n\nwhere $j, k, m$ is any permutation of $1,2,3$.\n\nAlthough we defined $\\lambda_{n}$ to be $\\lambda_{n-1} \\times \\lambda_{1}$, we could have defined $\\lambda_{n}$ to be $\\lambda_{j} \\times \\lambda_{k}$ for any positive integers $j, k$ with $j+k=n$. This potentially different definition would have led to the same $\\sigma$-algebra $\\mathcal{B}_{n}$ (by 5.39) and to the same measure $\\lambda_{n}$ [because both potential definitions of $\\lambda_{n}(E)$ can be written as identical iterations of $n$ integrals with respect to $\\lambda_{1}$ ].\n\n## Volume of Unit Ball in $\\mathbf{R}^{n}$\n\nThe proof of the next result provides good experience in working with the Lebesgue measure $\\lambda_{n}$. Recall that $t E=\\{t x: x \\in E\\}$.\n\n### 5.41 measure of a dilation\n\nSuppose $t>0$. If $E \\in \\mathcal{B}_{n}$, then $t E \\in \\mathcal{B}_{n}$ and $\\lambda_{n}(t E)=t^{n} \\lambda_{n}(E)$.\n\nProof Let\n\n$$\n\\mathcal{E}=\\left\\{E \\in \\mathcal{B}_{n}: t E \\in \\mathcal{B}_{n}\\right\\}\n$$\n\nThen $\\mathcal{E}$ contains every open subset of $\\mathbf{R}^{n}$ (because if $E$ is open in $\\mathbf{R}^{n}$ then $t E$ is open in $\\mathbf{R}^{n}$ ). Also, $\\mathcal{E}$ is closed under complementation and countable unions because\n\n$$\nt\\left(\\mathbf{R}^{n} \\backslash E\\right)=\\mathbf{R}^{n} \\backslash(t E) \\text { and } t\\left(\\bigcup_{k=1}^{\\infty} E_{k}\\right)=\\bigcup_{k=1}^{\\infty}\\left(t E_{k}\\right)\n$$\n\nHence $\\mathcal{E}$ is a $\\sigma$-algebra on $\\mathbf{R}^{n}$ containing the open subsets of $\\mathbf{R}^{n}$. Thus $\\mathcal{E}=\\mathcal{B}_{n}$. In other words, $t E \\in \\mathcal{B}_{n}$ for all $E \\in \\mathcal{B}_{n}$.\n\nTo prove $\\lambda_{n}(t E)=t^{n} \\lambda_{n}(E)$, first consider the case $n=1$. Lebesgue measure on $\\mathbf{R}$ is a restriction of outer measure. The outer measure of a set is determined by the sum of the lengths of countable collections of intervals whose union contains the set. Multiplying the set by $t$ corresponds to multiplying each such interval by $t$, which multiplies the length of each such interval by $t$. In other words, $\\lambda_{1}(t E)=t \\lambda_{1}(E)$.\n\nNow assume $n>1$. We will use induction on $n$ and assume that the desired result holds for $n-1$. If $A \\in \\mathcal{B}_{n-1}$ and $B \\in \\mathcal{B}_{1}$, then\n\n5.42\n\n$$\n\\begin{aligned}\n\\lambda_{n}(t(A \\times B)) & =\\lambda_{n}((t A) \\times(t B)) \\\\\n& =\\lambda_{n-1}(t A) \\cdot \\lambda_{1}(t B) \\\\\n& =t^{n-1} \\lambda_{n-1}(A) \\cdot t \\lambda_{1}(B) \\\\\n& =t^{n} \\lambda_{n}(A \\times B),\n\\end{aligned}\n$$\n\ngiving the desired result for $A \\times B$.\n\nFor $m \\in \\mathbf{Z}^{+}$, let $C_{m}$ be the open cube in $\\mathbf{R}^{n}$ centered at the origin and with side length $m$. Let\n\n$$\n\\mathcal{E}_{m}=\\left\\{E \\in \\mathcal{B}_{n}: E \\subset C_{m} \\text { and } \\lambda_{n}(t E)=t^{n} \\lambda_{n}(E)\\right\\} \\text {. }\n$$\n\nFrom 5.42 and using 5.13(b), we see that finite unions of measurable rectangles contained in $C_{m}$ are in $\\mathcal{E}_{m}$. You should verify that $\\mathcal{E}_{m}$ is closed under countable increasing unions (use 2.59) and countable decreasing intersections (use 2.60, whose finite measure condition holds because we are working inside $C_{m}$ ). From 5.13 and the Monotone Class Theorem (5.17), we conclude that $\\mathcal{E}_{m}$ is the $\\sigma$-algebra on $C_{m}$ consisting of Borel subsets of $C_{m}$. Thus $\\lambda_{n}(t E)=t^{n} \\lambda_{n}(E)$ for all $E \\in \\mathcal{B}_{n}$ such that $E \\subset C_{m}$.\n\nNow suppose $E \\in \\mathcal{B}_{n}$. Then 2.59 implies that\n\n$$\n\\lambda_{n}(t E)=\\lim _{m \\rightarrow \\infty} \\lambda_{n}\\left(t\\left(E \\cap C_{m}\\right)\\right)=t^{n} \\lim _{m \\rightarrow \\infty} \\lambda_{n}\\left(E \\cap C_{m}\\right)=t^{n} \\lambda_{n}(E)\n$$\n\nas desired.\n\n5.43 Definition open unit ball in $\\mathbf{R}^{n} ; \\mathbf{B}_{n}$\n\nThe open unit ball in $\\mathbf{R}^{n}$ is denoted by $\\mathbf{B}_{n}$ and is defined by\n\n$$\n\\mathbf{B}_{n}=\\left\\{\\left(x_{1}, \\ldots, x_{n}\\right) \\in \\mathbf{R}^{n}: x_{1}{ }^{2}+\\cdots+x_{n}{ }^{2}<1\\right\\} .\n$$\n\nThe open unit ball $\\mathbf{B}_{n}$ is open in $\\mathbf{R}^{n}$ (as you should verify) and thus is in the collection $\\mathcal{B}_{n}$ of Borel sets.\n\n### 5.44 volume of the unit ball in $\\mathbf{R}^{n}$\n\n$$\n\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)= \\begin{cases}\\frac{\\pi^{n / 2}}{(n / 2) !} & \\text { if } n \\text { is even, } \\\\ \\frac{2^{(n+1) / 2} \\pi^{(n-1) / 2}}{1 \\cdot 3 \\cdot 5 \\cdots \\cdot n} & \\text { if } n \\text { is odd. }\\end{cases}\n$$\n\nProof Because $\\lambda_{1}\\left(\\mathbf{B}_{1}\\right)=2$ and $\\lambda_{2}\\left(\\mathbf{B}_{2}\\right)=\\pi$, the claimed formula is correct when $n=1$ and when $n=2$.\n\nNow assume that $n>2$. We will use induction on $n$, assuming that the claimed formula is true for smaller values of $n$. Think of $\\mathbf{R}^{n}=\\mathbf{R}^{2} \\times \\mathbf{R}^{n-2}$ and $\\lambda_{n}=\\lambda_{2} \\times \\lambda_{n-2}$. Then\n\n$$\n\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)=\\int_{\\mathbf{R}^{2}} \\int_{\\mathbf{R}^{n-2}} \\chi_{\\mathbf{B}_{n}}(x, y) d y d x\n$$\n\nTemporarily fix $x=\\left(x_{1}, x_{2}\\right) \\in \\mathbf{R}^{2}$. If $x_{1}{ }^{2}+x_{2}{ }^{2} \\geq 1$, then $\\chi_{\\mathbf{B}_{n}}(x, y)=0$ for all $y \\in \\mathbf{R}^{n-2}$. If $x_{1}{ }^{2}+x_{2}{ }^{2}<1$ and $y \\in \\mathbf{R}^{n-2}$, then $\\chi_{\\mathbf{B}_{n}}(x, y)=1$ if and only if $y \\in\\left(1-x_{1}{ }^{2}-x_{2}{ }^{2}\\right)^{1 / 2} \\mathbf{B}_{n-2}$. Thus the inner integral in 5.45 equals\n\n$$\n\\lambda_{n-2}\\left(\\left(1-x_{1}^{2}-x_{2}^{2}\\right)^{1 / 2} \\mathbf{B}_{n-2}\\right) \\chi_{\\mathbf{B}_{2}}(x),\n$$\n\nwhich by 5.41 equals\n\n$$\n\\left(1-x_{1}^{2}-x_{2}^{2}\\right)^{(n-2) / 2} \\lambda_{n-2}\\left(\\mathbf{B}_{n-2}\\right) \\chi_{\\mathbf{B}_{2}}(x) .\n$$\n\nThus 5.45 becomes the equation\n\n$$\n\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)=\\lambda_{n-2}\\left(\\mathbf{B}_{n-2}\\right) \\int_{\\mathbf{B}_{2}}\\left(1-x_{1}^{2}-x_{2}^{2}\\right)^{(n-2) / 2} d \\lambda_{2}\\left(x_{1}, x_{2}\\right)\n$$\n\nTo evaluate this integral, switch to the usual polar coordinates that you learned about in calculus $\\left(d \\lambda_{2}=r d r d \\theta\\right)$, getting\n\n$$\n\\begin{aligned}\n\\lambda_{n}\\left(\\mathbf{B}_{n}\\right) & =\\lambda_{n-2}\\left(\\mathbf{B}_{n-2}\\right) \\int_{-\\pi}^{\\pi} \\int_{0}^{1}\\left(1-r^{2}\\right)^{(n-2) / 2} r d r d \\theta \\\\\n& =\\frac{2 \\pi}{n} \\lambda_{n-2}\\left(\\mathbf{B}_{n-2}\\right) .\n\\end{aligned}\n$$\n\nThe last equation and the induction hypothesis give the desired result.\n\nThis table gives the first five values of $\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)$, using 5.44. The last column of this table gives a decimal approximation to $\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)$, accurate to two digits after the decimal point. From this table, you might guess that $\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)$ is an increasing function of $n$, especially because the smallest cube containing the ball $\\mathbf{B}_{n}$ has $n$ dimensional Lebesgue measure $2^{n}$. However, Exercise 12 in this section shows that $\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)$ behaves much differently.\n\n| $n$ | $\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)$ | $\\approx \\lambda_{n}\\left(\\mathbf{B}_{n}\\right)$ |\n| :-: | :--------------------------------------: | :----------------------------------------------: |\n|  1  |                    2                     |                       2.00                       |\n|  2  |                  $\\pi$                   |                       3.14                       |\n|  3  |               $4 \\pi / 3$                |                       4.19                       |\n|  4  |              $\\pi^{2} / 2$               |                       4.93                       |\n|  5  |             $8 \\pi^{2} / 15$             |                       5.26                       |\n\n## Equality of Mixed Partial Derivatives Via Fubini's Theorem\n\n5.46 Definition partial derivatives; $D_{1} f$ and $D_{2} f$\n\nSuppose $G$ is an open subset of $\\mathbf{R}^{2}$ and $f: G \\rightarrow \\mathbf{R}$ is a function. For $(x, y) \\in G$, the partial derivatives $\\left(D_{1} f\\right)(x, y)$ and $\\left(D_{2} f\\right)(x, y)$ are defined by\n\n$$\n\\left(D_{1} f\\right)(x, y)=\\lim _{t \\rightarrow 0} \\frac{f(x+t, y)-f(x, y)}{t}\n$$\n\nand\n\n$$\n\\left(D_{2} f\\right)(x, y)=\\lim _{t \\rightarrow 0} \\frac{f(x, y+t)-f(x, y)}{t}\n$$\n\nif these limits exist.\n\nUsing the notation for the cross section of a function (see 5.7), we could write the definitions of $D_{1}$ and $D_{2}$ in the following form:\n\n$$\n\\left(D_{1} f\\right)(x, y)=\\left([f]^{y}\\right)^{\\prime}(x) \\quad \\text { and } \\quad\\left(D_{2} f\\right)(x, y)=\\left([f]_{x}\\right)^{\\prime}(y)\n$$\n\n### 5.47 Example partial derivatives of $x^{y}$\n\nLet $G=\\left\\{(x, y) \\in \\mathbf{R}^{2}: x>0\\right\\}$ and define $f: G \\rightarrow \\mathbf{R}$ by $f(x, y)=x^{y}$. Then\n\n$$\n\\left(D_{1} f\\right)(x, y)=y x^{y-1} \\quad \\text { and } \\quad\\left(D_{2} f\\right)(x, y)=x^{y} \\ln x\n$$\n\nas you should verify. Taking partial derivatives of those partial derivatives, we have\n\n$$\n\\left(D_{2}\\left(D_{1} f\\right)\\right)(x, y)=x^{y-1}+y x^{y-1} \\ln x\n$$\n\nand\n\n$$\n\\left(D_{1}\\left(D_{2} f\\right)\\right)(x, y)=x^{y-1}+y x^{y-1} \\ln x\n$$\n\nas you should also verify. The last two equations show that $D_{1}\\left(D_{2} f\\right)=D_{2}\\left(D_{1} f\\right)$ as functions on $G$.\n\nIn the example above, the two mixed partial derivatives turn out to equal each other, even though the intermediate results look quite different. The next result shows that the behavior in the example above is typical rather than a coincidence.\n\nSome proofs of the result below do not use Fubini's Theorem. However, Fubini's Theorem leads to the clean proof below.\n\nThe integrals that appear in the proof below make sense because continuous real-valued functions on $\\mathbf{R}^{2}$ are measurable (because for a continuous function, the inverse image of each open set is open) and because continuous real-valued func-\n\nAlthough the continuity hypotheses in the result below can be slightly weakened, they cannot be eliminated, as shown by Exercise 14 in this section. tions on closed bounded subsets of $\\mathbf{R}^{2}$ are bounded.\n\n### 5.48 equality of mixed partial derivatives\n\nSuppose $G$ is an open subset of $\\mathbf{R}^{2}$ and $f: G \\rightarrow \\mathbf{R}$ is a function such that $D_{1} f$, $D_{2} f, D_{1}\\left(D_{2} f\\right)$, and $D_{2}\\left(D_{1} f\\right)$ all exist and are continuous functions on $G$. Then\n\n$$\nD_{1}\\left(D_{2} f\\right)=D_{2}\\left(D_{1} f\\right)\n$$\n\non $G$.\n\nProof Fix $(a, b) \\in G$. For $\\delta>0$, let $S_{\\delta}=[a, a+\\delta] \\times[b, b+\\delta]$. If $S_{\\delta} \\subset G$, then\n\n$$\n\\begin{aligned}\n\\int_{S_{\\delta}} D_{1}\\left(D_{2} f\\right) d \\lambda_{2} & =\\int_{b}^{b+\\delta} \\int_{a}^{a+\\delta}\\left(D_{1}\\left(D_{2} f\\right)\\right)(x, y) d x d y \\\\\n& =\\int_{b}^{b+\\delta}\\left[\\left(D_{2} f\\right)(a+\\delta, y)-\\left(D_{2} f\\right)(a, y)\\right] d y \\\\\n& =f(a+\\delta, b+\\delta)-f(a+\\delta, b)-f(a, b+\\delta)+f(a, b),\n\\end{aligned}\n$$\n\nwhere the first equality comes from Fubini's Theorem (5.32) and the second and third equalities come from the Fundamental Theorem of Calculus.\n\nA similar calculation of $\\int_{S_{\\delta}} D_{2}\\left(D_{1} f\\right) d \\lambda_{2}$ yields the same result. Thus\n\n$$\n\\int_{S_{\\delta}}\\left[D_{1}\\left(D_{2} f\\right)-D_{2}\\left(D_{1} f\\right)\\right] d \\lambda_{2}=0\n$$\n\nfor all $\\delta$ such that $\\mathcal{S}_{\\delta} \\subset G$. If $\\left(D_{1}\\left(D_{2} f\\right)\\right)(a, b)>\\left(D_{2}\\left(D_{1} f\\right)\\right)(a, b)$, then by the continuity of $D_{1}\\left(D_{2} f\\right)$ and $D_{2}\\left(D_{1} f\\right)$, the integrand in the equation above is positive on $S_{\\delta}$ for $\\delta$ sufficiently small, which contradicts the integral above equaling 0 . Similarly, the inequality $\\left(D_{1}\\left(D_{2} f\\right)\\right)(a, b)<\\left(D_{2}\\left(D_{1} f\\right)\\right)(a, b)$ also contradicts the equation above for small $\\delta$. Thus we conclude that\n\n$$\n\\left(D_{1}\\left(D_{2} f\\right)\\right)(a, b)=\\left(D_{2}\\left(D_{1} f\\right)\\right)(a, b),\n$$\n\nas desired.\n\n## EXERCISES 5C\n\n1 Show that a set $G \\subset \\mathbf{R}^{n}$ is open in $\\mathbf{R}^{n}$ if and only if for each $\\left(b_{1}, \\ldots, b_{n}\\right) \\in G$, there exists $r>0$ such that\n\n$$\n\\left\\{\\left(a_{1}, \\ldots, a_{n}\\right) \\in \\mathbf{R}^{n}: \\sqrt{\\left(a_{1}-b_{1}\\right)^{2}+\\cdots+\\left(a_{n}-b_{n}\\right)^{2}}<r\\right\\} \\subset G .\n$$\n\n2 Show that there exists a set $E \\subset \\mathbf{R}^{2}$ (thinking of $\\mathbf{R}^{2}$ as equal to $\\mathbf{R} \\times \\mathbf{R}$ ) such that the cross sections $[E]_{a}$ and $[E]^{a}$ are open subsets of $\\mathbf{R}$ for every $a \\in \\mathbf{R}$, but $E \\notin \\mathcal{B}_{2}$.\n\n3 Suppose $(X, \\mathcal{S}),(Y, \\mathcal{T})$, and $(Z, \\mathcal{U})$ are measurable spaces. We can define $\\mathcal{S} \\otimes \\mathcal{T} \\otimes \\mathcal{U}$ to be the smallest $\\sigma$-algebra on $X \\times Y \\times Z$ that contains\n\n$$\n\\{A \\times B \\times C: A \\in \\mathcal{S}, B \\in \\mathcal{T}, C \\in \\mathcal{U}\\} .\n$$\n\nProve that if we make the obvious identifications of the products $(X \\times Y) \\times Z$ and $X \\times(Y \\times Z)$ with $X \\times Y \\times Z$, then\n\n$$\n\\mathcal{S} \\otimes \\mathcal{T} \\otimes \\mathcal{U}=(\\mathcal{S} \\otimes \\mathcal{T}) \\otimes \\mathcal{U}=\\mathcal{S} \\otimes(\\mathcal{T} \\otimes \\mathcal{U})\n$$\n\n4 Show that Lebesgue measure on $\\mathbf{R}^{n}$ is translation invariant. More precisely, show that if $E \\in \\mathcal{B}_{n}$ and $a \\in \\mathbf{R}^{n}$, then $a+E \\in \\mathcal{B}_{n}$ and $\\lambda_{n}(a+E)=\\lambda_{n}(E)$, where\n\n$$\na+E=\\{a+x: x \\in E\\} .\n$$\n\n5 Suppose $f: \\mathbf{R}^{n} \\rightarrow \\mathbf{R}$ is $\\mathcal{B}_{n}$-measurable and $t \\in \\mathbf{R} \\backslash\\{0\\}$. Define $f_{t}: \\mathbf{R}^{n} \\rightarrow \\mathbf{R}$ by $f_{t}(x)=f(t x)$.\n\n(a) Prove that $f_{t}$ is $\\mathcal{B}_{n}$-measurable.\n\n(b) Prove that if $\\int_{\\mathbf{R}^{n}} f d \\lambda_{n}$ is defined, then\n\n$$\n\\int_{\\mathbf{R}^{n}} f_{t} d \\lambda_{n}=\\frac{1}{|t|^{n}} \\int_{\\mathbf{R}^{n}} f d \\lambda_{n}\n$$\n\n6 Suppose $\\lambda$ denotes Lebesgue measure on $(\\mathbf{R}, \\mathcal{L})$, where $\\mathcal{L}$ is the $\\sigma$-algebra of Lebesgue measurable subsets of $\\mathbf{R}$. Show that there exist subsets $E$ and $F$ of $\\mathbf{R}^{2}$ such that\n\n- $F \\in \\mathcal{L} \\otimes \\mathcal{L}$ and $(\\lambda \\times \\lambda)(F)=0$;\n- $E \\subset F$ but $E \\notin \\mathcal{L} \\otimes \\mathcal{L}$.\n\n[The measure space $(\\mathbf{R}, \\mathcal{L}, \\lambda)$ has the property that every subset of a set with measure 0 is measurable. This exercise asks you to show that the measure space $\\left(\\mathbf{R}^{2}, \\mathcal{L} \\otimes \\mathcal{L}, \\lambda \\times \\lambda\\right)$ does not have this property. ]\n\n7 Suppose $m \\in \\mathbf{Z}^{+}$. Verify that the collection of sets $\\mathcal{E}_{m}$ that appears in the proof of 5.41 is a monotone class.\n\n8 Show that the open unit ball in $\\mathbf{R}^{n}$ is an open subset of $\\mathbf{R}^{n}$.\n\n9 Suppose $G_{1}$ is a nonempty subset of $\\mathbf{R}^{m}$ and $G_{2}$ is a nonempty subset of $\\mathbf{R}^{n}$. Prove that $G_{1} \\times G_{2}$ is an open subset of $\\mathbf{R}^{m} \\times \\mathbf{R}^{n}$ if and only if $G_{1}$ is an open subset of $\\mathbf{R}^{m}$ and $G_{2}$ is an open subset of $\\mathbf{R}^{n}$.\n\n[One direction of this result was already proved (see 5.36); both directions are stated here to make the result look prettier and to be comparable to the next exercise, where neither direction has been proved.]\n\n10 Suppose $F_{1}$ is a nonempty subset of $\\mathbf{R}^{m}$ and $F_{2}$ is a nonempty subset of $\\mathbf{R}^{n}$. Prove that $F_{1} \\times F_{2}$ is a closed subset of $\\mathbf{R}^{m} \\times \\mathbf{R}^{n}$ if and only if $F_{1}$ is a closed subset of $\\mathbf{R}^{m}$ and $F_{2}$ is a closed subset of $\\mathbf{R}^{n}$.\n\n11 Suppose $E$ is a subset of $\\mathbf{R}^{m} \\times \\mathbf{R}^{n}$ and\n\n$$\nA=\\left\\{x \\in \\mathbf{R}^{m}:(x, y) \\in E \\text { for some } y \\in \\mathbf{R}^{n}\\right\\} .\n$$\n\n(a) Prove that if $E$ is an open subset of $\\mathbf{R}^{m} \\times \\mathbf{R}^{n}$, then $A$ is an open subset of $\\mathbf{R}^{m}$.\n\n(b) Prove or give a counterexample: If $E$ is a closed subset of $\\mathbf{R}^{m} \\times \\mathbf{R}^{n}$, then $A$ is a closed subset of $\\mathbf{R}^{m}$.\n\n12 (a) Prove that $\\lim _{n \\rightarrow \\infty} \\lambda_{n}\\left(\\mathbf{B}_{n}\\right)=0$.\n\n(b) Find the value of $n$ that maximizes $\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)$.\n\n13 For readers familiar with the gamma function $\\Gamma$ : Prove that\n\n$$\n\\lambda_{n}\\left(\\mathbf{B}_{n}\\right)=\\frac{\\pi^{n / 2}}{\\Gamma\\left(\\frac{n}{2}+1\\right)}\n$$\n\nfor every positive integer $n$.\n\n14 Define $f: \\mathbf{R}^{2} \\rightarrow \\mathbf{R}$ by\n\n$$\nf(x, y)= \\begin{cases}\\frac{x y\\left(x^{2}-y^{2}\\right)}{x^{2}+y^{2}} & \\text { if }(x, y) \\neq(0,0) \\\\ 0 & \\text { if }(x, y)=(0,0)\\end{cases}\n$$\n\n(a) Prove that $D_{1}\\left(D_{2} f\\right)$ and $D_{2}\\left(D_{1} f\\right)$ exist everywhere on $\\mathbf{R}^{2}$.\n\n(b) Show that $\\left(D_{1}\\left(D_{2} f\\right)\\right)(0,0) \\neq\\left(D_{2}\\left(D_{1} f\\right)\\right)(0,0)$.\n\n(c) Explain why (b) does not violate 5.48.\n\n## Chapter 6\n\n## Banach Spaces\n\nWe begin this chapter with a quick review of the essentials of metric spaces. Then we extend our results on measurable functions and integration to complex-valued functions. After that, we rapidly review the framework of vector spaces, which allows us to consider natural collections of measurable functions that are closed under addition and scalar multiplication.\n\nNormed vector spaces and Banach spaces, which are introduced in the third section of this chapter, play a hugely important role in modern analysis. Most interest focuses on linear maps on these vector spaces. Key results about linear maps that we develop in this chapter include the Hahn-Banach Theorem, the Open Mapping Theorem, the Closed Graph Theorem, and the Principle of Uniform Boundedness.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-161.jpg?height=779&width=1160&top_left_y=939&top_left_x=67)\n\nMarket square in Lwów, a city that has been in several countries because of changing international boundaries. Before World War I, Lwów was in Austria-Hungary. During the period between World War I and World War II, Lwów was in Poland. During this time, mathematicians in Lwów, particularly Stefan Banach (1892-1945) and his colleagues, developed the basic results of modern functional analysis. After World War II, Lwów was in the USSR. Now Lwów is in Ukraine and is called Lviv.\n\nCC-BY-SA Petar Milošević\n\n## 6A Metric Spaces\n\n## Open Sets, Closed Sets, and Continuity\n\nMuch of analysis takes place in the context of a metric space, which is a set with a notion of distance that satisfies certain properties. The properties we would like a distance function to have are captured in the next definition, where you should think of $d(f, g)$ as measuring the distance between $f$ and $g$.\n\nSpecifically, we would like the distance between two elements of our metric space to be a nonnegative number that is 0 if and only if the two elements are the same. We would like the distance between two elements not to depend on the order in which we list them. Finally, we would like a triangle inequality (the last bullet point below), which states that the distance between two elements is less than or equal to the sum of the distances obtained when we insert an intermediate element.\n\nNow we are ready for the formal definition.\n\n### 6.1 Definition metric space\n\nA metric on a nonempty set $V$ is a function $d: V \\times V \\rightarrow[0, \\infty)$ such that\n\n- $d(f, f)=0$ for all $f \\in V$;\n- if $f, g \\in V$ and $d(f, g)=0$, then $f=g$;\n- $d(f, g)=d(g, f)$ for all $f, g \\in V$;\n- $d(f, h) \\leq d(f, g)+d(g, h)$ for all $f, g, h \\in V$.\n\nA metric space is a pair $(V, d)$, where $V$ is a nonempty set and $d$ is a metric on $V$.\n\n### 6.2 Example metric spaces\n\n- Suppose $V$ is a nonempty set. Define $d$ on $V \\times V$ by setting $d(f, g)$ to be 1 if $f \\neq g$ and to be 0 if $f=g$. Then $d$ is a metric on $V$.\n- Define $d$ on $\\mathbf{R} \\times \\mathbf{R}$ by $d(x, y)=|x-y|$. Then $d$ is a metric on $\\mathbf{R}$.\n- For $n \\in \\mathbf{Z}^{+}$, define $d$ on $\\mathbf{R}^{n} \\times \\mathbf{R}^{n}$ by\n\n$$\nd\\left(\\left(x_{1}, \\ldots, x_{n}\\right),\\left(y_{1}, \\ldots, y_{n}\\right)\\right)=\\max \\left\\{\\left|x_{1}-y_{1}\\right|, \\ldots,\\left|x_{n}-y_{n}\\right|\\right\\} .\n$$\n\nThen $d$ is a metric on $\\mathbf{R}^{n}$.\n\n- Define $d$ on $C([0,1]) \\times C([0,1])$ by $d(f, g)=\\sup \\{|f(t)-g(t)|: t \\in[0,1]\\}$; here $C([0,1])$ is the set of continuous real-valued functions on $[0,1]$. Then $d$ is a metric on $C([0,1])$.\n- Define $d$ on $\\ell^{1} \\times \\ell^{1}$ by $d\\left(\\left(a_{1}, a_{2}, \\ldots\\right),\\left(b_{1}, b_{2}, \\ldots\\right)\\right)=\\sum_{k=1}^{\\infty}\\left|a_{k}-b_{k}\\right|$; here $\\ell^{1}$ is the set of sequences $\\left(a_{1}, a_{2}, \\ldots\\right)$ of real numbers such that $\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|<\\infty$. Then $d$ is a metric on $\\ell$.\n\nThe material in this section is probably review for most readers of this book. Thus more details than usual are left to the reader to verify. Verifying those details and doing the exercises is the best way to solidify your understanding of these concepts. You should be able to transfer familiar definitions and proofs from the\n\nThis book often uses symbols such as $f, g, h$ as generic elements of $a$ generic metric space because many of the important metric spaces in analysis are sets of functions; for example, see the fourth bullet point of Example 6.2. context of $\\mathbf{R}$ or $\\mathbf{R}^{n}$ to the context of a metric space.\n\nWe will need to use a metric space's topological features, which we introduce now.\n\n### 6.3 Definition open ball; $B(f, r)$; closed ball; $\\bar{B}(f, r)$\n\nSuppose $(V, d)$ is a metric space, $f \\in V$, and $r>0$.\n\n- The open ball centered at $f$ with radius $r$ is denoted $B(f, r)$ and is defined by\n\n$$\nB(f, r)=\\{g \\in V: d(f, g)<r\\} .\n$$\n\n- The closed ball centered at $f$ with radius $r$ is denoted $\\bar{B}(f, r)$ and is defined by\n\n$$\n\\bar{B}(f, r)=\\{g \\in V: d(f, g) \\leq r\\} .\n$$\n\nAbusing terminology, many books (including this one) include phrases such as suppose $V$ is a metric space without mentioning the metric $d$. When that happens, you should assume that a metric $d$ lurks nearby, even if it is not explicitly named.\n\nOur next definition declares a subset of a metric space to be open if every element in the subset is the center of an open ball that is contained in the set.\n\n### 6.4 Definition open\n\nA subset $G$ of a metric space $V$ is called open if for every $f \\in G$, there exists $r>0$ such that $B(f, r) \\subset G$.\n\n## 6.5 open balls are open\n\nSuppose $V$ is a metric space, $f \\in V$, and $r>0$. Then $B(f, r)$ is an open subset of $V$.\n\nProof Suppose $g \\in B(f, r)$. We need to show that an open ball centered at $g$ is contained in $B(f, r)$. To do this, note that if $h \\in B(g, r-d(f, g))$, then\n\n$$\nd(f, h) \\leq d(f, g)+d(g, h)<d(f, g)+(r-d(f, g))=r,\n$$\n\nwhich implies that $h \\in B(f, r)$. Thus $B(g, r-d(f, g)) \\subset B(f, r)$, which implies that $B(f, r)$ is open.\n\nClosed sets are defined in terms of open sets.\n\n### 6.6 Definition closed\n\nA subset of a metric space $V$ is called closed if its complement in $V$ is open.\n\nFor example, each closed ball $\\bar{B}(f, r)$ in a metric space is closed, as you are asked to prove in Exercise 3.\n\nNow we define the closure of a subset of a metric space.\n\n### 6.7 Definition closure; $\\bar{E}$\n\nSuppose $V$ is a metric space and $E \\subset V$. The closure of $E$, denoted $\\bar{E}$, is defined by\n\n$$\n\\bar{E}=\\{g \\in V: B(g, \\varepsilon) \\cap E \\neq \\varnothing \\text { for every } \\varepsilon>0\\} .\n$$\n\nLimits in a metric space are defined by reducing to the context of real numbers, where limits have already been defined.\n\n### 6.8 Definition limit in metric space; $\\lim _{k \\rightarrow \\infty} f_{k}$\n\nSuppose $(V, d)$ is a metric space, $f_{1}, f_{2}, \\ldots$ is a sequence in $V$, and $f \\in V$. Then\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}=f \\text { means } \\lim _{k \\rightarrow \\infty} d\\left(f_{k}, f\\right)=0 \\text {. }\n$$\n\nIn other words, a sequence $f_{1}, f_{2}, \\ldots$ in $V$ converges to $f \\in V$ if for every $\\varepsilon>0$, there exists $n \\in \\mathbf{Z}^{+}$such that\n\n$$\nd\\left(f_{k}, f\\right)<\\varepsilon \\text { for all integers } k \\geq n \\text {. }\n$$\n\nThe next result states that the closure of a set is the collection of all limits of elements of the set. Also, a set is closed if and only if it equals its closure. The proof of the next result is left as an exercise that provides good practice in using these concepts.\n\n## 6.9 closure\n\nSuppose $V$ is a metric space and $E \\subset V$. Then\n\n(a) $\\bar{E}=\\left\\{g \\in V\\right.$ : there exist $f_{1}, f_{2}, \\ldots$ in $E$ such that $\\left.\\lim _{k \\rightarrow \\infty} f_{k}=g\\right\\}$;\n\n(b) $\\bar{E}$ is the intersection of all closed subsets of $V$ that contain $E$;\n\n(c) $\\bar{E}$ is a closed subset of $V$;\n\n(d) $E$ is closed if and only if $\\bar{E}=E$;\n\n(e) $E$ is closed if and only if $E$ contains the limit of every convergent sequence of elements of $E$.\n\nThe definition of continuity that follows uses the same pattern as the definition for a function from a subset of $\\mathbf{R}$ to $\\mathbf{R}$.\n\n### 6.10 Definition continuous\n\nSuppose $\\left(V, d_{V}\\right)$ and $\\left(W, d_{W}\\right)$ are metric spaces and $T: V \\rightarrow W$ is a function.\n\n- For $f \\in V$, the function $T$ is called continuous at $f$ if for every $\\varepsilon>0$, there exists $\\delta>0$ such that\n\n$$\nd_{W}(T(f), T(g))<\\varepsilon\n$$\n\nfor all $g \\in V$ with $d_{V}(f, g)<\\delta$.\n\n- The function $T$ is called continuous if $T$ is continuous at $f$ for every $f \\in V$.\n\nThe next result gives equivalent conditions for continuity. Recall that $T^{-1}(E)$ is called the inverse image of $E$ and is defined to be $\\{f \\in V: T(f) \\in E\\}$. Thus the equivalence of the (a) and (c) below could be restated as saying that a function is continuous if and only if the inverse image of every open set is open. The equivalence of the (a) and (d) below could be restated as saying that a function is continuous if and only if the inverse image of every closed set is closed.\n\n### 6.11 equivalent conditions for continuity\n\nSuppose $V$ and $W$ are metric spaces and $T: V \\rightarrow W$ is a function. Then the following are equivalent:\n\n(a) $T$ is continuous.\n\n(b) $\\lim _{k \\rightarrow \\infty} f_{k}=f$ in $V$ implies $\\lim _{k \\rightarrow \\infty} T\\left(f_{k}\\right)=T(f)$ in $W$.\n\n(c) $T^{-1}(G)$ is an open subset of $V$ for every open set $G \\subset W$.\n\n(d) $T^{-1}(F)$ is a closed subset of $V$ for every closed set $F \\subset W$.\n\nProof We first prove that (b) implies (d). Suppose (b) holds. Suppose $F$ is a closed subset of $W$. We need to prove that $T^{-1}(F)$ is closed. To do this, suppose $f_{1}, f_{2}, \\ldots$ is a sequence in $T^{-1}(F)$ and $\\lim _{k \\rightarrow \\infty} f_{k}=f$ for some $f \\in V$. Because (b) holds, we know that $\\lim _{k \\rightarrow \\infty} T\\left(f_{k}\\right)=T(f)$. Because $f_{k} \\in T^{-1}(F)$ for each $k \\in \\mathbf{Z}^{+}$, we know that $T\\left(f_{k}\\right) \\in F$ for each $k \\in \\mathbf{Z}^{+}$. Because $F$ is closed, this implies that $T(f) \\in F$. Thus $f \\in T^{-1}(F)$, which implies that $T^{-1}(F)$ is closed [by 6.9(e)], completing the proof that (b) implies (d).\n\nThe proof that (c) and (d) are equivalent follows from the equation\n\n$$\nT^{-1}(W \\backslash E)=V \\backslash T^{-1}(E)\n$$\n\nfor every $E \\subset W$ and the fact that a set is open if and only if its complement (in the appropriate metric space) is closed.\n\nThe proof of the remaining parts of this result are left as an exercise that should help strengthen your understanding of these concepts.\n\n## Cauchy Sequences and Completeness\n\nThe next definition is useful for showing (in some metric spaces) that a sequence has a limit, even when we do not have a good candidate for that limit.\n\n### 6.12 Definition Cauchy sequence\n\nA sequence $f_{1}, f_{2}, \\ldots$ in a metric space $(V, d)$ is called a Cauchy sequence if for every $\\varepsilon>0$, there exists $n \\in \\mathbf{Z}^{+}$such that $d\\left(f_{j}, f_{k}\\right)<\\varepsilon$ for all integers $j \\geq n$ and $k \\geq n$.\n\n### 6.13 every convergent sequence is a Cauchy sequence\n\nEvery convergent sequence in a metric space is a Cauchy sequence.\n\nProof Suppose $\\lim _{k \\rightarrow \\infty} f_{k}=f$ in a metric space $(V, d)$. Suppose $\\varepsilon>0$. Then there exists $n \\in \\mathbf{Z}^{+}$such that $d\\left(f_{k}, f\\right)<\\frac{\\varepsilon}{2}$ for all $k \\geq n$. If $j, k \\in \\mathbf{Z}^{+}$are such that $j \\geq n$ and $k \\geq n$, then\n\n$$\nd\\left(f_{j}, f_{k}\\right) \\leq d\\left(f_{j}, f\\right)+d\\left(f, f_{k}\\right)<\\frac{\\varepsilon}{2}+\\frac{\\varepsilon}{2}=\\varepsilon .\n$$\n\nThus $f_{1}, f_{2}, \\ldots$ is a Cauchy sequence, completing the proof.\n\nMetric spaces that satisfy the converse of the result above have a special name.\n\n### 6.14 Definition complete metric space\n\nA metric space $V$ is called complete if every Cauchy sequence in $V$ converges to some element of $V$.\n\n### 6.15 Example\n\n- All five of the metric spaces in Example 6.2 are complete, as you should verify.\n- The metric space $\\mathbf{Q}$, with metric defined by $d(x, y)=|x-y|$, is not complete. To see this, for $k \\in \\mathbf{Z}^{+}$let\n\n$$\nx_{k}=\\frac{1}{10^{1 !}}+\\frac{1}{10^{2 !}}+\\cdots+\\frac{1}{10^{k !}} \\text {. }\n$$\n\nIf $j<k$, then\n\n$$\n\\left|x_{k}-x_{j}\\right|=\\frac{1}{10^{(j+1) !}}+\\cdots+\\frac{1}{10^{k !}}<\\frac{2}{10^{(j+1) !}} .\n$$\n\nThus $x_{1}, x_{2}, \\ldots$ is a Cauchy sequence in $\\mathbf{Q}$. However, $x_{1}, x_{2}, \\ldots$ does not converge to an element of $\\mathbf{Q}$ because the limit of this sequence would have a decimal expansion $0.110001000000000000000001 \\ldots$ that is neither a terminating decimal nor a repeating decimal. Thus $\\mathbf{Q}$ is not a complete metric space.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-167.jpg?height=645&width=1158&top_left_y=152&top_left_x=68)\n\nEntrance to the École Polytechnique (Paris), where Augustin-Louis Cauchy (1789-1857) was a student and a faculty member. Cauchy wrote almost 800 mathematics papers and the highly influential textbook Cours d'Analyse (published in 1821), which greatly influenced the development of analysis.\n\nCC-BY-SA NonOmnisMoriar\n\nEvery nonempty subset of a metric space is a metric space. Specifically, suppose $(V, d)$ is a metric space and $U$ is a nonempty subset of $V$. Then restricting $d$ to $U \\times U$ gives a metric on $U$. Unless stated otherwise, you should assume that the metric on a subset is this restricted metric that the subset inherits from the bigger set.\n\nCombining the two bullet points in the result below shows that a subset of a complete metric space is complete if and only if it is closed.\n\n### 6.16 connection between complete and closed\n\n(a) A complete subset of a metric space is closed.\n\n(b) A closed subset of a complete metric space is complete.\n\nProof We begin with a proof of (a). Suppose $U$ is a complete subset of a metric space $V$. Suppose $f_{1}, f_{2}, \\ldots$ is a sequence in $U$ that converges to some $g \\in V$. Then $f_{1}, f_{2}, \\ldots$ is a Cauchy sequence in $U$ (by 6.13). Hence by the completeness of $U$, the sequence $f_{1}, f_{2}, \\ldots$ converges to some element of $U$, which must be $g$ (see Exercise 7). Hence $g \\in U$. Now 6.9(e) implies that $U$ is a closed subset of $V$, completing the proof of (a).\n\nTo prove (b), suppose $U$ is a closed subset of a complete metric space $V$. To show that $U$ is complete, suppose $f_{1}, f_{2}, \\ldots$ is a Cauchy sequence in $U$. Then $f_{1}, f_{2}, \\ldots$ is also a Cauchy sequence in $V$. By the completeness of $V$, this sequence converges to some $f \\in V$. Because $U$ is closed, this implies that $f \\in U$ (see 6.9). Thus the Cauchy sequence $f_{1}, f_{2}, \\ldots$ converges to an element of $U$, showing that $U$ is complete. Hence (b) has been proved.\n\n## EXERCISES 6 A\n\n1 Verify that each of the claimed metrics in Example 6.2 is indeed a metric.\n\n2 Prove that every finite subset of a metric space is closed.\n\n3 Prove that every closed ball in a metric space is closed.\n\n4 Suppose $V$ is a metric space.\n\n(a) Prove that the union of each collection of open subsets of $V$ is an open subset of $V$.\n\n(b) Prove that the intersection of each finite collection of open subsets of $V$ is an open subset of $V$.\n\n5 Suppose $V$ is a metric space.\n\n(a) Prove that the intersection of each collection of closed subsets of $V$ is a closed subset of $V$.\n\n(b) Prove that the union of each finite collection of closed subsets of $V$ is a closed subset of $V$.\n\n6 (a) Prove that if $V$ is a metric space, $f \\in V$, and $r>0$, then $\\overline{B(f, r)} \\subset \\bar{B}(f, r)$.\n\n(b) Give an example of a metric space $V, f \\in V$, and $r>0$ such that $\\overline{B(f, r)} \\neq \\bar{B}(f, r)$.\n\n7 Show that each sequence in a metric space has at most one limit.\n\n8 Prove 6.9.\n\n9 Prove that each open subset of a metric space $V$ is the union of some sequence of closed subsets of $V$.\n\n10 Prove or give a counterexample: If $V$ is a metric space and $U, W$ are subsets of $V$, then $\\bar{U} \\cup \\bar{W}=\\overline{U \\cup W}$.\n\n11 Prove or give a counterexample: If $V$ is a metric space and $U, W$ are subsets of $V$, then $\\bar{U} \\cap \\bar{W}=\\overline{U \\cap W}$.\n\n12 Suppose $\\left(U, d_{U}\\right),\\left(V, d_{V}\\right)$, and $\\left(W, d_{W}\\right)$ are metric spaces. Suppose also that $T: U \\rightarrow V$ and $S: V \\rightarrow W$ are continuous functions.\n\n(a) Using the definition of continuity, show that $S \\circ T: U \\rightarrow W$ is continuous.\n\n(b) Using the equivalence of 6.11(a) and 6.11(b), show that $S \\circ T: U \\rightarrow W$ is continuous.\n\n(c) Using the equivalence of 6.11(a) and 6.11(c), show that $S \\circ T: U \\rightarrow W$ is continuous.\n\n13 Prove the parts of 6.11 that were not proved in the text.\n\n14 Suppose a Cauchy sequence in a metric space has a convergent subsequence. Prove that the Cauchy sequence converges.\n\n15 Verify that all five of the metric spaces in Example 6.2 are complete metric spaces.\n\n16 Suppose $(U, d)$ is a metric space. Let $W$ denote the set of all Cauchy sequences of elements of $U$.\n\n(a) For $\\left(f_{1}, f_{2}, \\ldots\\right)$ and $\\left(g_{1}, g_{2}, \\ldots\\right)$ in $W$, define $\\left(f_{1}, f_{2}, \\ldots\\right) \\equiv\\left(g_{1}, g_{2}, \\ldots\\right)$ to mean that\n\n$$\n\\lim _{k \\rightarrow \\infty} d\\left(f_{k}, g_{k}\\right)=0\n$$\n\nShow that $\\equiv$ is an equivalence relation on $W$.\n\n(b) Let $V$ denote the set of equivalence classes of elements of $W$ under the equivalence relation above. For $\\left(f_{1}, f_{2}, \\ldots\\right) \\in W$, let $\\left(f_{1}, f_{2}, \\ldots\\right)^{\\wedge}$ denote the equivalence class of $\\left(f_{1}, f_{2}, \\ldots\\right)$. Define $d_{V}: V \\times V \\rightarrow[0, \\infty)$ by\n\n$$\nd_{V}\\left(\\left(f_{1}, f_{2}, \\ldots\\right)^{\\wedge},\\left(g_{1}, g_{2}, \\ldots\\right)^{\\wedge}\\right)=\\lim _{k \\rightarrow \\infty} d\\left(f_{k}, g_{k}\\right) .\n$$\n\nShow that this definition of $d_{V}$ makes sense and that $d_{V}$ is a metric on $V$.\n\n(c) Show that $\\left(V, d_{V}\\right)$ is a complete metric space.\n\n(d) Show that the map from $U$ to $V$ that takes $f \\in U$ to $(f, f, f, \\ldots)$ preserves distances, meaning that\n\n$$\nd(f, g)=d_{V}\\left((f, f, f, \\ldots)^{\\wedge},(g, g, g, \\ldots)^{\\wedge}\\right)\n$$\n\nfor all $f, g \\in U$.\n\n(e) Explain why (d) shows that every metric space is a subset of some complete metric space.\n\n## 6B Vector Spaces\n\n## Integration of Complex-Valued Functions\n\nComplex numbers were invented so that we can take square roots of negative numbers. The idea is to assume we have a square root of -1 , denoted $i$, that obeys the usual rules of arithmetic. Here are the formal definitions:\n\n### 6.17 Definition complex numbers; C; addition and multiplication in C\n\n- A complex number is an ordered pair $(a, b)$, where $a, b \\in \\mathbf{R}$, but we write this as $a+b i$ or $a+i b$.\n- The set of all complex numbers is denoted by $\\mathbf{C}$ :\n\n$$\n\\mathbf{C}=\\{a+b i: a, b \\in \\mathbf{R}\\} .\n$$\n\n- Addition and multiplication in $\\mathbf{C}$ are defined by\n\n$$\n\\begin{gathered}\n(a+b i)+(c+d i)=(a+c)+(b+d) i \\\\\n(a+b i)(c+d i)=(a c-b d)+(a d+b c) i\n\\end{gathered}\n$$\n\nhere $a, b, c, d \\in \\mathbf{R}$.\n\nIf $a \\in \\mathbf{R}$, then we identify $a+0 i$ with $a$. Thus we think of $\\mathbf{R}$ as a subset of C. We also usually write $0+b i$ as $b i$, and we usually write $0+1 i$ as $i$. You should verify that $i^{2}=-1$.\n\nWith the definitions as above, $\\mathbf{C}$ satisfies the usual rules of arithmetic. Specifically, with addition and multiplication defined as above, $\\mathbf{C}$ is a field, as you should verify. Thus subtraction and division of complex numbers are defined as in any field.\n\nThe field $\\mathbf{C}$ cannot be made into an ordered field. However, the useful concept of an absolute value can still be defined\n\nThe symbol $i$ was first used to denote $\\sqrt{-1}$ by Leonhard Euler (1707-1783) in 1777. on $\\mathbf{C}$.\n\n6.18 Definition real part; $\\operatorname{Re} z$; imaginary part; $\\operatorname{Im} z ;$ absolute value; limits\n\nSuppose $z=a+b i$, where $a$ and $b$ are real numbers.\n\n- The real part of $z$, denoted $\\operatorname{Re} z$, is defined by $\\operatorname{Re} z=a$.\n- The imaginary part of $z$, denoted $\\operatorname{Im} z$, is defined by $\\operatorname{Im} z=b$.\n- The absolute value of $z$, denoted $|z|$, is defined by $|z|=\\sqrt{a^{2}+b^{2}}$.\n- If $z_{1}, z_{2}, \\ldots \\in \\mathbf{C}$ and $L \\in \\mathbf{C}$, then $\\lim _{k \\rightarrow \\infty} z_{k}=L$ means $\\lim _{k \\rightarrow \\infty}\\left|z_{k}-L\\right|=0$.\n\nFor $b$ a real number, the usual definition of $|b|$ as a real number is consistent with the new definition just given of $|b|$ with $b$ thought of as a complex number. Note that if $z_{1}, z_{2}, \\ldots$ is a sequence of complex numbers and $L \\in \\mathbf{C}$, then\n\n$$\n\\lim _{k \\rightarrow \\infty} z_{k}=L \\Longleftrightarrow \\lim _{k \\rightarrow \\infty} \\operatorname{Re} z_{k}=\\operatorname{Re} L \\text { and } \\lim _{k \\rightarrow \\infty} \\operatorname{Im} z_{k}=\\operatorname{Im} L .\n$$\n\nWe will reduce questions concerning measurability and integration of a complexvalued function to the corresponding questions about the real and imaginary parts of the function. We begin this process with the following definition.\n\n### 6.19 Definition measurable complex-valued function\n\nSuppose $(X, \\mathcal{S})$ is a measurable space. A function $f: X \\rightarrow \\mathrm{C}$ is called $\\mathcal{S}$-measurable if $\\operatorname{Re} f$ and $\\operatorname{Im} f$ are both $\\mathcal{S}$-measurable functions.\n\nSee Exercise 5 in this section for two natural conditions that are equivalent to measurability for complex-valued functions.\n\nWe will make frequent use of the following result. See Exercise 6 in this section for algebraic combinations of complex-valued measurable functions.\n\n## $6.20|f|^{p}$ is measurable if $f$ is measurable\n\nSuppose $(X, \\mathcal{S})$ is a measurable space, $f: X \\rightarrow \\mathbf{C}$ is an $\\mathcal{S}$-measurable function, and $0<p<\\infty$. Then $|f|^{p}$ is an $\\mathcal{S}$-measurable function.\n\nProof The functions $(\\operatorname{Re} f)^{2}$ and $(\\operatorname{Im} f)^{2}$ are $\\mathcal{S}$-measurable because the square of an $\\mathcal{S}$-measurable function is measurable (by Example 2.45). Thus the function $(\\operatorname{Re} f)^{2}+(\\operatorname{Im} f)^{2}$ is $\\mathcal{S}$-measurable (because the sum of two $\\mathcal{S}$-measurable functions is $\\mathcal{S}$-measurable by 2.46). Now $\\left((\\operatorname{Re} f)^{2}+(\\operatorname{Im} f)^{2}\\right)^{p / 2}$ is $\\mathcal{S}$-measurable because it is the composition of a continuous function on $[0, \\infty)$ and an $\\mathcal{S}$-measurable function (see 2.44 and 2.41). In other words, $|f|^{p}$ is an $\\mathcal{S}$-measurable function.\n\nNow we define integration of a complex-valued function by separating the function into its real and imaginary parts.\n\n### 6.21 Definition integral of a complex-valued function; $\\int f d \\mu$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow \\mathbf{C}$ is an $\\mathcal{S}$-measurable function with $\\int|f| d \\mu<\\infty$ [the collection of such functions is denoted $\\mathcal{L}^{1}(\\mu)$ ]. Then $\\int f d \\mu$ is defined by\n\n$$\n\\int f d \\mu=\\int(\\operatorname{Re} f) d \\mu+i \\int(\\operatorname{Im} f) d \\mu .\n$$\n\nThe integral of a complex-valued measurable function is defined above only when the absolute value of the function has a finite integral. In contrast, the integral of every nonnegative measurable function is defined (although the value may be $\\infty$ ), and if $f$ is real valued then $\\int f d \\mu$ is defined to be $\\int f^{+} d \\mu-\\int f^{-} d \\mu$ if at least one of $\\int f^{+} d \\mu$ and $\\int f^{-} d \\mu$ is finite.\n\nYou can easily show that if $f, g: X \\rightarrow \\mathbf{C}$ are $\\mathcal{S}$-measurable functions such that $\\int|f| d \\mu<\\infty$ and $\\int|g| d \\mu<\\infty$, then\n\n$$\n\\int(f+g) d \\mu=\\int f d \\mu+\\int g d \\mu .\n$$\n\nSimilarly, the definition of complex multiplication leads to the conclusion that\n\n$$\n\\int \\alpha f d \\mu=\\alpha \\int f d \\mu\n$$\n\nfor all $\\alpha \\in \\mathbf{C}$ (see Exercise 8).\n\nThe inequality in the result below concerning integration of complex-valued functions does not follow immediately from the corresponding result for real-valued functions. However, the small trick used in the proof below does give a reasonably simple proof.\n\n### 6.22 bound on the absolute value of an integral\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow \\mathbf{C}$ is an $\\mathcal{S}$-measurable function such that $\\int|f| d \\mu<\\infty$. Then\n\n$$\n\\left|\\int f d \\mu\\right| \\leq \\int|f| d \\mu\n$$\n\nProof The result clearly holds if $\\int f d \\mu=0$. Thus assume that $\\int f d \\mu \\neq 0$.\n\nLet\n\n$$\n\\alpha=\\frac{\\left|\\int f d \\mu\\right|}{\\int f d \\mu}\n$$\n\nThen\n\n$$\n\\begin{aligned}\n\\left|\\int f d \\mu\\right|=\\alpha \\int f d \\mu & =\\int \\alpha f d \\mu \\\\\n& =\\int \\operatorname{Re}(\\alpha f) d \\mu+i \\int \\operatorname{Im}(\\alpha f) d \\mu \\\\\n& =\\int \\operatorname{Re}(\\alpha f) d \\mu \\\\\n& \\leq \\int|\\alpha f| d \\mu \\\\\n& =\\int|f| d \\mu\n\\end{aligned}\n$$\n\nwhere the second equality holds by Exercise 8, the fourth equality holds because $\\left|\\int f d \\mu\\right| \\in \\mathbf{R}$, the inequality on the fourth line holds because $\\operatorname{Re} z \\leq|z|$ for every complex number $z$, and the equality in the last line holds because $|\\alpha|=1$.\n\nBecause of the result above, the Bounded Convergence Theorem (3.26) and the Dominated Convergence Theorem (3.31) hold if the functions $f_{1}, f_{2}, \\ldots$ and $f$ in the statements of those theorems are allowed to be complex valued.\n\nWe now define the complex conjugate of a complex number.\n\n### 6.23 Definition complex conjugate; $\\bar{z}$\n\nSuppose $z \\in \\mathbf{C}$. The complex conjugate of $z \\in \\mathbf{C}$, denoted $\\bar{z}$ (pronounced $z$-bar), is defined by\n\n$$\n\\bar{z}=\\operatorname{Re} z-(\\operatorname{Im} z) i\n$$\n\nFor example, if $z=5+7 i$ then $\\bar{z}=5-7 i$. Note that a complex number $z$ is a real number if and only if $z=\\bar{z}$.\n\nThe next result gives basic properties of the complex conjugate.\n\n### 6.24 properties of complex conjugates\n\nSuppose $w, z \\in \\mathbf{C}$. Then\n\n- product of $z$ and $\\bar{z}$\n\n$z \\bar{z}=|z|^{2}$\n\n- sum and difference of $z$ and $\\bar{z}$\n\n$z+\\bar{z}=2 \\operatorname{Re} z$ and $z-\\bar{z}=2(\\operatorname{Im} z) i$\n\n- additivity and multiplicativity of complex conjugate\n\n$\\overline{w+z}=\\bar{w}+\\bar{z}$ and $\\overline{w z}=\\bar{w} \\bar{z}$\n\n- complex conjugate of complex conjugate\n\n$\\overline{\\bar{z}}=z$\n\n- absolute value of complex conjugate\n\n$|\\bar{z}|=|z|$\n\n- integral of complex conjugate of a function\n\n$\\int \\bar{f} d \\mu=\\overline{\\int f d \\mu}$ for every measure $\\mu$ and every $f \\in \\mathcal{L}^{1}(\\mu)$.\n\nProof The first item holds because\n\n$$\nz \\bar{z}=(\\operatorname{Re} z+i \\operatorname{Im} z)(\\operatorname{Re} z-i \\operatorname{Im} z)=(\\operatorname{Re} z)^{2}+(\\operatorname{Im} z)^{2}=|z|^{2}\n$$\n\nTo prove the last item, suppose $\\mu$ is a measure and $f \\in \\mathcal{L}^{1}(\\mu)$. Then\n\n$$\n\\begin{aligned}\n\\int \\bar{f} d \\mu=\\int(\\operatorname{Re} f-i \\operatorname{Im} f) d \\mu & =\\int \\operatorname{Re} f d \\mu-i \\int \\operatorname{Im} f d \\mu \\\\\n& =\\overline{\\int \\operatorname{Re} f d \\mu+i \\int \\operatorname{Im} f d \\mu} \\\\\n& =\\overline{\\int f d \\mu},\n\\end{aligned}\n$$\n\nas desired.\n\nThe straightforward proofs of the remaining items are left to the reader.\n\n## Vector Spaces and Subspaces\n\nThe structure and language of vector spaces will help us focus on certain features of collections of measurable functions. So that we can conveniently make definitions and prove theorems that apply to both real and complex numbers, we adopt the following notation.\n\n### 6.25 Definition $\\mathbf{F}$\n\nFrom now on, $\\mathbf{F}$ stands for either $\\mathbf{R}$ or $\\mathbf{C}$.\n\nIn the definitions that follow, we use $f$ and $g$ to denote elements of $V$ because in the crucial examples the elements of $V$ are functions from a set $X$ to $\\mathbf{F}$.\n\n### 6.26 Definition addition; scalar multiplication\n\n- An addition on a set $V$ is a function that assigns an element $f+g \\in V$ to each pair of elements $f, g \\in V$.\n- A scalar multiplication on a set $V$ is a function that assigns an element $\\alpha f \\in V$ to each $\\alpha \\in \\mathbf{F}$ and each $f \\in V$.\n\nNow we are ready to give the formal definition of a vector space.\n\n### 6.27 Definition vector space\n\nA vector space (over $\\mathbf{F}$ ) is a set $V$ along with an addition on $V$ and a scalar multiplication on $V$ such that the following properties hold:\n\n## commutativity\n\n$f+g=g+f$ for all $f, g \\in V$;\n\n## associativity\n\n$(f+g)+h=f+(g+h)$ and $(\\alpha \\beta) f=\\alpha(\\beta f)$ for all $f, g, h \\in V$ and $\\alpha, \\beta \\in \\mathbf{F} ;$\n\n## additive identity\n\nthere exists an element $0 \\in V$ such that $f+0=f$ for all $f \\in V$;\n\nadditive inverse\n\nfor every $f \\in V$, there exists $g \\in V$ such that $f+g=0$;\n\n## multiplicative identity\n\n$1 f=f$ for all $f \\in V$\n\n## distributive properties\n\n$\\alpha(f+g)=\\alpha f+\\alpha g$ and $(\\alpha+\\beta) f=\\alpha f+\\beta f$ for all $\\alpha, \\beta \\in \\mathbf{F}$ and $f, g \\in V$.\n\nMost vector spaces that you will encounter are subsets of the vector space $\\mathbf{F}^{X}$ presented in the next example.\n\n### 6.28 Example the vector space $\\mathbf{F}^{X}$\n\nSuppose $X$ is a nonempty set. Let $\\mathbf{F}^{X}$ denote the set of functions from $X$ to $\\mathbf{F}$. Addition and scalar multiplication on $\\mathbf{F}^{X}$ are defined as expected: for $f, g \\in \\mathbf{F}^{X}$ and $\\alpha \\in \\mathbf{F}$, define\n\n$$\n(f+g)(x)=f(x)+g(x) \\quad \\text { and } \\quad(\\alpha f)(x)=\\alpha(f(x))\n$$\n\nfor $x \\in X$. Then, as you should verify, $\\mathbf{F}^{X}$ is a vector space; the additive identity in this vector space is the function $0 \\in \\mathbf{F}^{X}$ defined by $0(x)=0$ for all $x \\in X$.\n\n### 6.29 Example $\\mathbf{F}^{n} ; \\mathbf{F}^{\\mathbf{Z}^{+}}$\n\nSpecial case of the previous example: if $n \\in \\mathbf{Z}^{+}$and $X=\\{1, \\ldots, n\\}$, then $\\mathbf{F}^{X}$ is the familiar space $\\mathbf{R}^{n}$ or $\\mathbf{C}^{n}$, depending upon whether $\\mathbf{F}=\\mathbf{R}$ or $\\mathbf{F}=\\mathbf{C}$.\n\nAnother special case: $\\mathbf{F}^{\\mathbf{Z}}$ is the vector space of all sequences of real numbers or complex numbers, again depending upon whether $\\mathbf{F}=\\mathbf{R}$ or $\\mathbf{F}=\\mathbf{C}$.\n\nBy considering subspaces, we can greatly expand our examples of vector spaces.\n\n### 6.30 Definition subspace\n\nA subset $U$ of $V$ is called a subspace of $V$ if $U$ is also a vector space (using the same addition and scalar multiplication as on $V$ ).\n\nThe next result gives the easiest way to check whether a subset of a vector space is a subspace.\n\n### 6.31 conditions for a subspace\n\nA subset $U$ of $V$ is a subspace of $V$ if and only if $U$ satisfies the following three conditions:\n\n- additive identity $0 \\in U$\n\n## - closed under addition\n\n$f, g \\in U$ implies $f+g \\in U$;\n\n- closed under scalar multiplication\n\n$\\alpha \\in \\mathbf{F}$ and $f \\in U$ implies $\\alpha f \\in U$.\n\nProof If $U$ is a subspace of $V$, then $U$ satisfies the three conditions above by the definition of vector space.\n\nConversely, suppose $U$ satisfies the three conditions above. The first condition above ensures that the additive identity of $V$ is in $U$.\n\nThe second condition above ensures that addition makes sense on $U$. The third condition ensures that scalar multiplication makes sense on $U$.\n\nIf $f \\in V$, then $0 f=(0+0) f=0 f+0 f$. Adding the additive inverse of $0 f$ to both sides of this equation shows that $0 f=0$. Now if $f \\in U$, then $(-1) f$ is also in $U$ by the third condition above. Because $f+(-1) f=(1+(-1)) f=0 f=0$, we see that $(-1) f$ is an additive inverse of $f$. Hence every element of $U$ has an additive inverse in $U$.\n\nThe other parts of the definition of a vector space, such as associativity and commutativity, are automatically satisfied for $U$ because they hold on the larger space $V$. Thus $U$ is a vector space and hence is a subspace of $V$.\n\nThe three conditions in 6.31 usually enable us to determine quickly whether a given subset of $V$ is a subspace of $V$, as illustrated below. All the examples below except for the first bullet point involve concepts from measure theory.\n\n### 6.32 Example subspaces of $\\mathbf{F}^{X}$\n\n- The set $C([0,1])$ of continuous real-valued functions on $[0,1]$ is a vector space over $\\mathbf{R}$ because the sum of two continuous functions is continuous and a constant multiple of a continuous functions is continuous. In other words, $C([0,1])$ is a subspace of $\\mathbf{R}^{[0,1]}$.\n- Suppose $(X, \\mathcal{S})$ is a measurable space. Then the set of $\\mathcal{S}$-measurable functions from $X$ to $\\mathbf{F}$ is a subspace of $\\mathbf{F}^{X}$ because the sum of two $\\mathcal{S}$-measurable functions is $\\mathcal{S}$-measurable and a constant multiple of an $\\mathcal{S}$-measurable function is $\\mathcal{S}$ measurable.\n- Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space. Then the set $\\mathcal{Z}(\\mu)$ of $\\mathcal{S}$-measurable functions $f$ from $X$ to $\\mathbf{F}$ such that $f=0$ almost everywhere [meaning that $\\mu(\\{x \\in X: f(x) \\neq 0\\})=0]$ is a vector space over $\\mathbf{F}$ because the union of two sets with $\\mu$-measure 0 is a set with $\\mu$-measure 0 [which implies that $\\mathcal{Z}(\\mu)$ is closed under addition]. Note that $\\mathcal{Z}(\\mu)$ is a subspace of $\\mathbf{F}^{X}$.\n- Suppose $(X, \\mathcal{S})$ is a measurable space. Then the set of bounded measurable functions from $X$ to $\\mathbf{F}$ is a subspace of $\\mathbf{F}^{X}$ because the sum of two bounded $\\mathcal{S}$-measurable functions is a bounded $\\mathcal{S}$-measurable function and a constant multiple of a bounded $\\mathcal{S}$-measurable function is a bounded $\\mathcal{S}$-measurable function.\n- Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space. Then the set of $\\mathcal{S}$-measurable functions $f$ from $X$ to $\\mathbf{F}$ such that $\\int f d \\mu=0$ is a subspace of $\\mathbf{F}^{X}$ because of standard properties of integration.\n- Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space. Then the set $\\mathcal{L}^{1}(\\mu)$ of $\\mathcal{S}$-measurable functions from $X$ to $\\mathbf{F}$ such that $\\int|f| d \\mu<\\infty$ is a subspace of $\\mathbf{F}^{X}$ [we are now redefining $\\mathcal{L}^{1}(\\mu)$ to allow for the possibility that $\\mathbf{F}=\\mathbf{R}$ or $\\left.\\mathbf{F}=\\mathbf{C}\\right]$. The set $\\mathcal{L}^{1}(\\mu)$ is closed under addition and scalar multiplication because $\\int|f+g| d \\mu \\leq$ $\\int|f| d \\mu+\\int|g| d \\mu$ and $\\int|\\alpha f| d \\mu=|\\alpha| \\int|f| d \\mu$.\n- The set $\\ell^{1}$ of all sequences $\\left(a_{1}, a_{2}, \\ldots\\right)$ of elements of $\\mathbf{F}$ such that $\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|<\\infty$ is a subspace of $\\mathbf{F}^{\\mathbf{Z}^{+}}$. Note that $\\ell^{1}$ is a special case of the example in the previous bullet point (take $\\mu$ to be counting measure on $\\mathbf{Z}^{+}$).\n\n## EXERCISES 6B\n\n1 Show that if $a, b \\in \\mathbf{R}$ with $a+b i \\neq 0$, then\n\n$$\n\\frac{1}{a+b i}=\\frac{a}{a^{2}+b^{2}}-\\frac{b}{a^{2}+b^{2}} i\n$$\n\n2 Suppose $z \\in$ C. Prove that\n\n$$\n\\max \\{|\\operatorname{Re} z|,|\\operatorname{Im} z|\\} \\leq|z| \\leq \\sqrt{2} \\max \\{|\\operatorname{Re} z|,|\\operatorname{Im} z|\\} .\n$$\n\n3 Suppose $z \\in \\mathbf{C}$. Prove that $\\frac{|\\operatorname{Re} z|+|\\operatorname{Im} z|}{\\sqrt{2}} \\leq|z| \\leq|\\operatorname{Re} z|+|\\operatorname{Im} z|$.\n\n4 Suppose $w, z \\in \\mathbf{C}$. Prove that $|w z|=|w||z|$ and $|w+z| \\leq|w|+|z|$.\n\n5 Suppose $(X, \\mathcal{S})$ is a measurable space and $f: X \\rightarrow \\mathbf{C}$ is a complex-valued function. For conditions (b) and (c) below, identify $\\mathbf{C}$ with $\\mathbf{R}^{2}$. Prove that the following are equivalent:\n\n(a) $f$ is $\\mathcal{S}$-measurable.\n\n(b) $f^{-1}(G) \\in \\mathcal{S}$ for every open set $G$ in $\\mathbf{R}^{2}$.\n\n(c) $f^{-1}(B) \\in \\mathcal{S}$ for every Borel set $B \\in \\mathcal{B}_{2}$.\n\n6 Suppose $(X, \\mathcal{S})$ is a measurable space and $f, g: X \\rightarrow \\mathbf{C}$ are $\\mathcal{S}$-measurable. Prove that\n\n(a) $f+g, f-g$, and $f g$ are $\\mathcal{S}$-measurable functions;\n\n(b) if $g(x) \\neq 0$ for all $x \\in X$, then $\\frac{f}{g}$ is an $\\mathcal{S}$-measurable function.\n\n7 Suppose $(X, \\mathcal{S})$ is a measurable space and $f_{1}, f_{2}, \\ldots$ is a sequence of $\\mathcal{S}$ measurable functions from $X$ to $C$. Suppose $\\lim _{k \\rightarrow \\infty} f_{k}(x)$ exists for each $x \\in X$. Define $f: X \\rightarrow \\mathbf{C}$ by\n\n$$\nf(x)=\\lim _{k \\rightarrow \\infty} f_{k}(x)\n$$\n\nProve that $f$ is an $\\mathcal{S}$-measurable function.\n\n8 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f: X \\rightarrow \\mathbf{C}$ is an $\\mathcal{S}$-measurable function such that $\\int|f| d \\mu<\\infty$. Prove that if $\\alpha \\in \\mathbf{C}$, then\n\n$$\n\\int \\alpha f d \\mu=\\alpha \\int f d \\mu\n$$\n\n9 Suppose $V$ is a vector space. Show that the intersection of every collection of subspaces of $V$ is a subspace of $V$.\n\n10 Suppose $V$ and $W$ are vector spaces. Define $V \\times W$ by\n\n$$\nV \\times W=\\{(f, g): f \\in V \\text { and } g \\in W\\}\n$$\n\nDefine addition and scalar multiplication on $V \\times W$ by\n\n$$\n\\left(f_{1}, g_{1}\\right)+\\left(f_{2}, g_{2}\\right)=\\left(f_{1}+f_{2}, g_{1}+g_{2}\\right) \\quad \\text { and } \\quad \\alpha(f, g)=(\\alpha f, \\alpha g) \\text {. }\n$$\n\nProve that $V \\times W$ is a vector space with these operations.\n\n## 6C Normed Vector Spaces\n\n## Norms and Complete Norms\n\nThis section begins with a crucial definition.\n\n### 6.33 Definition norm; normed vector space\n\nA norm on a vector space $V($ over $\\mathbf{F})$ is a function $\\|\\cdot\\|: V \\rightarrow[0, \\infty)$ such that\n\n- $\\|f\\|=0$ if and only if $f=0$ (positive definite);\n- $\\|\\alpha f\\|=|\\alpha|\\|f\\|$ for all $\\alpha \\in \\mathbf{F}$ and $f \\in V$ (homogeneity);\n- $\\|f+g\\| \\leq\\|f\\|+\\|g\\|$ for all $f, g \\in V$ (triangle inequality).\n\nA normed vector space is a pair $(V,\\|\\cdot\\|)$, where $V$ is a vector space and $\\|\\cdot\\|$ is a norm on $V$.\n\n### 6.34 Example norms\n\n- Suppose $n \\in \\mathbf{Z}^{+}$. Define $\\|\\cdot\\|_{1}$ and $\\|\\cdot\\|_{\\infty}$ on $\\mathbf{F}^{n}$ by\n\n$$\n\\left\\|\\left(a_{1}, \\ldots, a_{n}\\right)\\right\\|_{1}=\\left|a_{1}\\right|+\\cdots+\\left|a_{n}\\right|\n$$\n\nand\n\n$$\n\\left\\|\\left(a_{1}, \\ldots, a_{n}\\right)\\right\\|_{\\infty}=\\max \\left\\{\\left|a_{1}\\right|, \\ldots,\\left|a_{n}\\right|\\right\\} .\n$$\n\nThen $\\|\\cdot\\|_{1}$ and $\\|\\cdot\\|_{\\infty}$ are norms on $\\mathbf{F}^{n}$, as you should verify.\n\n- On $\\ell^{1}$ (see the last bullet point in Example 6.32 for the definition of $\\ell^{1}$ ), define $\\|\\cdot\\|_{1}$ by\n\n$$\n\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{1}=\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|\n$$\n\nThen $\\|\\cdot\\|_{1}$ is a norm on $\\ell^{1}$, as you should verify.\n\n- Suppose $X$ is a nonempty set and $b(X)$ is the subspace of $\\mathbf{F}^{X}$ consisting of the bounded functions from $X$ to $\\mathbf{F}$. For $f$ a bounded function from $X$ to $\\mathbf{F}$, define $\\|f\\|$ by\n\n$$\n\\|f\\|=\\sup \\{|f(x)|: x \\in X\\} .\n$$\n\nThen $\\|\\cdot\\|$ is a norm on $b(X)$, as you should verify.\n\n- Let $C([0,1])$ denote the vector space of continuous functions from the interval $[0,1]$ to $\\mathbf{F}$. Define $\\|\\cdot\\|$ on $C([0,1])$ by\n\n$$\n\\|f\\|=\\int_{0}^{1}|f|\n$$\n\nThen $\\|\\cdot\\|$ is a norm on $C([0,1])$, as you should verify.\n\nSometimes examples that do not satisfy a definition help you gain understanding.\n\n### 6.35 Example not norms\n\n- Let $\\mathcal{L}^{1}(\\mathbf{R})$ denote the vector space of Borel (or Lebesgue) measurable functions $f: \\mathbf{R} \\rightarrow \\mathbf{F}$ such that $\\int|f| d \\lambda<\\infty$, where $\\lambda$ is Lebesgue measure on $\\mathbf{R}$. Define $\\|\\cdot\\|_{1}$ on $\\mathcal{L}^{1}(\\mathbf{R})$ by\n\n$$\n\\|f\\|_{1}=\\int|f| d \\lambda\n$$\n\nThen $\\|\\cdot\\|_{1}$ satisfies the homogeneity condition and the triangle inequality on $\\mathcal{L}^{1}(\\mathbf{R})$, as you should verify. However, $\\|\\cdot\\|_{1}$ is not a norm on $\\mathcal{L}^{1}(\\mathbf{R})$ because the positive definite condition is not satisfied. Specifically, if $E$ is a nonempty Borel subset of $\\mathbf{R}$ with Lebesgue measure 0 (for example, $E$ might consist of a single element of $\\mathbf{R}$ ), then $\\left\\|\\chi_{E}\\right\\|_{1}=0$ but $\\chi_{E} \\neq 0$. In the next chapter, we will discuss a modification of $\\mathcal{L}^{1}(\\mathbf{R})$ that removes this problem.\n\n- If $n \\in \\mathbf{Z}^{+}$and $\\|\\cdot\\|$ is defined on $\\mathbf{F}^{n}$ by\n\n$$\n\\left\\|\\left(a_{1}, \\ldots, a_{n}\\right)\\right\\|=\\left|a_{1}\\right|^{1 / 2}+\\cdots+\\left|a_{n}\\right|^{1 / 2},\n$$\n\nthen $\\|\\cdot\\|$ satisfies the positive definite condition and the triangle inequality (as you should verify). However, $\\|\\cdot\\|$ as defined above is not a norm because it does not satisfy the homogeneity condition.\n\n- If $\\|\\cdot\\|_{1 / 2}$ is defined on $\\mathbf{F}^{n}$ by\n\n$$\n\\left\\|\\left(a_{1}, \\ldots, a_{n}\\right)\\right\\|_{1 / 2}=\\left(\\left|a_{1}\\right|^{1 / 2}+\\cdots+\\left|a_{n}\\right|^{1 / 2}\\right)^{2},\n$$\n\nthen $\\|\\cdot\\|_{1 / 2}$ satisfies the positive definite condition and the homogeneity condition. However, if $n>1$ then $\\|\\cdot\\|_{1 / 2}$ is not a norm on $\\mathbf{F}^{n}$ because the triangle inequality is not satisfied (as you should verify).\n\nThe next result shows that every normed vector space is also a metric space in a natural fashion.\n\n### 6.36 normed vector spaces are metric spaces\n\nSuppose $(V,\\|\\cdot\\|)$ is a normed vector space. Define $d: V \\times V \\rightarrow[0, \\infty)$ by\n\n$$\nd(f, g)=\\|f-g\\| \\text {. }\n$$\n\nThen $d$ is a metric on $V$.\n\nProof Suppose $f, g, h \\in V$. Then\n\n$$\n\\begin{aligned}\nd(f, h)=\\|f-h\\| & =\\|(f-g)+(g-h)\\| \\\\\n& \\leq\\|f-g\\|+\\|g-h\\| \\\\\n& =d(f, g)+d(g, h) .\n\\end{aligned}\n$$\n\nThus the triangle inequality requirement for a metric is satisfied. The verification of the other required properties for a metric are left to the reader.\n\nFrom now on, all metric space notions in the context of a normed vector space should be interpreted with respect to the metric introduced in the previous result. However, usually there is no need to introduce the metric $d$ explicitly—just use the norm of the difference of two elements. For example, suppose $(V,\\|\\cdot\\|)$ is a normed vector space, $f_{1}, f_{2}, \\ldots$ is a sequence in $V$, and $f \\in V$. Then in the context of a normed vector space, the definition of limit (6.8) becomes the following statement:\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}=f \\text { means } \\lim _{k \\rightarrow \\infty}\\left\\|f_{k}-f\\right\\|=0\n$$\n\nAs another example, in the context of a normed vector space, the definition of a Cauchy sequence (6.12) becomes the following statement:\n\nA sequence $f_{1}, f_{2}, \\ldots$ in a normed vector space $(V,\\|\\cdot\\|)$ is a Cauchy sequence if for every $\\varepsilon>0$, there exists $n \\in \\mathbf{Z}^{+}$such that $\\left\\|f_{j}-f_{k}\\right\\|<\\varepsilon$ for all integers $j \\geq n$ and $k \\geq n$.\n\nEvery sequence in a normed vector space that has a limit is a Cauchy sequence (see 6.13). Normed vector spaces that satisfy the converse have a special name.\n\n### 6.37 Definition Banach space\n\nA complete normed vector space is called a Banach space.\n\nIn other words, a normed vector space $V$ is a Banach space if every Cauchy sequence in $V$ converges to some element of $V$.\n\nThe verifications of the assertions in Examples 6.38 and 6.39 below are left to the reader as exercises.\n\nIn a slight abuse of terminology, we often refer to a normed vector space $V$ without mentioning the norm $\\|\\cdot\\|$. When that happens, you should assume that a norm $\\|\\cdot\\|$ lurks nearby, even if it is not explicitly displayed.\n\n### 6.38 Example Banach spaces\n\n- The vector space $C([0,1])$ with the norm defined by $\\|f\\|=\\sup |f|$ is a Banach space.\n- The vector space $\\ell^{1}$ with the norm defined by $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{1}=\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|$ is a Banach space.\n\n### 6.39 Example not a Banach space\n\n- The vector space $C([0,1])$ with the norm defined by $\\|f\\|=\\int_{0}^{1}|f|$ is not a Banach space.\n- The vector space $\\ell^{1}$ with the norm defined by $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{\\infty}=\\sup \\left|a_{k}\\right|$ is not a Banach space.\n\n### 6.40 Definition infinite sum in a normed vector space\n\nSuppose $g_{1}, g_{2}, \\ldots$ is a sequence in a normed vector space $V$. Then $\\sum_{k=1}^{\\infty} g_{k}$ is defined by\n\n$$\n\\sum_{k=1}^{\\infty} g_{k}=\\lim _{n \\rightarrow \\infty} \\sum_{k=1}^{n} g_{k}\n$$\n\nif this limit exists, in which case the infinite series is said to converge.\n\nRecall from your calculus course that if $a_{1}, a_{2}, \\ldots$ is a sequence of real numbers such that $\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|<\\infty$, then $\\sum_{k=1}^{\\infty} a_{k}$ converges. The next result states that the analogous property for normed vector spaces characterizes Banach spaces.\n\n$6.41\\left(\\sum_{k=1}^{\\infty}\\left\\|g_{k}\\right\\|<\\infty \\Longrightarrow \\sum_{k=1}^{\\infty} g_{k}\\right.$ converges $) \\Longleftrightarrow$ Banach space\n\nSuppose $V$ is a normed vector space. Then $V$ is a Banach space if and only if $\\sum_{k=1}^{\\infty} g_{k}$ converges for every sequence $g_{1}, g_{2}, \\ldots$ in $V$ such that $\\sum_{k=1}^{\\infty}\\left\\|g_{k}\\right\\|<\\infty$.\n\nProof First suppose $V$ is a Banach space. Suppose $g_{1}, g_{2}, \\ldots$ is a sequence in $V$ such that $\\sum_{k=1}^{\\infty}\\left\\|g_{k}\\right\\|<\\infty$. Suppose $\\varepsilon>0$. Let $n \\in \\mathbf{Z}^{+}$be such that $\\sum_{m=n}^{\\infty}\\left\\|g_{m}\\right\\|<\\varepsilon$. For $j \\in \\mathbf{Z}^{+}$, let $f_{j}$ denote the partial sum defined by\n\n$$\nf_{j}=g_{1}+\\cdots+g_{j} .\n$$\n\nIf $k>j \\geq n$, then\n\n$$\n\\begin{aligned}\n\\left\\|f_{k}-f_{j}\\right\\| & =\\left\\|g_{j+1}+\\cdots+g_{k}\\right\\| \\\\\n& \\leq\\left\\|g_{j+1}\\right\\|+\\cdots+\\left\\|g_{k}\\right\\| \\\\\n& \\leq \\sum_{m=n}^{\\infty}\\left\\|g_{m}\\right\\| \\\\\n& <\\varepsilon .\n\\end{aligned}\n$$\n\nThus $f_{1}, f_{2}, \\ldots$ is a Cauchy sequence in $V$. Because $V$ is a Banach space, we conclude that $f_{1}, f_{2}, \\ldots$ converges to some element of $V$, which is precisely what it means for $\\sum_{k=1}^{\\infty} g_{k}$ to converge, completing one direction of the proof.\n\nTo prove the other direction, suppose $\\sum_{k=1}^{\\infty} g_{k}$ converges for every sequence $g_{1}, g_{2}, \\ldots$ in $V$ such that $\\sum_{k=1}^{\\infty}\\left\\|g_{k}\\right\\|<\\infty$. Suppose $f_{1}, f_{2}, \\ldots$ is a Cauchy sequence in $V$. We want to prove that $f_{1}, f_{2}, \\ldots$ converges to some element of $V$. It suffices to show that some subsequence of $f_{1}, f_{2}, \\ldots$ converges (by Exercise 14 in Section 6A). Dropping to a subsequence (but not relabeling) and setting $f_{0}=0$, we can assume that\n\n$$\n\\sum_{k=1}^{\\infty}\\left\\|f_{k}-f_{k-1}\\right\\|<\\infty\n$$\n\nHence $\\sum_{k=1}^{\\infty}\\left(f_{k}-f_{k-1}\\right)$ converges. The partial sum of this series after $n$ terms is $f_{n}$. Thus $\\lim _{n \\rightarrow \\infty} f_{n}$ exists, completing the proof.\n\n## Bounded Linear Maps\n\nWhen dealing with two or more vector spaces, as in the definition below, assume that the vector spaces are over the same field (either $\\mathbf{R}$ or $\\mathbf{C}$, but denoted in this book as $\\mathbf{F}$ to give us the flexibility to consider both cases).\n\nThe notation $T f$, in addition to the standard functional notation $T(f)$, is often used when considering linear maps, which we now define.\n\n### 6.42 Definition linear map\n\nSuppose $V$ and $W$ are vector spaces. A function $T: V \\rightarrow W$ is called linear if\n\n- $T(f+g)=T f+T g$ for all $f, g \\in V$;\n- $T(\\alpha f)=\\alpha T f$ for all $\\alpha \\in \\mathbf{F}$ and $f \\in V$.\n\nA linear function is often called a linear map.\n\nThe set of linear maps from a vector space $V$ to a vector space $W$ is itself a vector space, using the usual operations of addition and scalar multiplication of functions. Most attention in analysis focuses on the subset of bounded linear functions, defined below, which we will see is itself a normed vector space.\n\nIn the next definition, we have two normed vector spaces, $V$ and $W$, which may have different norms. However, we use the same notation $\\|\\cdot\\|$ for both norms (and for the norm of a linear map from $V$ to $W$ ) because the context makes the meaning clear. For example, in the definition below, $f$ is in $V$ and thus $\\|f\\|$ refers to the norm in $V$. Similarly, $T f \\in W$ and thus $\\|T f\\|$ refers to the norm in $W$.\n\n6.43 Definition bounded linear map; $\\|T\\| ; \\mathcal{B}(V, W)$\n\nSuppose $V$ and $W$ are normed vector spaces and $T: V \\rightarrow W$ is a linear map.\n\n- The norm of $T$, denoted $\\|T\\|$, is defined by\n\n$$\n\\|T\\|=\\sup \\{\\|T f\\|: f \\in V \\text { and }\\|f\\| \\leq 1\\} .\n$$\n\n- $T$ is called bounded if $\\|T\\|<\\infty$.\n- The set of bounded linear maps from $V$ to $W$ is denoted $\\mathcal{B}(V, W)$.\n\n### 6.44 Example bounded linear map\n\nLet $C([0,3])$ be the normed vector space of continuous functions from $[0,3]$ to $\\mathbf{F}$, with $\\|f\\|=\\sup |f|$. Define $T: C([0,3]) \\rightarrow C([0,3])$ by\n\n$$\n(T f)(x)=x^{2} f(x) .\n$$\n\nThen $T$ is a bounded linear map and $\\|T\\|=9$, as you should verify.\n\n### 6.45 Example linear map that is not bounded\n\nLet $V$ be the normed vector space of sequences $\\left(a_{1}, a_{2}, \\ldots\\right)$ of elements of $\\mathbf{F}$ such that $a_{k}=0$ for all but finitely many $k \\in \\mathbf{Z}^{+}$, with $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{\\infty}=\\max _{k \\in \\mathbf{Z}^{+}}\\left|a_{k}\\right|$. Define $T: V \\rightarrow V$ by\n\n$$\nT\\left(a_{1}, a_{2}, a_{3}, \\ldots\\right)=\\left(a_{1}, 2 a_{2}, 3 a_{3}, \\ldots\\right) .\n$$\n\nThen $T$ is a linear map that is not bounded, as you should verify.\n\nThe next result shows that if $V$ and $W$ are normed vector spaces, then $\\mathcal{B}(V, W)$ is a normed vector space with the norm defined above.\n\n## $6.46\\|\\cdot\\|$ is a norm on $\\mathcal{B}(V, W)$\n\nSuppose $V$ and $W$ are normed vector spaces. Then $\\|S+T\\| \\leq\\|S\\|+\\|T\\|$ and $\\|\\alpha T\\|=|\\alpha|\\|T\\|$ for all $S, T \\in \\mathcal{B}(V, W)$ and all $\\alpha \\in \\mathbf{F}$. Furthermore, the function $\\|\\cdot\\|$ is a norm on $\\mathcal{B}(V, W)$.\n\nProof Suppose $S, T \\in \\mathcal{B}(V, W)$. Then\n\n$$\n\\begin{aligned}\n\\|S+T\\|= & \\sup \\{\\|(S+T) f\\|: f \\in V \\text { and }\\|f\\| \\leq 1\\} \\\\\n\\leq & \\sup \\{\\|S f\\|+\\|T f\\|: f \\in V \\text { and }\\|f\\| \\leq 1\\} \\\\\n\\leq & \\sup \\{\\|S f\\|: f \\in V \\text { and }\\|f\\| \\leq 1\\} \\\\\n& \\quad \\quad+\\sup \\{\\|T f\\|: f \\in V \\text { and }\\|f\\| \\leq 1\\} \\\\\n& =\\|S\\|+\\|T\\| .\n\\end{aligned}\n$$\n\nThe inequality above shows that $\\|\\cdot\\|$ satisfies the triangle inequality on $\\mathcal{B}(V, W)$. The verification of the other properties required for a normed vector space is left to the reader.\n\nBe sure that you are comfortable using all four equivalent formulas for $\\|T\\|$ shown in Exercise 16. For example, you should often think of $\\|T\\|$ as the smallest number such that $\\|T f\\| \\leq\\|T\\|\\|f\\|$ for all $f$ in the domain of $T$.\n\nNote that in the next result, the hypothesis requires $W$ to be a Banach space but there is no requirement for $V$ to be a Banach space.\n\n### 6.47 $\\mathcal{B}(V, W)$ is a Banach space if $W$ is a Banach space\n\nSuppose $V$ is a normed vector space and $W$ is a Banach space. Then $\\mathcal{B}(V, W)$ is a Banach space.\n\nProof Suppose $T_{1}, T_{2}, \\ldots$ is a Cauchy sequence in $\\mathcal{B}(V, W)$. If $f \\in V$, then\n\n$$\n\\left\\|T_{j} f-T_{k} f\\right\\| \\leq\\left\\|T_{j}-T_{k}\\right\\|\\|f\\|,\n$$\n\nwhich implies that $T_{1} f, T_{2} f, \\ldots$ is a Cauchy sequence in $W$. Because $W$ is a Banach space, this implies that $T_{1} f, T_{2} f, \\ldots$ has a limit in $W$, which we call $T f$.\n\nWe have now defined a function $T: V \\rightarrow W$. The reader should verify that $T$ is a linear map. Clearly\n\n$$\n\\begin{aligned}\n\\|T f\\| & \\leq \\sup \\left\\{\\left\\|T_{k} f\\right\\|: k \\in \\mathbf{Z}^{+}\\right\\} \\\\\n& \\leq\\left(\\sup \\left\\{\\left\\|T_{k}\\right\\|: k \\in \\mathbf{Z}^{+}\\right\\}\\right)\\|f\\|\n\\end{aligned}\n$$\n\nfor each $f \\in V$. The last supremum above is finite because every Cauchy sequence is bounded (see Exercise 4). Thus $T \\in \\mathcal{B}(V, W)$.\n\nWe still need to show that $\\lim _{k \\rightarrow \\infty}\\left\\|T_{k}-T\\right\\|=0$. To do this, suppose $\\varepsilon>0$. Let $n \\in \\mathbf{Z}^{+}$be such that $\\left\\|T_{j}-T_{k}\\right\\|<\\varepsilon$ for all $j \\geq n$ and $k \\geq n$. Suppose $j \\geq n$ and suppose $f \\in V$. Then\n\n$$\n\\begin{aligned}\n\\left\\|\\left(T_{j}-T\\right) f\\right\\| & =\\lim _{k \\rightarrow \\infty}\\left\\|T_{j} f-T_{k} f\\right\\| \\\\\n& \\leq \\varepsilon\\|f\\| .\n\\end{aligned}\n$$\n\nThus $\\left\\|T_{j}-T\\right\\| \\leq \\varepsilon$, completing the proof.\n\nThe next result shows that the phrase bounded linear map means the same as the phrase continuous linear map.\n\n6.48 continuity is equivalent to boundedness for linear maps\n\nA linear map from one normed vector space to another normed vector space is continuous if and only if it is bounded.\n\nProof Suppose $V$ and $W$ are normed vector spaces and $T: V \\rightarrow W$ is linear.\n\nFirst suppose $T$ is not bounded. Thus there exists a sequence $f_{1}, f_{2}, \\ldots$ in $V$ such that $\\left\\|f_{k}\\right\\| \\leq 1$ for each $k \\in \\mathbf{Z}^{+}$and $\\left\\|T f_{k}\\right\\| \\rightarrow \\infty$ as $k \\rightarrow \\infty$. Hence\n\n$$\n\\lim _{k \\rightarrow \\infty} \\frac{f_{k}}{\\left\\|T f_{k}\\right\\|}=0 \\quad \\text { and } \\quad T\\left(\\frac{f_{k}}{\\left\\|T f_{k}\\right\\|}\\right)=\\frac{T f_{k}}{\\left\\|T f_{k}\\right\\|} \\not \\rightarrow 0\n$$\n\nwhere the nonconvergence to 0 holds because $T f_{k} /\\left\\|T f_{k}\\right\\|$ has norm 1 for every $k \\in \\mathbf{Z}^{+}$. The displayed line above implies that $T$ is not continuous, completing the proof in one direction.\n\nTo prove the other direction, now suppose $T$ is bounded. Suppose $f \\in V$ and $f_{1}, f_{2}, \\ldots$ is a sequence in $V$ such that $\\lim _{k \\rightarrow \\infty} f_{k}=f$. Then\n\n$$\n\\begin{aligned}\n\\left\\|T f_{k}-T f\\right\\| & =\\left\\|T\\left(f_{k}-f\\right)\\right\\| \\\\\n& \\leq\\|T\\|\\left\\|f_{k}-f\\right\\| .\n\\end{aligned}\n$$\n\nThus $\\lim _{k \\rightarrow \\infty} T f_{k}=T f$. Hence $T$ is continuous, completing the proof in the other direction.\n\nExercise 18 gives several additional equivalent conditions for a linear map to be continuous.\n\n## EXERCISES 6C\n\n1 Show that the map $f \\mapsto\\|f\\|$ from a normed vector space $V$ to $\\mathbf{F}$ is continuous (where the norm on $\\mathbf{F}$ is the usual absolute value).\n\n2 Prove that if $V$ is a normed vector space, $f \\in V$, and $r>0$, then\n\n$$\n\\overline{B(f, r)}=\\bar{B}(f, r) .\n$$\n\n3 Show that the functions defined in the last two bullet points of Example 6.35 are not norms.\n\n4 Prove that each Cauchy sequence in a normed vector space is bounded (meaning that there is a real number that is greater than the norm of every element in the Cauchy sequence).\n\n5 Show that if $n \\in \\mathbf{Z}^{+}$, then $\\mathbf{F}^{n}$ is a Banach space with both the norms used in the first bullet point of Example 6.34.\n\n6 Suppose $X$ is a nonempty set and $b(X)$ is the vector space of bounded functions from $X$ to $\\mathbf{F}$. Prove that if $\\|\\cdot\\|$ is defined on $b(X)$ by $\\|f\\|=\\sup _{X}|f|$, then $b(X)$ is a Banach space.\n\n7 Show that $\\ell^{1}$ with the norm defined by $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{\\infty}=\\sup \\left|a_{k}\\right|$ is not a Banach space.\n\n8 Show that $\\ell^{1}$ with the norm defined by $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{1}=\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|$ is a Banach space.\n\n9 Show that the vector space $C([0,1])$ of continuous functions from $[0,1]$ to $\\mathbf{F}$ with the norm defined by $\\|f\\|=\\int_{0}^{1}|f|$ is not a Banach space.\n\n10 Suppose $U$ is a subspace of a normed vector space $V$ such that some open ball of $V$ is contained in $U$. Prove that $U=V$.\n\n11 Prove that the only subsets of a normed vector space $V$ that are both open and closed are $\\varnothing$ and $V$.\n\n12 Suppose $V$ is a normed vector space. Prove that the closure of each subspace of $V$ is a subspace of $V$.\n\n13 Suppose $U$ is a normed vector space. Let $d$ be the metric on $U$ defined by $d(f, g)=\\|f-g\\|$ for $f, g \\in U$. Let $V$ be the complete metric space constructed in Exercise 16 in Section 6A.\n\n(a) Show that the set $V$ is a vector space under natural operations of addition and scalar multiplication.\n\n(b) Show that there is a natural way to make $V$ into a normed vector space and that with this norm, $V$ is a Banach space.\n\n(c) Explain why (b) shows that every normed vector space is a subspace of some Banach space.\n\n14 Suppose $U$ is a subspace of a normed vector space $V$. Suppose also that $W$ is a Banach space and $S: U \\rightarrow W$ is a bounded linear map.\n\n(a) Prove that there exists a unique continuous function $T: \\bar{U} \\rightarrow W$ such that $\\left.T\\right|_{U}=S$.\n\n(b) Prove that the function $T$ in part (a) is a bounded linear map from $\\bar{U}$ to $W$ and $\\|T\\|=\\|S\\|$.\n\n(c) Give an example to show that part (a) can fail if the assumption that $W$ is a Banach space is replaced by the assumption that $W$ is a normed vector space.\n\n15 For readers familiar with the quotient of a vector space and a subspace: Suppose $V$ is a normed vector space and $U$ is a subspace of $V$. Define $\\|\\cdot\\|$ on $V / U$ by\n\n$$\n\\|f+U\\|=\\inf \\{\\|f+g\\|: g \\in U\\} .\n$$\n\n(a) Prove that $\\|\\cdot\\|$ is a norm on $V / U$ if and only if $U$ is a closed subspace of $V$.\n\n(b) Prove that if $V$ is a Banach space and $U$ is a closed subspace of $V$, then $V / U$ (with the norm defined above) is a Banach space.\n\n(c) Prove that if $U$ is a Banach space (with the norm it inherits from $V$ ) and $V / U$ is a Banach space (with the norm defined above), then $V$ is a Banach space.\n\n16 Suppose $V$ and $W$ are normed vector spaces with $V \\neq\\{0\\}$ and $T: V \\rightarrow W$ is a linear map.\n\n(a) Show that $\\|T\\|=\\sup \\{\\|T f\\|: f \\in V$ and $\\|f\\|<1\\}$.\n\n(b) Show that $\\|T\\|=\\sup \\{\\|T f\\|: f \\in V$ and $\\|f\\|=1\\}$.\n\n(c) Show that $\\|T\\|=\\inf \\{c \\in[0, \\infty):\\|T f\\| \\leq c\\|f\\|$ for all $f \\in V\\}$.\n\n(d) Show that $\\|T\\|=\\sup \\left\\{\\frac{\\|T f\\|}{\\|f\\|}: f \\in V\\right.$ and $\\left.f \\neq 0\\right\\}$.\n\n17 Suppose $U, V$, and $W$ are normed vector spaces and $T: U \\rightarrow V$ and $S: V \\rightarrow W$ are linear. Prove that $\\|S \\circ T\\| \\leq\\|S\\|\\|T\\|$.\n\n18 Suppose $V$ and $W$ are normed vector spaces and $T: V \\rightarrow W$ is a linear map. Prove that the following are equivalent:\n\n(a) $T$ is bounded.\n\n(b) There exists $f \\in V$ such that $T$ is continuous at $f$.\n\n(c) $T$ is uniformly continuous (which means that for every $\\varepsilon>0$, there exists $\\delta>0$ such that $\\|T f-T g\\|<\\varepsilon$ for all $f, g \\in V$ with $\\|f-g\\|<\\delta$ ).\n\n(d) $T^{-1}(B(0, r))$ is an open subset of $V$ for some $r>0$.\n\n## 6D Linear Functionals\n\n## Bounded Linear Functionals\n\nLinear maps into the scalar field $\\mathbf{F}$ are so important that they get a special name.\n\n### 6.49 Definition linear functional\n\nA linear functional on a vector space $V$ is a linear map from $V$ to $\\mathbf{F}$.\n\nWhen we think of the scalar field $\\mathbf{F}$ as a normed vector space, as in the next example, the norm $\\|z\\|$ of a number $z \\in \\mathbf{F}$ is always intended to be just the usual absolute value $|z|$. This norm makes $\\mathbf{F}$ a Banach space.\n\n### 6.50 Example linear functional\n\nLet $V$ be the vector space of sequences $\\left(a_{1}, a_{2}, \\ldots\\right)$ of elements of $\\mathbf{F}$ such that $a_{k}=0$ for all but finitely many $k \\in \\mathbf{Z}^{+}$. Define $\\varphi: V \\rightarrow \\mathbf{F}$ by\n\n$$\n\\varphi\\left(a_{1}, a_{2}, \\ldots\\right)=\\sum_{k=1}^{\\infty} a_{k}\n$$\n\nThen $\\varphi$ is a linear functional on $V$.\n\n- If we make $V$ a normed vector space with the norm $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{1}=\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|$,\n  then $\\varphi$ is a bounded linear functional on $V$, as you should verify.\n- If we make $V$ a normed vector space with the norm $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{\\infty}=\\max _{k \\in \\mathbf{Z}^{+}}\\left|a_{k}\\right|$, then $\\varphi$ is not a bounded linear functional on $V$, as you should verify.\n\n### 6.51 Definition null space; null T\n\nSuppose $V$ and $W$ are vector spaces and $T: V \\rightarrow W$ is a linear map. Then the null space of $T$ is denoted by null $T$ and is defined by\n\n$$\n\\operatorname{null} T=\\{f \\in V: T f=0\\}\n$$\n\nIf $T$ is a linear map on a vector space $V$, then null $T$ is a subspace of $V$, as you should verify. If $T$ is a continuous linear map from a normed vector space $V$ to a normed vector space $W$, then null $T$ is a closed subspace of $V$ because null $T=$ $T^{-1}(\\{0\\})$ and the inverse image of the\n\nThe term kernel is also used in the mathematics literature with the same meaning as null space. This book uses null space instead of kernel because null space better captures the connection with 0 . closed set $\\{0\\}$ is closed [by 6.11(d)].\n\nThe converse of the last sentence fails, because a linear map between normed vector spaces can have a closed null space but not be continuous. For example, the linear map in 6.45 has a closed null space (equal to $\\{0\\}$ ) but it is not continuous.\n\nHowever, the next result states that for linear functionals, as opposed to more general linear maps, having a closed null space is equivalent to continuity.\n\n### 6.52 bounded linear functionals\n\nSuppose $V$ is a normed vector space and $\\varphi: V \\rightarrow \\mathbf{F}$ is a linear functional that is not identically 0 . Then the following are equivalent:\n\n(a) $\\varphi$ is a bounded linear functional.\n\n(b) $\\varphi$ is a continuous linear functional.\n\n(c) null $\\varphi$ is a closed subspace of $V$.\n\n(d) $\\overline{\\text { null } \\varphi} \\neq V$.\n\nProof The equivalence of (a) and (b) is just a special case of 6.48.\n\nTo prove that (b) implies (c), suppose $\\varphi$ is a continuous linear functional. Then null $\\varphi$, which is the inverse image of the closed set $\\{0\\}$, is a closed subset of $V$ by 6.11(d). Thus (b) implies (c).\n\nTo prove that (c) implies (a), we will show that the negation of (a) implies the negation of (c). Thus suppose $\\varphi$ is not bounded. Thus there is a sequence $f_{1}, f_{2}, \\ldots$ in $V$ such that $\\left\\|f_{k}\\right\\| \\leq 1$ and $\\left|\\varphi\\left(f_{k}\\right)\\right| \\geq k$ for each $k \\in \\mathbf{Z}^{+}$. Now\n\n$$\n\\frac{f_{1}}{\\varphi\\left(f_{1}\\right)}-\\frac{f_{k}}{\\varphi\\left(f_{k}\\right)} \\in \\operatorname{null} \\varphi\n$$\n\nfor each $k \\in \\mathbf{Z}^{+}$and\n\n$$\n\\lim _{k \\rightarrow \\infty}\\left(\\frac{f_{1}}{\\varphi\\left(f_{1}\\right)}-\\frac{f_{k}}{\\varphi\\left(f_{k}\\right)}\\right)=\\frac{f_{1}}{\\varphi\\left(f_{1}\\right)}\n$$\n\nThis proof makes major use of dividing by expressions of the form $\\varphi(f)$, which would not make sense for a linear mapping into a vector space other than $\\mathbf{F}$.\n\nClearly\n\n$$\n\\varphi\\left(\\frac{f_{1}}{\\varphi\\left(f_{1}\\right)}\\right)=1 \\text { and thus } \\frac{f_{1}}{\\varphi\\left(f_{1}\\right)} \\notin \\text { null } \\varphi \\text {. }\n$$\n\nThe last three displayed items imply that null $\\varphi$ is not closed, completing the proof that the negation of (a) implies the negation of (c). Thus (c) implies (a).\n\nWe now know that (a), (b), and (c) are equivalent to each other.\n\nUsing the hypothesis that $\\varphi$ is not identically 0 , we see that (c) implies (d). To complete the proof, we need only show that (d) implies (c), which we will do by showing that the negation of (c) implies the negation of (d). Thus suppose null $\\varphi$ is not a closed subspace of $V$. Because null $\\varphi$ is a subspace of $V$, we know that null $\\varphi$ is also a subspace of $V$ (see Exercise 12 in Section 6C). Let $f \\in \\overline{\\operatorname{null} \\varphi} \\backslash$ null $\\varphi$. Suppose $g \\in V$. Then\n\n$$\ng=\\left(g-\\frac{\\varphi(g)}{\\varphi(f)} f\\right)+\\frac{\\varphi(g)}{\\varphi(f)} f\n$$\n\nThe term in large parentheses above is in null $\\varphi$ and hence is in $\\overline{\\text { null } \\varphi}$. The term above following the plus sign is a scalar multiple of $f$ and thus is in null $\\varphi$. Because the equation above writes $g$ as the sum of two elements of $\\overline{\\text { null } \\varphi}$, we conclude that $g \\in \\overline{\\text { null } \\varphi}$. Hence we have shown that $V=\\overline{\\text { null } \\varphi}$, completing the proof that the negation of (c) implies the negation of (d).\n\n## Discontinuous Linear Functionals\n\nThe second bullet point in Example 6.50 shows that there exists a discontinuous linear functional on a certain normed vector space. Our next major goal is to show that every infinite-dimensional normed vector space has a discontinuous linear functional (see 6.62). Thus infinite-dimensional normed vector spaces behave in this respect much differently from $\\mathbf{F}^{n}$, where all linear functionals are continuous (see Exercise 4).\n\nWe need to extend the notion of a basis of a finite-dimensional vector space to an infinite-dimensional context. In a finite-dimensional vector space, we might consider a basis of the form $e_{1}, \\ldots, e_{n}$, where $n \\in \\mathbf{Z}^{+}$and each $e_{k}$ is an element of our vector space. We can think of the list $e_{1}, \\ldots, e_{n}$ as a function from $\\{1, \\ldots, n\\}$ to our vector space, with the value of this function at $k \\in\\{1, \\ldots, n\\}$ denoted by $e_{k}$ with a subscript $k$ instead of by the usual functional notation $e(k)$. To generalize, in the next definition we allow $\\{1, \\ldots, n\\}$ to be replaced by an arbitrary set that might not be a finite set.\n\n### 6.53 Definition family\n\nA family $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ in a set $V$ is a function $e$ from a set $\\Gamma$ to $V$, with the value of the function $e$ at $k \\in \\Gamma$ denoted by $e_{k}$.\n\nEven though a family in $V$ is a function mapping into $V$ and thus is not a subset of $V$, the set terminology and the bracket notation $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ are useful, and the range of a family in $V$ really is a subset of $V$.\n\nWe now restate some basic linear algebra concepts, but in the context of vector spaces that might be infinite-dimensional. Note that only finite sums appear in the definition below, even though we might be working with an infinite family.\n\n### 6.54 Definition linearly independent; span; finite-dimensional; basis\n\nSuppose $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is a family in a vector space $V$.\n\n- $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is called linearly independent if there does not exist a finite nonempty subset $\\Omega$ of $\\Gamma$ and a family $\\left\\{\\alpha_{j}\\right\\}_{j \\in \\Omega}$ in $\\mathbf{F} \\backslash\\{0\\}$ such that $\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}=0$.\n- The span of $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is denoted by span $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ and is defined to be the set of all sums of the form\n\n$$\n\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}\n$$\n\nwhere $\\Omega$ is a finite subset of $\\Gamma$ and $\\left\\{\\alpha_{j}\\right\\}_{j \\in \\Omega}$ is a family in $\\mathbf{F}$.\n\n- A vector space $V$ is called finite-dimensional if there exists a finite set $\\Gamma$ and a family $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ in $V$ such that $\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}=V$.\n- A vector space is called infinite-dimensional if it is not finite-dimensional.\n- A family in $V$ is called a basis of $V$ if it is linearly independent and its span equals $V$.\n\nFor example, $\\left\\{x^{n}\\right\\}_{n \\in\\{0,1,2, \\ldots\\}}$ is a basis of the vector space of polynomials.\n\nOur definition of span does not take advantage of the possibility of summing an infinite number of elements in contexts where a notion of limit exists (as is the\n\nThe term Hamel basis is sometimes used to denote what has been called $a$ basis here. The use of the term Hamel basis emphasizes that only finite sums are under consideration. case in normed vector spaces). When we get to Hilbert spaces in Chapter 8, we consider another kind of basis that does involve infinite sums. As we will soon see, the kind of basis as defined here is just what we need to produce discontinuous linear functionals.\n\nNow we introduce terminology that will be needed in our proof that every vector space has a basis.\n\nNo one has ever produced a concrete example of a basis of an infinite-dimensional Banach space.\n\n### 6.55 Definition maximal element\n\nSuppose $\\mathcal{A}$ is a collection of subsets of a set $V$. A set $\\Gamma \\in \\mathcal{A}$ is called a maximal element of $\\mathcal{A}$ if there does not exist $\\Gamma^{\\prime} \\in \\mathcal{A}$ such that $\\Gamma \\varsubsetneqq \\Gamma^{\\prime}$.\n\n### 6.56 Example maximal elements\n\nFor $k \\in \\mathbf{Z}$, let $k \\mathbf{Z}$ denote the set of integer multiples of $k$; thus $k \\mathbf{Z}=\\{k m: m \\in \\mathbf{Z}\\}$. Let $\\mathcal{A}$ be the collection of subsets of $\\mathbf{Z}$ defined by $\\mathcal{A}=\\{k \\mathbf{Z}: k=2,3,4, \\ldots\\}$. Suppose $k \\in \\mathbf{Z}^{+}$. Then $k \\mathbf{Z}$ is a maximal element of $\\mathcal{A}$ if and only if $k$ is a prime number, as you should verify.\n\nA subset $\\Gamma$ of a vector space $V$ can be thought of as a family in $V$ by considering $\\left\\{e_{f}\\right\\}_{f \\in \\Gamma}$, where $e_{f}=f$. With this convention, the next result shows that the bases of $V$ are exactly the maximal elements among the collection of linearly independent subsets of $V$.\n\n### 6.57 bases as maximal elements\n\nSuppose $V$ is a vector space. Then a subset of $V$ is a basis of $V$ if and only if it is a maximal element of the collection of linearly independent subsets of $V$.\n\nProof Suppose $\\Gamma$ is a linearly independent subset of $V$.\n\nFirst suppose also that $\\Gamma$ is a basis of $V$. If $f \\in V$ but $f \\notin \\Gamma$, then $f \\in \\operatorname{span} \\Gamma$, which implies that $\\Gamma \\cup\\{f\\}$ is not linearly independent. Thus $\\Gamma$ is a maximal element among the collection of linearly independent subsets of $V$, completing one direction of the proof.\n\nTo prove the other direction, suppose now that $\\Gamma$ is a maximal element of the collection of linearly independent subsets of $V$. If $f \\in V$ but $f \\notin \\operatorname{span} \\Gamma$, then $\\Gamma \\cup\\{f\\}$ is linearly independent, which would contradict the maximality of $\\Gamma$ among the collection of linearly independent subsets of $V$. Thus span $\\Gamma=V$, which means that $\\Gamma$ is a basis of $V$, completing the proof in the other direction.\n\nThe notion of a chain plays a key role in our next result.\n\n### 6.58 Definition chain\n\nA collection $\\mathcal{C}$ of subsets of a set $V$ is called a chain if $\\Omega, \\Gamma \\in \\mathcal{C}$ implies $\\Omega \\subset \\Gamma$ or $\\Gamma \\subset \\Omega$.\n\n### 6.59 Example chains\n\n- The collection $\\mathcal{C}=\\{4 \\mathbf{Z}, 6 \\mathbf{Z}\\}$ of subsets of $\\mathbf{Z}$ is not a chain because neither of the sets $4 \\mathbf{Z}$ or $6 \\mathbf{Z}$ is a subset of the other.\n- The collection $\\mathcal{C}=\\left\\{2^{n} \\mathbf{Z}: n \\in \\mathbf{Z}^{+}\\right\\}$of subsets of $\\mathbf{Z}$ is a chain because if $m, n \\in \\mathbf{Z}^{+}$, then $2^{m} \\mathbf{Z} \\subset 2^{n} \\mathbf{Z}$ or $2^{n} \\mathbf{Z} \\subset 2^{m} \\mathbf{Z}$.\n\nThe next result follows from the Axiom of Choice, although it is not as intuitively believable as the Axiom of Choice. Because the techniques used to prove the next result are so different from techniques used elsewhere in this book, the\n\nZorn's Lemma is named in honor of Max Zorn (1906-1993), who published a paper containing the result in 1935, when he had a postdoctoral position at Yale. reader is asked either to accept this result without proof or find one of the good proofs available via the internet or in other books. The version of Zorn's Lemma stated here is simpler than the standard more general version, but this version is all that we need.\n\n### 6.60 Zorn's Lemma\n\nSuppose $V$ is a set and $\\mathcal{A}$ is a collection of subsets of $V$ with the property that the union of all the sets in $\\mathcal{C}$ is in $\\mathcal{A}$ for every chain $\\mathcal{C} \\subset \\mathcal{A}$. Then $\\mathcal{A}$ contains a maximal element.\n\nZorn's Lemma now allows us to prove that every vector space has a basis. The proof does not help us find a concrete basis because Zorn's Lemma is an existence result rather than a constructive technique.\n\n### 6.61 bases exist\n\nEvery vector space has a basis.\n\nProof Suppose $V$ is a vector space. If $\\mathcal{C}$ is a chain of linearly independent subsets of $V$, then the union of all the sets in $\\mathcal{C}$ is also a linearly independent subset of $V$ (this holds because linear independence is a condition that is checked by considering finite subsets, and each finite subset of the union is contained in one of the elements of the chain).\n\nThus if $\\mathcal{A}$ denotes the collection of linearly independent subsets of $V$, then $\\mathcal{A}$ satisfies the hypothesis of Zorn's Lemma (6.60). Hence $\\mathcal{A}$ contains a maximal element, which by 6.57 is a basis of $V$.\n\nNow we can prove the promised result about the existence of discontinuous linear functionals on every infinite-dimensional normed vector space.\n\n### 6.62 discontinuous linear functionals\n\nEvery infinite-dimensional normed vector space has a discontinuous linear functional.\n\nProof Suppose $V$ is an infinite-dimensional vector space. By 6.61, $V$ has a basis $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$. Because $V$ is infinite-dimensional, $\\Gamma$ is not a finite set. Thus we can assume $\\mathbf{Z}^{+} \\subset \\Gamma$ (by relabeling a countable subset of $\\Gamma$ ).\n\nDefine a linear functional $\\varphi: V \\rightarrow \\mathbf{F}$ by setting $\\varphi\\left(e_{j}\\right)$ equal to $j\\left\\|e_{j}\\right\\|$ for $j \\in \\mathbf{Z}^{+}$, setting $\\varphi\\left(e_{j}\\right)$ equal to 0 for $j \\in \\Gamma \\backslash \\mathbf{Z}^{+}$, and extending linearly. More precisely, define a linear functional $\\varphi: V \\rightarrow \\mathbf{F}$ by\n\n$$\n\\varphi\\left(\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}\\right)=\\sum_{j \\in \\Omega \\cap \\mathbf{Z}^{+}} \\alpha_{j} j\\left\\|e_{j}\\right\\|\n$$\n\nfor every finite subset $\\Omega \\subset \\Gamma$ and every family $\\left\\{\\alpha_{j}\\right\\}_{j \\in \\Omega}$ in $\\mathbf{F}$.\n\nBecause $\\varphi\\left(e_{j}\\right)=j\\left\\|e_{j}\\right\\|$ for each $j \\in \\mathbf{Z}^{+}$, the linear functional $\\varphi$ is unbounded, completing the proof.\n\n## Hahn-Banach Theorem\n\nIn the last subsection, we showed that there exists a discontinuous linear functional on each infinite-dimensional normed vector space. Now we turn our attention to the existence of continuous linear functionals.\n\nThe existence of a nonzero continuous linear functional on each Banach space is not obvious. For example, consider the Banach space $\\ell^{\\infty} / c_{0}$, where $\\ell^{\\infty}$ is the Banach space of bounded sequences in $\\mathbf{F}$ with\n\n$$\n\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{\\infty}=\\sup _{k \\in \\mathbf{Z}^{+}}\\left|a_{k}\\right|\n$$\n\nand $c_{0}$ is the subspace of $\\ell^{\\infty}$ consisting of those sequences in $\\mathbf{F}$ that have limit 0 . The quotient space $\\ell^{\\infty} / c_{0}$ is an infinite-dimensional Banach space (see Exercise 15 in Section 6C). However, no one has ever exhibited a concrete nonzero linear functional on the Banach space $\\ell^{\\infty} / c_{0}$.\n\nIn this subsection, we show that infinite-dimensional normed vector spaces have plenty of continuous linear functionals. We do this by showing that a bounded linear functional on a subspace of a normed vector space can be extended to a bounded linear functional on the whole space without increasing its norm-this result is called the Hahn-Banach Theorem (6.69).\n\nCompleteness plays no role in this topic. Thus this subsection deals with normed vector spaces instead of Banach spaces.\n\nWe do most of the work needed to prove the Hahn-Banach Theorem in the next lemma, which shows that we can extend a linear functional to a subspace generated by one additional element, without increasing the norm. This one-element-at-a-time approach, when combined with a maximal object produced by Zorn's Lemma, gives us the desired extension to the full normed vector space.\n\nIf $V$ is a real vector space, $U$ is a subspace of $V$, and $h \\in V$, then $U+\\mathbf{R} h$ is the subspace of $V$ defined by\n\n$$\nU+\\mathbf{R} h=\\{f+\\alpha h: f \\in U \\text { and } \\alpha \\in \\mathbf{R}\\} .\n$$\n\n### 6.63 Extension Lemma\n\nSuppose $V$ is a real normed vector space, $U$ is a subspace of $V$, and $\\psi: U \\rightarrow \\mathbf{R}$ is a bounded linear functional. Suppose $h \\in V \\backslash U$. Then $\\psi$ can be extended to a bounded linear functional $\\varphi: U+\\mathbf{R} h \\rightarrow \\mathbf{R}$ such that $\\|\\varphi\\|=\\|\\psi\\|$.\n\nProof Suppose $c \\in \\mathbf{R}$. Define $\\varphi(h)$ to be $c$, and then extend $\\varphi$ linearly to $U+\\mathbf{R} h$. Specifically, define $\\varphi: U+\\mathbf{R} h \\rightarrow \\mathbf{R}$ by\n\n$$\n\\varphi(f+\\alpha h)=\\psi(f)+\\alpha c\n$$\n\nfor $f \\in U$ and $\\alpha \\in \\mathbf{R}$. Then $\\varphi$ is a linear functional on $U+\\mathbf{R} h$.\n\nClearly $\\left.\\varphi\\right|_{U}=\\psi$. Thus $\\|\\varphi\\| \\geq\\|\\psi\\|$. We need to show that for some choice of $c \\in \\mathbf{R}$, the linear functional $\\varphi$ defined above satisfies the equation $\\|\\varphi\\|=\\|\\psi\\|$. In other words, we want\n\n6.64\n\n$$\n|\\psi(f)+\\alpha c| \\leq\\|\\psi\\|\\|f+\\alpha h\\| \\quad \\text { for all } f \\in U \\text { and all } \\alpha \\in \\mathbf{R}\n$$\n\nIt would be enough to have\n\n$$\n|\\psi(f)+c| \\leq\\|\\psi\\|\\|f+h\\| \\quad \\text { for all } f \\in U\n$$\n\nbecause replacing $f$ by $\\frac{f}{\\alpha}$ in the last inequality and then multiplying both sides by $|\\alpha|$ would give 6.64.\n\nRewriting 6.65, we want to show that there exists $c \\in \\mathbf{R}$ such that\n\n$$\n-\\|\\psi\\|\\|f+h\\| \\leq \\psi(f)+c \\leq\\|\\psi\\|\\|f+h\\| \\quad \\text { for all } f \\in U\n$$\n\nEquivalently, we want to show that there exists $c \\in \\mathbf{R}$ such that\n\n$$\n-\\|\\psi\\|\\|f+h\\|-\\psi(f) \\leq c \\leq\\|\\psi\\|\\|f+h\\|-\\psi(f) \\quad \\text { for all } f \\in U\n$$\n\nThe existence of $c \\in \\mathbf{R}$ satisfying the line above follows from the inequality\n\n$$\n\\sup _{f \\in U}(-\\|\\psi\\|\\|f+h\\|-\\psi(f)) \\leq \\inf _{g \\in U}(\\|\\psi\\|\\|g+h\\|-\\psi(g))\n$$\n\nTo prove the inequality above, suppose $f, g \\in U$. Then\n\n$$\n\\begin{aligned}\n-\\|\\psi\\|\\|f+h\\|-\\psi(f) & \\leq\\|\\psi\\|(\\|g+h\\|-\\|g-f\\|)-\\psi(f) \\\\\n& =\\|\\psi\\|(\\|g+h\\|-\\|g-f\\|)+\\psi(g-f)-\\psi(g) \\\\\n& \\leq\\|\\psi\\|\\|g+h\\|-\\psi(g) .\n\\end{aligned}\n$$\n\nThe inequality above proves 6.66 , which completes the proof.\n\nBecause our simplified form of Zorn's Lemma deals with set inclusions rather than more general orderings, we need to use the notion of the graph of a function.\n\n### 6.67 Definition graph\n\nSuppose $T: V \\rightarrow W$ is a function from a set $V$ to a set $W$. Then the graph of $T$ is denoted $\\operatorname{graph}(T)$ and is the subset of $V \\times W$ defined by\n\n$$\n\\operatorname{graph}(T)=\\{(f, T(f)) \\in V \\times W: f \\in V\\}\n$$\n\nFormally, a function from a set $V$ to a set $W$ equals its graph as defined above. However, because we usually think of a function more intuitively as a mapping, the separate notion of the graph of a function remains useful.\n\nThe easy proof of the next result is left to the reader. The first bullet point below uses the vector space structure of $V \\times W$, which is a vector space with natural operations of addition and scalar multiplication, as given in Exercise 10 in Section 6B.\n\n### 6.68 function properties in terms of graphs\n\nSuppose $V$ and $W$ are normed vector spaces and $T: V \\rightarrow W$ is a function.\n\n(a) $T$ is a linear map if and only if $\\operatorname{graph}(T)$ is a subspace of $V \\times W$.\n\n(b) Suppose $U \\subset V$ and $S: U \\rightarrow W$ is a function. Then $T$ is an extension of $S$ if and only if $\\operatorname{graph}(S) \\subset \\operatorname{graph}(T)$.\n\n(c) If $T: V \\rightarrow W$ is a linear map and $c \\in[0, \\infty)$, then $\\|T\\| \\leq c$ if and only if $\\|g\\| \\leq c\\|f\\|$ for all $(f, g) \\in \\operatorname{graph}(T)$.\n\nThe proof of the Extension Lemma (6.63) used inequalities that do not make sense when $\\mathbf{F}=\\mathbf{C}$. Thus the proof of the Hahn-Banach Theorem below requires some extra steps when $\\mathbf{F}=\\mathbf{C}$.\n\nHans Hahn (1879-1934) was a student and later a faculty member at the University of Vienna, where one of his PhD students was Kurt Gödel (1906-1978).\n\n### 6.69 Hahn-Banach Theorem\n\nSuppose $V$ is a normed vector space, $U$ is a subspace of $V$, and $\\psi: U \\rightarrow \\mathbf{F}$ is a bounded linear functional. Then $\\psi$ can be extended to a bounded linear functional on $V$ whose norm equals $\\|\\psi\\|$.\n\nProof First we consider the case where $\\mathbf{F}=\\mathbf{R}$. Let $\\mathcal{A}$ be the collection of subsets $E$ of $V \\times \\mathbf{R}$ that satisfy all the following conditions:\n\n- $E=\\operatorname{graph}(\\varphi)$ for some linear functional $\\varphi$ on some subspace of $V$;\n- $\\operatorname{graph}(\\psi) \\subset E$;\n- $|\\alpha| \\leq\\|\\psi\\|\\|f\\|$ for every $(f, \\alpha) \\in E$.\n\nThen $\\mathcal{A}$ satisfies the hypothesis of Zorn's Lemma (6.60). Thus $\\mathcal{A}$ has a maximal element. The Extension Lemma (6.63) implies that this maximal element is the graph of a linear functional defined on all of $V$. This linear functional is an extension of $\\psi$ to $V$ and it has norm $\\|\\psi\\|$, completing the proof in the case where $\\mathbf{F}=\\mathbf{R}$.\n\nNow consider the case where $\\mathbf{F}=\\mathbf{C}$. Define $\\psi_{1}: U \\rightarrow \\mathbf{R}$ by\n\n$$\n\\psi_{1}(f)=\\operatorname{Re} \\psi(f)\n$$\n\nfor $f \\in U$. Then $\\psi_{1}$ is an $\\mathbf{R}$-linear map from $U$ to $\\mathbf{R}$ and $\\left\\|\\psi_{1}\\right\\| \\leq\\|\\psi\\|$ (actually $\\left\\|\\psi_{1}\\right\\|=\\|\\psi\\|$, but we need only the inequality). Also,\n\n6.70\n\n$$\n\\begin{aligned}\n\\psi(f) & =\\operatorname{Re} \\psi(f)+i \\operatorname{Im} \\psi(f) \\\\\n& =\\psi_{1}(f)+i \\operatorname{Im}(-i \\psi(i f)) \\\\\n& =\\psi_{1}(f)-i \\operatorname{Re}(\\psi(i f)) \\\\\n& =\\psi_{1}(f)-i \\psi_{1}(i f)\n\\end{aligned}\n$$\n\nfor all $f \\in U$.\n\nTemporarily forget that complex scalar multiplication makes sense on $V$ and temporarily think of $V$ as a real normed vector space. The case of the result that we have already proved then implies that there exists an extension $\\varphi_{1}$ of $\\psi_{1}$ to an $\\mathbf{R}$-linear functional $\\varphi_{1}: V \\rightarrow \\mathbf{R}$ with $\\left\\|\\varphi_{1}\\right\\|=\\left\\|\\psi_{1}\\right\\| \\leq\\|\\psi\\|$.\n\nMotivated by 6.70 , we define $\\varphi: V \\rightarrow \\mathbf{C}$ by\n\n$$\n\\varphi(f)=\\varphi_{1}(f)-i \\varphi_{1}(i f)\n$$\n\nfor $f \\in V$. The equation above and 6.70 imply that $\\varphi$ is an extension of $\\psi$ to $V$. The equation above also implies that $\\varphi(f+g)=\\varphi(f)+\\varphi(g)$ and $\\varphi(\\alpha f)=\\alpha \\varphi(f)$ for all $f, g \\in V$ and all $\\alpha \\in \\mathbf{R}$. Also,\n\n$\\varphi(i f)=\\varphi_{1}(i f)-i \\varphi_{1}(-f)=\\varphi_{1}(i f)+i \\varphi_{1}(f)=i\\left(\\varphi_{1}(f)-i \\varphi_{1}(i f)\\right)=i \\varphi(f)$.\n\nThe reader should use the equation above to show that $\\varphi$ is a $\\mathbf{C}$-linear map.\n\nThe only part of the proof that remains is to show that $\\|\\varphi\\| \\leq\\|\\psi\\|$. To do this, note that\n\n$$\n|\\varphi(f)|^{2}=\\varphi(\\overline{\\varphi(f)} f)=\\varphi_{1}(\\overline{\\varphi(f)} f) \\leq\\|\\psi\\|\\|\\overline{\\varphi(f)} f\\|=\\|\\psi\\||\\varphi(f)|\\|f\\|\n$$\n\nfor all $f \\in V$, where the second equality holds because $\\varphi(\\overline{\\varphi(f)} f) \\in \\mathbf{R}$. Dividing by $|\\varphi(f)|$, we see from the line above that $|\\varphi(f)| \\leq\\|\\psi\\|\\|f\\|$ for all $f \\in V$ (no division necessary if $\\varphi(f)=0$ ). This implies that $\\|\\varphi\\| \\leq\\|\\psi\\|$, completing the proof.\n\nWe have given the special name linear functionals to linear maps into the scalar field $\\mathbf{F}$. The vector space of bounded linear functionals now also gets a special name and a special notation.\n\n### 6.71 Definition dual space; $V^{\\prime}$\n\nSuppose $V$ is a normed vector space. Then the dual space of $V$, denoted $V^{\\prime}$, is the normed vector space consisting of the bounded linear functionals on $V$. In other words, $V^{\\prime}=\\mathcal{B}(V, \\mathbf{F})$.\n\nBy 6.47, the dual space of every normed vector space is a Banach space.\n$6.72\\|f\\|=\\max \\left\\{|\\varphi(f)|: \\varphi \\in V^{\\prime}\\right.$ and $\\left.\\|\\varphi\\|=1\\right\\}$\n\nSuppose $V$ is a normed vector space and $f \\in V \\backslash\\{0\\}$. Then there exists $\\varphi \\in V^{\\prime}$ such that $\\|\\varphi\\|=1$ and $\\|f\\|=\\varphi(f)$.\n\nProof Let $U$ be the 1-dimensional subspace of $V$ defined by\n\n$$\nU=\\{\\alpha f: \\alpha \\in \\mathbf{F}\\}\n$$\n\nDefine $\\psi: U \\rightarrow \\mathbf{F}$ by\n\n$$\n\\psi(\\alpha f)=\\alpha\\|f\\|\n$$\n\nfor $\\alpha \\in \\mathbf{F}$. Then $\\psi$ is a linear functional on $U$ with $\\|\\psi\\|=1$ and $\\psi(f)=\\|f\\|$. The Hahn-Banach Theorem (6.69) implies that there exists an extension of $\\psi$ to a linear functional $\\varphi$ on $V$ with $\\|\\varphi\\|=1$, completing the proof.\n\nThe next result gives another beautiful application of the Hahn-Banach Theorem, with a useful necessary and sufficient condition for an element of a normed vector space to be in the closure of a subspace.\n\n### 6.73 condition to be in the closure of a subspace\n\nSuppose $U$ is a subspace of a normed vector space $V$ and $h \\in V$. Then $h \\in \\bar{U}$ if and only if $\\varphi(h)=0$ for every $\\varphi \\in V^{\\prime}$ such that $\\left.\\varphi\\right|_{U}=0$.\n\nProof First suppose $h \\in \\bar{U}$. If $\\varphi \\in V^{\\prime}$ and $\\left.\\varphi\\right|_{U}=0$, then $\\varphi(h)=0$ by the continuity of $\\varphi$, completing the proof in one direction.\n\nTo prove the other direction, suppose now that $h \\notin \\bar{U}$. Define $\\psi: U+\\mathbf{F} h \\rightarrow \\mathbf{F}$ by\n\n$$\n\\psi(f+\\alpha h)=\\alpha\n$$\n\nfor $f \\in U$ and $\\alpha \\in \\mathbf{F}$. Then $\\psi$ is a linear functional on $U+\\mathbf{F} h$ with null $\\psi=U$ and $\\psi(h)=1$.\n\nBecause $h \\notin \\bar{U}$, the closure of the null space of $\\psi$ does not equal $U+\\mathbf{F} h$. Thus 6.52 implies that $\\psi$ is a bounded linear functional on $U+\\mathbf{F} h$.\n\nThe Hahn-Banach Theorem (6.69) implies that $\\psi$ can be extended to a bounded linear functional $\\varphi$ on $V$. Thus we have found $\\varphi \\in V^{\\prime}$ such that $\\left.\\varphi\\right|_{U}=0$ but $\\varphi(h) \\neq 0$, completing the proof in the other direction.\n\n## EXERCISES 6D\n\n1 Suppose $V$ is a normed vector space and $\\varphi$ is a linear functional on $V$. Suppose $\\alpha \\in \\mathbf{F} \\backslash\\{0\\}$. Prove that the following are equivalent:\n\n(a) $\\varphi$ is a bounded linear functional.\n\n(b) $\\varphi^{-1}(\\alpha)$ is a closed subset of $V$.\n\n(c) $\\overline{\\varphi^{-1}(\\alpha)} \\neq V$.\n\n2 Suppose $\\varphi$ is a linear functional on a vector space $V$. Prove that if $U$ is a subspace of $V$ such that null $\\varphi \\subset U$, then $U=$ null $\\varphi$ or $U=V$.\n\n3 Suppose $\\varphi$ and $\\psi$ are linear functionals on the same vector space. Prove that\n\n$$\n\\text { null } \\varphi \\subset \\operatorname{null} \\psi\n$$\n\nif and only if there exists $\\alpha \\in \\mathbf{F}$ such that $\\psi=\\alpha \\varphi$.\n\nFor the next two exercises, $F^{n}$ should be endowed with the norm $\\|\\cdot\\|_{\\infty}$ as defined in Example 6.34.\n\n4 Suppose $n \\in \\mathbf{Z}^{+}$and $V$ is a normed vector space. Prove that every linear map from $\\mathbf{F}^{n}$ to $V$ is continuous.\n\n5 Suppose $n \\in \\mathbf{Z}^{+}, V$ is a normed vector space, and $T: \\mathbf{F}^{n} \\rightarrow V$ is a linear map that is one-to-one and onto $V$.\n\n(a) Show that\n\n$$\n\\inf \\left\\{\\|T x\\|: x \\in \\mathbf{F}^{n} \\text { and }\\|x\\|_{\\infty}=1\\right\\}>0\n$$\n\n(b) Prove that $T^{-1}: V \\rightarrow \\mathbf{F}^{n}$ is a bounded linear map.\n\n6 Suppose $n \\in \\mathbf{Z}^{+}$.\n\n(a) Prove that all norms on $\\mathbf{F}^{n}$ have the same convergent sequences, the same open sets, and the same closed sets.\n\n(b) Prove that all norms on $\\mathbf{F}^{n}$ make $\\mathbf{F}^{n}$ into a Banach space.\n\n7 Suppose $V$ and $W$ are normed vector spaces and $V$ is finite-dimensional. Prove that every linear map from $V$ to $W$ is continuous.\n\n8 Prove that every finite-dimensional normed vector space is a Banach space.\n\n9 Prove that every finite-dimensional subspace of each normed vector space is closed.\n\n10 Give a concrete example of an infinite-dimensional normed vector space and a basis of that normed vector space.\n\n11 Show that the collection $\\mathcal{A}=\\{k \\mathbf{Z}: k=2,3,4, \\ldots\\}$ of subsets of $\\mathbf{Z}$ satisfies the hypothesis of Zorn's Lemma (6.60).\n\n12 Prove that every linearly independent family in a vector space can be extended to a basis of the vector space.\n\n13 Suppose $V$ is a normed vector space, $U$ is a subspace of $V$, and $\\psi: U \\rightarrow \\mathbf{R}$ is a bounded linear functional. Prove that $\\psi$ has a unique extension to a bounded linear functional $\\varphi$ on $V$ with $\\|\\varphi\\|=\\|\\psi\\|$ if and only if\n\n$$\n\\sup _{f \\in U}(-\\|\\psi\\|\\|f+h\\|-\\psi(f))=\\inf _{g \\in U}(\\|\\psi\\|\\|g+h\\|-\\psi(g))\n$$\n\nfor every $h \\in V \\backslash U$.\n\n14 Show that there exists a linear functional $\\varphi: \\ell^{\\infty} \\rightarrow \\mathbf{F}$ such that\n\n$$\n\\left|\\varphi\\left(a_{1}, a_{2}, \\ldots\\right)\\right| \\leq\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{\\infty}\n$$\n\nfor all $\\left(a_{1}, a_{2}, \\ldots\\right) \\in \\ell^{\\infty}$ and\n\n$$\n\\varphi\\left(a_{1}, a_{2}, \\ldots\\right)=\\lim _{k \\rightarrow \\infty} a_{k}\n$$\n\nfor all $\\left(a_{1}, a_{2}, \\ldots\\right) \\in \\ell^{\\infty}$ such that the limit above on the right exists.\n\n15 Suppose $B$ is an open ball in a normed vector space $V$ such that $0 \\notin B$. Prove that there exists $\\varphi \\in V^{\\prime}$ such that\n\n$$\n\\operatorname{Re} \\varphi(f)>0\n$$\n\nfor all $f \\in B$.\n\n16 Show that the dual space of each infinite-dimensional normed vector space is infinite-dimensional.\n\nA normed vector space is called separable if it has a countable subset whose closure equals the whole space.\n\n17 Suppose $V$ is a separable normed vector space. Explain how the Hahn-Banach Theorem (6.69) for $V$ can be proved without using any results (such as Zorn's Lemma) that depend upon the Axiom of Choice.\n\n18 Suppose $V$ is a normed vector space such that the dual space $V^{\\prime}$ is a separable Banach space. Prove that $V$ is separable.\n\n19 Prove that the dual of the Banach space $C([0,1])$ is not separable; here the norm on $C([0,1])$ is defined by $\\|f\\|=\\sup |f|$.\n\nThe double dual space of a normed vector space is defined to be the dual space of the dual space. If $V$ is a normed vector space, then the double dual space of $V$ is denoted by $V^{\\prime \\prime}$; thus $V^{\\prime \\prime}=\\left(V^{\\prime}\\right)^{\\prime}$. The norm on $V^{\\prime \\prime}$ is defined to be the norm it receives as the dual space of $V^{\\prime}$.\n\n20 Define $\\Phi: V \\rightarrow V^{\\prime \\prime}$ by\n\n$$\n(\\Phi f)(\\varphi)=\\varphi(f)\n$$\n\nfor $f \\in V$ and $\\varphi \\in V^{\\prime}$. Show that $\\|\\Phi f\\|=\\|f\\|$ for every $f \\in V$.\n\n[The map $\\Phi$ defined above is called the canonical isometry of $V$ into $V^{\\prime \\prime}$.]\n\n21 Suppose $V$ is an infinite-dimensional normed vector space. Show that there is a convex subset $U$ of $V$ such that $\\bar{U}=V$ and such that the complement $V \\backslash U$ is also a convex subset of $V$ with $\\overline{V \\backslash U}=V$.\n\n[See 8.25 for the definition of a convex set. This exercise should stretch your geometric intuition because this behavior cannot happen in finite dimensions.]\n\n## 6E Consequences of Baire's Theorem\n\nThis section focuses on several important results about Banach spaces that depend upon Baire's Theorem. This result was first proved by René-Louis Baire (18741932) as part of his 1899 doctoral dissertation at École Normale Supérieure (Paris).\n\nEven though our interest lies primarily in applications to Banach spaces, the proper setting for Baire's Theorem is the more general context of complete metric spaces.\n\n## Baire's Theorem\n\nThe result here called Baire's Theorem is often called the Baire Category Theorem. This book uses the shorter name of this result because we do not need the categories introduced by Baire. Furthermore, the use of the word category in this context can be confusing because Baire's categories have no connection with the category theory that developed decades after Baire's work.\n\nWe begin with some key topological notions.\n\n### 6.74 Definition interior\n\nSuppose $U$ is a subset of a metric space $V$. The interior of $U$, denoted int $U$, is the set of $f \\in U$ such that some open ball of $V$ centered at $f$ with positive radius is contained in $U$.\n\nYou should verify the following elementary facts about the interior.\n\n- The interior of each subset of a metric space is open.\n- The interior of a subset $U$ of a metric space $V$ is the largest open subset of $V$ contained in $U$.\n\n### 6.75 Definition dense\n\nA subset $U$ of a metric space $V$ is called dense in $V$ if $\\bar{U}=V$.\n\nFor example, $\\mathbf{Q}$ and $\\mathbf{R} \\backslash \\mathbf{Q}$ are both dense in $\\mathbf{R}$, where $\\mathbf{R}$ has its standard metric $d(x, y)=|x-y|$.\n\nYou should verify the following elementary facts about dense subsets.\n\n- A subset $U$ of a metric space $V$ is dense in $V$ if and only if every nonempty open subset of $V$ contains at least one element of $U$.\n- A subset $U$ of a metric space $V$ has an empty interior if and only if $V \\backslash U$ is dense in $V$.\n\nThe proof of the next result uses the following fact, which you should first prove: If $G$ is an open subset of a metric space $V$ and $f \\in G$, then there exists $r>0$ such that $\\bar{B}(f, r) \\subset G$.\n\n### 6.76 Baire's Theorem\n\n(a) A complete metric space is not the countable union of closed subsets with empty interior.\n\n(b) The countable intersection of dense open subsets of a complete metric space is nonempty.\n\nProof We will prove (b) and then use (b) to prove (a).\n\nTo prove (b), suppose $(V, d)$ is a complete metric space and $G_{1}, G_{2}, \\ldots$ is a sequence of dense open subsets of $V$. We need to show that $\\bigcap_{k=1}^{\\infty} G_{k} \\neq \\varnothing$.\n\nLet $f_{1} \\in G_{1}$ and let $r_{1} \\in(0,1)$ be such that $\\bar{B}\\left(f_{1}, r_{1}\\right) \\subset G_{1}$. Now suppose $n \\in \\mathbf{Z}^{+}$, and $f_{1}, \\ldots, f_{n}$ and $r_{1}, \\ldots, r_{n}$ have been chosen such that\n\n$$\n\\bar{B}\\left(f_{1}, r_{1}\\right) \\supset \\bar{B}\\left(f_{2}, r_{2}\\right) \\supset \\cdots \\supset \\bar{B}\\left(f_{n}, r_{n}\\right)\n$$\n\nand\n\n6.78\n\n$$\nr_{j} \\in\\left(0, \\frac{1}{j}\\right) \\quad \\text { and } \\quad \\bar{B}\\left(f_{j}, r_{j}\\right) \\subset G_{j} \\text { for } j=1, \\ldots, n \\text {. }\n$$\n\nBecause $B\\left(f_{n}, r_{n}\\right)$ is an open subset of $V$ and $G_{n+1}$ is dense in $V$, there exists $f_{n+1} \\in B\\left(f_{n}, r_{n}\\right) \\cap G_{n+1}$. Let $r_{n+1} \\in\\left(0, \\frac{1}{n+1}\\right)$ be such that\n\n$$\n\\bar{B}\\left(f_{n+1}, r_{n+1}\\right) \\subset \\bar{B}\\left(f_{n}, r_{n}\\right) \\cap G_{n+1} .\n$$\n\nThus we inductively construct a sequence $f_{1}, f_{2}, \\ldots$ that satisfies 6.77 and 6.78 for all $n \\in \\mathbf{Z}^{+}$.\n\nIf $j \\in \\mathbf{Z}^{+}$, then 6.77 and 6.78 imply that\n\n6.79\n\n$$\nf_{k} \\in \\bar{B}\\left(f_{j}, r_{j}\\right) \\quad \\text { and } \\quad d\\left(f_{j}, f_{k}\\right) \\leq r_{j}<\\frac{1}{j} \\quad \\text { for all } k>j \\text {. }\n$$\n\nHence $f_{1}, f_{2}, \\ldots$ is a Cauchy sequence. Because $(V, d)$ is a complete metric space, there exists $f \\in V$ such that $\\lim _{k \\rightarrow \\infty} f_{k}=f$.\n\nNow 6.79 and 6.78 imply that for each $j \\in \\mathbf{Z}^{+}$, we have $f \\in \\bar{B}\\left(f_{j}, r_{j}\\right) \\subset G_{j}$. Hence $f \\in \\bigcap_{k=1}^{\\infty} G_{k}$, which means that $\\bigcap_{k=1}^{\\infty} G_{k}$ is not the empty set, completing the proof of (b).\n\nTo prove (a), suppose $(V, d)$ is a complete metric space and $F_{1}, F_{2}, \\ldots$ is a sequence of closed subsets of $V$ with empty interior. Then $V \\backslash F_{1}, V \\backslash F_{2}, \\ldots$ is a sequence of dense open subsets of $V$. Now (b) implies that\n\n$$\n\\varnothing \\neq \\bigcap_{k=1}^{\\infty}\\left(V \\backslash F_{k}\\right)\n$$\n\nTaking complements of both sides above, we conclude that\n\n$$\nV \\neq \\bigcup_{k=1}^{\\infty} F_{k}\n$$\n\ncompleting the proof of (a).\n\nBecause\n\n$$\n\\mathbf{R}=\\bigcup_{x \\in \\mathbf{R}}\\{x\\}\n$$\n\nand each set $\\{x\\}$ has empty interior in $\\mathbf{R}$, Baire's Theorem implies $\\mathbf{R}$ is uncountable. Thus we have yet another proof that $\\mathbf{R}$ is uncountable, different than Cantor's original diagonal proof and different from the proof via measure theory (see 2.17).\n\nThe next result is another nice consequence of Baire's Theorem.\n\n### 6.80 the set of irrational numbers is not a countable union of closed sets\n\nThere does not exist a countable collection of closed subsets of $\\mathbf{R}$ whose union equals $\\mathbf{R} \\backslash \\mathbf{Q}$.\n\nProof This will be a proof by contradiction. Suppose $F_{1}, F_{2}, \\ldots$ is a countable collection of closed subsets of $\\mathbf{R}$ whose union equals $\\mathbf{R} \\backslash \\mathbf{Q}$. Thus each $F_{k}$ contains no rational numbers, which implies that each $F_{k}$ has empty interior. Now\n\n$$\n\\mathbf{R}=\\left(\\bigcup_{r \\in \\mathbf{Q}}\\{r\\}\\right) \\cup\\left(\\bigcup_{k=1}^{\\infty} F_{k}\\right)\n$$\n\nThe equation above writes the complete metric space $\\mathbf{R}$ as a countable union of closed sets with empty interior, which contradicts Baire's Theorem [6.76(a)]. This contradiction completes the proof.\n\n## Open Mapping Theorem and Inverse Mapping Theorem\n\nThe next result shows that a surjective bounded linear map from one Banach space onto another Banach space maps open sets to open sets. As shown in Exercises 10 and 11, this result can fail if the hypothesis that both spaces are Banach spaces is weakened to allow either of the spaces to be a normed vector space.\n\n### 6.81 Open Mapping Theorem\n\nSuppose $V$ and $W$ are Banach spaces and $T$ is a bounded linear map of $V$ onto $W$. Then $T(G)$ is an open subset of $W$ for every open subset $G$ of $V$.\n\nProof Let $B$ denote the open unit ball $B(0,1)=\\{f \\in V:\\|f\\|<1\\}$ of $V$. For any open ball $B(f, a)$ in $V$, the linearity of $T$ implies that\n\n$$\nT(B(f, a))=T f+a T(B) .\n$$\n\nSuppose $G$ is an open subset of $V$. If $f \\in G$, then there exists $a>0$ such that $B(f, a) \\subset G$. If we can show that $0 \\in \\operatorname{int} T(B)$, then the equation above shows that $T f \\in \\operatorname{int} T(B(f, a))$. This would imply that $T(G)$ is an open subset of $W$. Thus to complete the proof we need only show that $T(B)$ contains some open ball centered at 0 .\n\nThe surjectivity and linearity of $T$ imply that\n\n$$\nW=\\bigcup_{k=1}^{\\infty} T(k B)=\\bigcup_{k=1}^{\\infty} k T(B)\n$$\n\nThus $W=\\bigcup_{k=1}^{\\infty} \\overline{k T(B)}$. Baire's Theorem [6.76(a)] now implies that $\\overline{k T(B)}$ has a nonempty interior for some $k \\in \\mathbf{Z}^{+}$. The linearity of $T$ allows us to conclude that $\\overline{T(B)}$ has a nonempty interior.\n\nThus there exists $g \\in B$ such that $T g \\in \\operatorname{int} \\overline{T(B)}$. Hence\n\n$$\n0 \\in \\operatorname{int} \\overline{T(B-g)} \\subset \\operatorname{int} \\overline{T(2 B)}=\\operatorname{int} \\overline{2 T(B)} .\n$$\n\nThus there exists $r>0$ such that $\\bar{B}(0,2 r) \\subset \\overline{2 T(B)}$ [here $\\bar{B}(0,2 r)$ is the closed ball in $W$ centered at 0 with radius $2 r]$. Hence $\\bar{B}(0, r) \\subset \\overline{T(B)}$. The definition of what it means to be in the closure of $T(B)$ [see 6.7] now shows that\n\n$$\nh \\in W \\text { and }\\|h\\| \\leq r \\text { and } \\varepsilon>0 \\Longrightarrow \\exists f \\in B \\text { such that }\\|h-T f\\|<\\varepsilon \\text {. }\n$$\n\nFor arbitrary $h \\neq 0$ in $W$, applying the result in the line above to $\\frac{r}{\\|h\\|} h$ shows that\n\n$$\nh \\in W \\text { and } \\varepsilon>0 \\Longrightarrow \\exists f \\in \\frac{\\|h\\|}{r} B \\text { such that }\\|h-T f\\|<\\varepsilon\n$$\n\nNow suppose $g \\in W$ and $\\|g\\|<1$. Applying 6.82 with $h=g$ and $\\varepsilon=\\frac{1}{2}$, we see that\n\n$$\n\\text { there exists } f_{1} \\in \\frac{1}{r} B \\text { such that }\\left\\|g-T f_{1}\\right\\|<\\frac{1}{2} \\text {. }\n$$\n\nNow applying 6.82 with $h=g-T f_{1}$ and $\\varepsilon=\\frac{1}{4}$, we see that\n\n$$\n\\text { there exists } f_{2} \\in \\frac{1}{2 r} B \\text { such that }\\left\\|g-T f_{1}-T f_{2}\\right\\|<\\frac{1}{4} \\text {. }\n$$\n\nApplying 6.82 again, this time with $h=g-T f_{1}-T f_{2}$ and $\\varepsilon=\\frac{1}{8}$, we see that\n\n$$\n\\text { there exists } f_{3} \\in \\frac{1}{4 r} B \\text { such that }\\left\\|g-T f_{1}-T f_{2}-T f_{3}\\right\\|<\\frac{1}{8} \\text {. }\n$$\n\nContinue in this pattern, constructing a sequence $f_{1}, f_{2}, \\ldots$ in $V$. Let\n\n$$\nf=\\sum_{k=1}^{\\infty} f_{k}\n$$\n\nwhere the infinite sum converges in $V$ because\n\n$$\n\\sum_{k=1}^{\\infty}\\left\\|f_{k}\\right\\|<\\sum_{k=1}^{\\infty} \\frac{1}{2^{k-1} r}=\\frac{2}{r}\n$$\n\nhere we are using 6.41 (this is the place in the proof where we use the hypothesis that $V$ is a Banach space). The inequality displayed above shows that $\\|f\\|<\\frac{2}{r}$.\n\nBecause\n\n$$\n\\left\\|g-T f_{1}-T f_{2}-\\cdots-T f_{n}\\right\\|<\\frac{1}{2^{n}}\n$$\n\nand because $T$ is a continuous linear map, we have $g=T f$.\n\nWe have now shown that $B(0,1) \\subset \\frac{2}{r} T(B)$. Thus $\\frac{r}{2} B(0,1) \\subset T(B)$, completing the proof.\n\nThe next result provides the useful information that if a bounded linear map from one Banach space to another Banach space has an algebraic inverse (meaning that the linear map is injective and surjec-\n\nThe Open Mapping Theorem was first proved by Banach and his colleague Juliusz Schauder (1899-1943) in 1929-1930. tive), then the inverse mapping is automatically bounded.\n\n### 6.83 Bounded Inverse Theorem\n\nSuppose $V$ and $W$ are Banach spaces and $T$ is a one-to-one bounded linear map from $V$ onto $W$. Then $T^{-1}$ is a bounded linear map from $W$ onto $V$.\n\nProof The verification that $T^{-1}$ is a linear map from $W$ to $V$ is left to the reader.\n\nTo prove that $T^{-1}$ is bounded, suppose $G$ is an open subset of $V$. Then\n\n$$\n\\left(T^{-1}\\right)^{-1}(G)=T(G) .\n$$\n\nBy the Open Mapping Theorem (6.81), $T(G)$ is an open subset of $W$. Thus the equation above shows that the inverse image under the function $T^{-1}$ of every open set is open. By the equivalence of parts (a) and (c) of 6.11, this implies that $T^{-1}$ is continuous. Thus $T^{-1}$ is a bounded linear map (by 6.48).\n\nThe result above shows that completeness for normed vector spaces sometimes plays a role analogous to compactness for metric spaces (think of the theorem stating that a continuous one-to-one function from a compact metric space onto another compact metric space has an inverse that is also continuous).\n\n## Closed Graph Theorem\n\nSuppose $V$ and $W$ are normed vector spaces. Then $V \\times W$ is a vector space with the natural operations of addition and scalar multiplication as defined in Exercise 10 in Section 6B. There are several natural norms on $V \\times W$ that make $V \\times W$ into a normed vector space; the choice used in the next result seems to be the easiest. The proof of the next result is left to the reader as an exercise.\n\n### 6.84 product of Banach spaces\n\nSuppose $V$ and $W$ are Banach spaces. Then $V \\times W$ is a Banach space if given the norm defined by\n\n$$\n\\|(f, g)\\|=\\max \\{\\|f\\|,\\|g\\|\\}\n$$\n\nfor $f \\in V$ and $g \\in W$. With this norm, a sequence $\\left(f_{1}, g_{1}\\right),\\left(f_{2}, g_{2}\\right), \\ldots$ in $V \\times W$ converges to $(f, g)$ if and only if $\\lim _{k \\rightarrow \\infty} f_{k}=f$ and $\\lim _{k \\rightarrow \\infty} g_{k}=g$.\n\nThe next result gives a terrific way to show that a linear map between Banach spaces is bounded. The proof is remarkably clean because the hard work has been done in the proof of the Open Mapping Theorem (which was used to prove the Bounded Inverse Theorem).\n\n### 6.85 Closed Graph Theorem\n\nSuppose $V$ and $W$ are Banach spaces and $T$ is a function from $V$ to $W$. Then $T$ is a bounded linear map if and only if $\\operatorname{graph}(T)$ is a closed subspace of $V \\times W$.\n\nProof First suppose $T$ is a bounded linear map. Suppose $\\left(f_{1}, T f_{1}\\right),\\left(f_{2}, T f_{2}\\right), \\ldots$ is a sequence in $\\operatorname{graph}(T)$ converging to $(f, g) \\in V \\times W$. Thus\n\n$$\n\\lim _{k \\rightarrow \\infty} f_{k}=f \\quad \\text { and } \\quad \\lim _{k \\rightarrow \\infty} T f_{k}=g .\n$$\n\nBecause $T$ is continuous, the first equation above implies that $\\lim _{k \\rightarrow \\infty} T f_{k}=T f$; when combined with the second equation above this implies that $g=T f$. Thus $(f, g)=(f, T f) \\in \\operatorname{graph}(T)$, which implies that graph $(T)$ is closed, completing the proof in one direction.\n\nTo prove the other direction, now suppose $\\operatorname{graph}(T)$ is a closed subspace of $V \\times W$. Thus graph $(T)$ is a Banach space with the norm that it inherits from $V \\times W$ [from 6.84 and 6.16(b)]. Consider the linear map $S: \\operatorname{graph}(T) \\rightarrow V$ defined by\n\n$$\nS(f, T f)=f \\text {. }\n$$\n\nThen\n\n$$\n\\|S(f, T f)\\|=\\|f\\| \\leq \\max \\{\\|f\\|,\\|T f\\|\\}=\\|(f, T f)\\|\n$$\n\nfor all $f \\in V$. Thus $S$ is a bounded linear map from graph $(T)$ onto $V$ with $\\|S\\| \\leq 1$. Clearly $S$ is injective. Thus the Bounded Inverse Theorem (6.83) implies that $S^{-1}$ is bounded. Because $S^{-1}: V \\rightarrow \\operatorname{graph}(T)$ satisfies the equation $S^{-1} f=(f, T f)$, we have\n\n$$\n\\begin{aligned}\n\\|T f\\| & \\leq \\max \\{\\|f\\|,\\|T f\\|\\} \\\\\n& =\\|(f, T f)\\| \\\\\n& =\\left\\|S^{-1} f\\right\\| \\\\\n& \\leq\\left\\|S^{-1}\\right\\|\\|f\\|\n\\end{aligned}\n$$\n\nfor all $f \\in V$. The inequality above implies that $T$ is a bounded linear map with $\\|T\\| \\leq\\left\\|S^{-1}\\right\\|$, completing the proof.\n\n## Principle of Uniform Boundedness\n\nThe next result states that a family of bounded linear maps on a Banach space that is pointwise bounded is bounded in norm (which means that it is uniformly bounded as a collection of maps on the unit ball). This result is sometimes called the Banach-Steinhaus Theorem. Exercise 17 is also sometimes called the Banach-\n\nThe Principle of Uniform Boundedness was proved in 1927 by Banach and Hugo Steinhaus (1887-1972). Steinhaus recruited Banach to advanced mathematics after overhearing him discuss\n\nLebesgue integration in a park. Steinhaus Theorem.\n\n### 6.86 Principle of Uniform Boundedness\n\nSuppose $V$ is a Banach space, $W$ is a normed vector space, and $\\mathcal{A}$ is a family of bounded linear maps from $V$ to $W$ such that\n\n$$\n\\sup \\{\\|T f\\|: T \\in \\mathcal{A}\\}<\\infty \\text { for every } f \\in V \\text {. }\n$$\n\nThen\n\n$$\n\\sup \\{\\|T\\|: T \\in \\mathcal{A}\\}<\\infty .\n$$\n\nProof Our hypothesis implies that\n\n$$\nV=\\bigcup_{n=1}^{\\infty} \\underbrace{\\{f \\in V:\\|T f\\| \\leq n \\text { for all } T \\in \\mathcal{A}\\}}_{V_{n}}\n$$\n\nwhere $V_{n}$ is defined by the expression above. Because each $T \\in \\mathcal{A}$ is continuous, $V_{n}$ is a closed subset of $V$ for each $n \\in \\mathbf{Z}^{+}$. Thus Baire's Theorem [6.76(a)] and the equation above imply that there exist $n \\in \\mathbf{Z}^{+}$and $h \\in V$ and $r>0$ such that\n\n$$\nB(h, r) \\subset V_{n} .\n$$\n\nNow suppose $g \\in V$ and $\\|g\\|<1$. Thus $r g+h \\in B(h, r)$. Hence if $T \\in \\mathcal{A}$, then 6.87 implies $\\|T(r g+h)\\| \\leq n$, which implies that\n\n$$\n\\|T g\\|=\\left\\|\\frac{T(r g+h)}{r}-\\frac{T h}{r}\\right\\| \\leq \\frac{\\|T(r g+h)\\|}{r}+\\frac{\\|T h\\|}{r} \\leq \\frac{n+\\|T h\\|}{r} .\n$$\n\nThus\n\n$$\n\\sup \\{\\|T\\|: T \\in \\mathcal{A}\\} \\leq \\frac{n+\\sup \\{\\|T h\\|: T \\in \\mathcal{A}\\}}{r}<\\infty \\text {, }\n$$\n\ncompleting the proof.\n\n## EXERCISES 6E\n\n1 Suppose $U$ is a subset of a metric space $V$. Show that $U$ is dense in $V$ if and only if every nonempty open subset of $V$ contains at least one element of $U$.\n\n2 Suppose $U$ is a subset of a metric space $V$. Show that $U$ has an empty interior if and only if $V \\backslash U$ is dense in $V$.\n\n3 Prove or give a counterexample: If $V$ is a metric space and $U, W$ are subsets of $V$, then $(\\operatorname{int} U) \\cup(\\operatorname{int} W)=\\operatorname{int}(U \\cup W)$.\n\n4 Prove or give a counterexample: If $V$ is a metric space and $U, W$ are subsets of $V$, then $(\\operatorname{int} U) \\cap(\\operatorname{int} W)=\\operatorname{int}(U \\cap W)$.\n\nSuppose\n\n$$\nX=\\{0\\} \\cup \\bigcup_{k=1}^{\\infty}\\left\\{\\frac{1}{k}\\right\\}\n$$\n\nand $d(x, y)=|x-y|$ for $x, y \\in X$.\n\n(a) Show that $(X, d)$ is a complete metric space.\n\n(b) Each set of the form $\\{x\\}$ for $x \\in X$ is a closed subset of $\\mathbf{R}$ that has an empty interior as a subset of $\\mathbf{R}$. Clearly $X$ is a countable union of such sets. Explain why this does not violate the statement of Baire's Theorem that a complete metric space is not the countable union of closed subsets with empty interior.\n\n6 Give an example of a metric space that is the countable union of closed subsets with empty interior.\n\n[This exercise shows that the completeness hypothesis in Baire's Theorem cannot be dropped.]\n\n7 (a) Define $f: \\mathbf{R} \\rightarrow \\mathbf{R}$ as follows:\n\n$$\nf(a)= \\begin{cases}0 & \\text { if } a \\text { is irrational, } \\\\ \\frac{1}{n} & \\text { if } a \\text { is rational and } n \\text { is the smallest positive integer } \\\\ & \\text { such that } a=\\frac{m}{n} \\text { for some integer } m .\\end{cases}\n$$\n\nAt which numbers in $\\mathbf{R}$ is $f$ continuous?\n\n(b) Show that there does not exist a countable collection of open subsets of $\\mathbf{R}$ whose intersection equals $\\mathbf{Q}$.\n\n(c) Show that there does not exist a function $f: \\mathbf{R} \\rightarrow \\mathbf{R}$ such that $f$ is continuous at each element of $\\mathbf{Q}$ and discontinuous at each element of $\\mathbf{R} \\backslash \\mathbf{Q}$.\n\n8 Suppose $(X, d)$ is a complete metric space and $G_{1}, G_{2}, \\ldots$ is a sequence of dense open subsets of $X$. Prove that $\\bigcap_{k=1}^{\\infty} G_{k}$ is a dense subset of $X$.\n\n9 Prove that there does not exist an infinite-dimensional Banach space with a countable basis.\n\n[This exercise implies, for example, that there is not a norm that makes the vector space of polynomials with coefficients in $\\mathbf{F}$ into a Banach space.]\n\n10 Give an example of a Banach space $V$, a normed vector space $W$, a bounded linear map $T$ of $V$ onto $W$, and an open subset $G$ of $V$ such that $T(G)$ is not an open subset of $W$.\n\n[This exercise shows that the hypothesis in the Open Mapping Theorem that $W$ is a Banach space cannot be relaxed to the hypothesis that $W$ is a normed vector space.]\n\n11 Show that there exists a normed vector space $V$, a Banach space $W$, a bounded linear map $T$ of $V$ onto $W$, and an open subset $G$ of $V$ such that $T(G)$ is not an open subset of $W$.\n\n[This exercise shows that the hypothesis in the Open Mapping Theorem that $V$ is a Banach space cannot be relaxed to the hypothesis that $V$ is a normed vector space.]\n\n## A linear map $T: V \\rightarrow W$ from a normed vector space $V$ to a normed vector space $W$ is called bounded below if there exists $c \\in(0, \\infty)$ such that $\\|f\\| \\leq c\\|T f\\|$ for all $f \\in V$\n\n12 Suppose $T: V \\rightarrow W$ is a bounded linear map from a Banach space $V$ to a Banach space $W$. Prove that $T$ is bounded below if and only if $T$ is injective and the range of $T$ is a closed subspace of $W$.\n\n13 Give an example of a Banach space $V$, a normed vector space $W$, and a one-toone bounded linear map $T$ of $V$ onto $W$ such that $T^{-1}$ is not a bounded linear map of $W$ onto $V$.\n\n[This exercise shows that the hypothesis in the Bounded Inverse Theorem (6.83) that $W$ is a Banach space cannot be relaxed to the hypothesis that $W$ is a normed vector space.]\n\n14 Show that there exists a normed space $V$, a Banach space $W$, and a one-to-one bounded linear map $T$ of $V$ onto $W$ such that $T^{-1}$ is not a bounded linear map of $W$ onto $V$.\n\n[This exercise shows that the hypothesis in the Bounded Inverse Theorem (6.83) that $V$ is a Banach space cannot be relaxed to the hypothesis that $V$ is a normed vector space.]\n\n15 Prove 6.84.\n\n16 Suppose $V$ is a Banach space with norm $\\|\\cdot\\|$ and that $\\varphi: V \\rightarrow \\mathbf{F}$ is a linear functional. Define another norm $\\|\\cdot\\|_{\\varphi}$ on $V$ by\n\n$$\n\\|f\\|_{\\varphi}=\\|f\\|+|\\varphi(f)| .\n$$\n\nProve that if $V$ is a Banach space with the norm $\\|\\cdot\\|_{\\varphi}$, then $\\varphi$ is a continuous linear functional on $V$ (with the original norm).\n\n17 Suppose $V$ is a Banach space, $W$ is a normed vector space, and $T_{1}, T_{2}, \\ldots$ is a sequence of bounded linear maps from $V$ to $W$ such that $\\lim _{k \\rightarrow \\infty} T_{k} f$ exists for each $f \\in V$. Define $T: V \\rightarrow W$ by\n\n$$\nT f=\\lim _{k \\rightarrow \\infty} T_{k} f\n$$\n\nfor $f \\in V$. Prove that $T$ is a bounded linear map from $V$ to $W$.\n\n[This result states that the pointwise limit of a sequence of bounded linear maps on a Banach space is a bounded linear map.]\n\n18 Suppose $V$ is a normed vector space and $B$ is a subset of $V$ such that\n\n$$\n\\sup _{f \\in B}|\\varphi(f)|<\\infty\n$$\n\nfor every $\\varphi \\in V^{\\prime}$. Prove that sup $\\|f\\|<\\infty$.\n\n$$\nf \\in B\n$$\n\n19 Suppose $T: V \\rightarrow W$ is a linear map from a Banach space $V$ to a Banach space $W$ such that\n\n$$\n\\varphi \\circ T \\in V^{\\prime} \\text { for all } \\varphi \\in W^{\\prime}\n$$\n\nProve that $T$ is a bounded linear map.\n\n## Chapter 7\n\n## $L^{p}$ Spaces\n\nFix a measure space $(X, \\mathcal{S}, \\mu)$ and a positive number $p$. We begin this chapter by looking at the vector space of measurable functions $f: X \\rightarrow \\mathbf{F}$ such that\n\n$$\n\\int|f|^{p} d \\mu<\\infty\n$$\n\nImportant results called Hölder's inequality and Minkowski's inequality help us investigate this vector space. A useful class of Banach spaces appears when we identify functions that differ only on a set of measure 0 and require $p \\geq 1$.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-208.jpg?height=772&width=1145&top_left_y=865&top_left_x=79)\n\nThe main building of the Swiss Federal Institute of Technology (ETH Zürich). Hermann Minkowski (1864-1909) taught at this university from 1896 to 1902.\n\nDuring this time, Albert Einstein (1879-1955) was a student in several of Minkowski's mathematics classes. Minkowski later created mathematics that helped explain Einstein's special theory of relativity.\n\nCC-BY-SA Roland zh\n\n## $7 \\mathrm{~A} \\quad \\mathcal{L}^{p}(\\mu)$\n\n## Hölder's Inequality\n\nOur next major goal is to define an important class of vector spaces that generalize the vector spaces $\\mathcal{L}^{1}(\\mu)$ and $\\ell^{1}$ introduced in the last two bullet points of Example 6.32. We begin this process with the definition below. The terminology p-norm introduced below is convenient, even though it is not necessarily a norm.\n\n### 7.1 Definition $\\|f\\|_{p}$; essential supremum\n\nSuppose that $(X, \\mathcal{S}, \\mu)$ is a measure space, $0<p<\\infty$, and $f: X \\rightarrow \\mathbf{F}$ is $\\mathcal{S}$-measurable. Then the $p$-norm of $f$ is denoted by $\\|f\\|_{p}$ and is defined by\n\n$$\n\\|f\\|_{p}=\\left(\\int|f|^{p} d \\mu\\right)^{1 / p} .\n$$\n\nAlso, $\\|f\\|_{\\infty}$, which is called the essential supremum of $f$, is defined by\n\n$$\n\\|f\\|_{\\infty}=\\inf \\{t>0: \\mu(\\{x \\in X:|f(x)|>t\\})=0\\} .\n$$\n\nThe exponent $1 / p$ appears in the definition of the $p$-norm $\\|f\\|_{p}$ because we want the equation $\\|\\alpha f\\|_{p}=|\\alpha|\\|f\\|_{p}$ to hold for all $\\alpha \\in \\mathbf{F}$.\n\nFor $0<p<\\infty$, the $p$-norm $\\|f\\|_{p}$ does not change if $f$ changes on a set of $\\mu$-measure 0 . By using the essential supremum rather than the supremum in the definition of $\\|f\\|_{\\infty}$, we arrange for the $\\infty$-norm $\\|f\\|_{\\infty}$ to enjoy this same property. Think of $\\|f\\|_{\\infty}$ as the smallest that you can make the supremum of $|f|$ after modifications on sets of measure 0 .\n\n### 7.2 Example p-norm for counting measure\n\nSuppose $\\mu$ is counting measure on $\\mathbf{Z}^{+}$. If $a=\\left(a_{1}, a_{2}, \\ldots\\right)$ is a sequence in $\\mathbf{F}$ and $0<p<\\infty$, then\n\n$$\n\\|a\\|_{p}=\\left(\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|^{p}\\right)^{1 / p} \\quad \\text { and } \\quad\\|a\\|_{\\infty}=\\sup \\left\\{\\left|a_{k}\\right|: k \\in \\mathbf{Z}^{+}\\right\\}\n$$\n\nNote that for counting measure, the essential supremum and the supremum are the same because in this case there are no sets of measure 0 other than the empty set.\n\nNow we can define our generalization of $\\mathcal{L}^{1}(\\mu)$, which was defined in the secondto-last bullet point of Example 6.32.\n\n### 7.3 Definition Lebesgue space; $\\mathcal{L}^{p}(\\mu)$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $0<p \\leq \\infty$. The Lebesgue space $\\mathcal{L}^{p}(\\mu)$, sometimes denoted $\\mathcal{L}^{p}(X, \\mathcal{S}, \\mu)$, is defined to be the set of $\\mathcal{S}$-measurable functions $f: X \\rightarrow \\mathbf{F}$ such that $\\|f\\|_{p}<\\infty$.\n\n### 7.4 Example $\\ell^{p}$\n\nWhen $\\mu$ is counting measure on $\\mathbf{Z}^{+}$, the set $\\mathcal{L}^{p}(\\mu)$ is often denoted by $\\ell^{p}$ (pronounced little el-p). Thus if $0<p<\\infty$, then\n\n$$\n\\ell^{p}=\\left\\{\\left(a_{1}, a_{2}, \\ldots\\right): \\text { each } a_{k} \\in \\mathbf{F} \\text { and } \\sum_{k=1}^{\\infty}\\left|a_{k}\\right|^{p}<\\infty\\right\\}\n$$\n\nand\n\n$$\n\\ell^{\\infty}=\\left\\{\\left(a_{1}, a_{2}, \\ldots\\right) \\text { : each } a_{k} \\in \\mathbf{F} \\text { and } \\sup \\left|a_{k}\\right|<\\infty\\right\\}\n$$\n\n$k \\in \\mathbf{Z}^{+}$\n\nInequality 7.5(a) below provides an easy proof that $\\mathcal{L}^{p}(\\mu)$ is closed under addition. Soon we will prove Minkowski's inequality (7.14), which provides an important improvement of 7.5(a) when $p \\geq 1$ but is more complicated to prove.\n\n## $7.5 \\quad \\mathcal{L}^{p}(\\mu)$ is a vector space\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $0<p<\\infty$. Then\n\n$$\n\\|f+g\\|_{p}^{p} \\leq 2^{p}\\left(\\|f\\|_{p}^{p}+\\|g\\|_{p}^{p}\\right)\n$$\n\nand\n\n$$\n\\|\\alpha f\\|_{p}=|\\alpha|\\|f\\|_{p}\n$$\n\nfor all $f, g \\in \\mathcal{L}^{p}(\\mu)$ and all $\\alpha \\in \\mathbf{F}$. Furthermore, with the usual operations of addition and scalar multiplication of functions, $\\mathcal{L}^{p}(\\mu)$ is a vector space.\n\nProof Suppose $f, g \\in \\mathcal{L}^{p}(\\mu)$. If $x \\in X$, then\n\n$$\n\\begin{aligned}\n|f(x)+g(x)|^{p} & \\leq(|f(x)|+|g(x)|)^{p} \\\\\n& \\leq(2 \\max \\{|f(x)|,|g(x)|\\})^{p} \\\\\n& \\leq 2^{p}\\left(|f(x)|^{p}+|g(x)|^{p}\\right) .\n\\end{aligned}\n$$\n\nIntegrating both sides of the inequality above with respect to $\\mu$ gives the desired inequality\n\n$$\n\\|f+g\\|_{p}^{p} \\leq 2^{p}\\left(\\|f\\|_{p}^{p}+\\|g\\|_{p}^{p}\\right) .\n$$\n\nThis inequality implies that if $\\|f\\|_{p}<\\infty$ and $\\|g\\|_{p}<\\infty$, then $\\|f+g\\|_{p}<\\infty$. Thus $\\mathcal{L}^{p}(\\mu)$ is closed under addition.\n\nThe proof that\n\n$$\n\\|\\alpha f\\|_{p}=|\\alpha|\\|f\\|_{p}\n$$\n\nfollows easily from the definition of $\\|\\cdot\\|_{p}$. This equality implies that $\\mathcal{L}^{p}(\\mu)$ is closed under scalar multiplication.\n\nBecause $\\mathcal{L}^{p}(\\mu)$ contains the constant function 0 and is closed under addition and scalar multiplication, $\\mathcal{L}^{p}(\\mu)$ is a subspace of $\\mathbf{F}^{X}$ and thus is a vector space.\n\nWhat we call the dual exponent in the definition below is often called the conjugate exponent or the conjugate index. However, the terminology dual exponent conveys more meaning because of results ( 7.25 and 7.26 ) that we will see in the next section.\n\n### 7.6 Definition dual exponent; $p^{\\prime}$\n\nFor $1 \\leq p \\leq \\infty$, the dual exponent of $p$ is denoted by $p^{\\prime}$ and is the element of $[1, \\infty]$ such that\n\n$$\n\\frac{1}{p}+\\frac{1}{p^{\\prime}}=1\n$$\n\n7.7 Example dual exponents\n\n$$\n1^{\\prime}=\\infty, \\quad \\infty^{\\prime}=1, \\quad 2^{\\prime}=2, \\quad 4^{\\prime}=4 / 3, \\quad(4 / 3)^{\\prime}=4\n$$\n\nThe result below is a key tool in proving Hölder's inequality (7.9).\n\n### 7.8 Young's inequality\n\nSuppose $1<p<\\infty$. Then\n\n$$\na b \\leq \\frac{a^{p}}{p}+\\frac{b^{p^{\\prime}}}{p^{\\prime}}\n$$\n\nfor all $a \\geq 0$ and $b \\geq 0$.\n\nProof Fix $b>0$ and define a function $f:(0, \\infty) \\rightarrow \\mathbf{R}$ by\n\n$$\nf(a)=\\frac{a^{p}}{p}+\\frac{b^{p^{\\prime}}}{p^{\\prime}}-a b\n$$\n\nWilliam Henry Young (1863-1942) published what is now called Young's inequality in 1912.\n\nThus $f^{\\prime}(a)=a^{p-1}-b$. Hence $f$ is decreasing on the interval $\\left(0, b^{1 /(p-1)}\\right)$ and $f$ is increasing on the interval $\\left(b^{1 /(p-1)}, \\infty\\right)$. Thus $f$ has a global minimum at $b^{1 /(p-1)}$. A tiny bit of arithmetic [use $p /(p-1)=p^{\\prime}$ ] shows that $f\\left(b^{1 /(p-1)}\\right)=0$. Thus $f(a) \\geq 0$ for all $a \\in(0, \\infty)$, which implies the desired inequality.\n\nThe important result below furnishes a key tool that is used in the proof of Minkowski's inequality (7.14).\n\n### 7.9 Hölder's inequality\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $1 \\leq p \\leq \\infty$, and $f, h: X \\rightarrow \\mathbf{F}$ are $\\mathcal{S}$-measurable. Then\n\n$$\n\\|f h\\|_{1} \\leq\\|f\\|_{p}\\|h\\|_{p^{\\prime}} .\n$$\n\nProof Suppose $1<p<\\infty$, leaving the cases $p=1$ and $p=\\infty$ as exercises for the reader.\n\nFirst consider the special case where $\\|f\\|_{p}=\\|h\\|_{p^{\\prime}}=1$. Young's inequality (7.8) tells us that\n\n$$\n|f(x) h(x)| \\leq \\frac{|f(x)|^{p}}{p}+\\frac{|h(x)|^{p^{\\prime}}}{p^{\\prime}}\n$$\n\nfor all $x \\in X$. Integrating both sides of the inequality above with respect to $\\mu$ shows that $\\|f h\\|_{1} \\leq 1=\\|f\\|_{p}\\|h\\|_{p^{\\prime}}$, completing the proof in this special case.\n\nIf $\\|f\\|_{p}=0$ or $\\|h\\|_{p^{\\prime}}=0$, then $\\|f h\\|_{1}=0$ and the desired inequality holds. Similarly, if $\\|f\\|_{p}=\\infty$ or\n\nHölder's inequality was proved in 1889 by Otto Hölder (1859-1937). $\\|h\\|_{p^{\\prime}}=\\infty$, then the desired inequality clearly holds. Thus we assume that $0<\\|f\\|_{p}<\\infty$ and $0<\\|h\\|_{p^{\\prime}}<\\infty$.\n\nNow define $\\mathcal{S}$-measurable functions $f_{1}, h_{1}: X \\rightarrow \\mathbf{F}$ by\n\n$$\nf_{1}=\\frac{f}{\\|f\\|_{p}} \\quad \\text { and } \\quad h_{1}=\\frac{h}{\\|h\\|_{p^{\\prime}}}\n$$\n\nThen $\\left\\|f_{1}\\right\\|_{p}=1$ and $\\left\\|h_{1}\\right\\|_{p^{\\prime}}=1$. By the result for our special case, we have $\\left\\|f_{1} h_{1}\\right\\|_{1} \\leq 1$, which implies that $\\|f h\\|_{1} \\leq\\|f\\|_{p}\\|h\\|_{p^{\\prime}}$.\n\nThe next result gives a key containment among Lebesgue spaces with respect to a finite measure. Note the crucial role that Hölder's inequality plays in the proof.\n\n$7.10 \\quad \\mathcal{L}^{q}(\\mu) \\subset \\mathcal{L}^{p}(\\mu)$ if $p<q$ and $\\mu(X)<\\infty$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a finite measure space and $0<p<q<\\infty$. Then\n\n$$\n\\|f\\|_{p} \\leq \\mu(X)^{(q-p) /(p q)}\\|f\\|_{q}\n$$\n\nfor all $f \\in \\mathcal{L}^{q}(\\mu)$. Furthermore, $\\mathcal{L}^{q}(\\mu) \\subset \\mathcal{L}^{p}(\\mu)$.\n\nProof Fix $f \\in \\mathcal{L}^{q}(\\mu)$. Let $r=\\frac{q}{p}$. Thus $r>1$. A short calculation shows that $r^{\\prime}=\\frac{q}{q-p}$. Now Hölder's inequality (7.9) with $p$ replaced by $r$ and $f$ replaced by $|f|^{p}$ and $h$ replaced by the constant function 1 gives\n\n$$\n\\begin{aligned}\n\\int|f|^{p} d \\mu & \\leq\\left(\\int\\left(|f|^{p}\\right)^{r} d \\mu\\right)^{1 / r}\\left(\\int 1^{r^{\\prime}} d \\mu\\right)^{1 / r^{\\prime}} \\\\\n& =\\mu(X)^{(q-p) / q}\\left(\\int|f|^{q} d \\mu\\right)^{p / q} .\n\\end{aligned}\n$$\n\nNow raise both sides of the inequality above to the power $\\frac{1}{p}$, getting\n\n$$\n\\left(\\int|f|^{p} d \\mu\\right)^{1 / p} \\leq \\mu(X)^{(q-p) /(p q)}\\left(\\int|f|^{q} d \\mu\\right)^{1 / q}\n$$\n\nwhich is the desired inequality.\n\nThe inequality above shows that $f \\in \\mathcal{L}^{p}(\\mu)$. Thus $\\mathcal{L}^{q}(\\mu) \\subset \\mathcal{L}^{p}(\\mu)$.\n\n### 7.11 Example $\\mathcal{L}^{p}(E)$\n\nWe adopt the common convention that if $E$ is a Borel (or Lebesgue measurable) subset of $\\mathbf{R}$ and $0<p \\leq \\infty$, then $\\mathcal{L}^{p}(E)$ means $\\mathcal{L}^{p}\\left(\\lambda_{E}\\right)$, where $\\lambda_{E}$ denotes Lebesgue measure $\\lambda$ restricted to the Borel (or Lebesgue measurable) subsets of $\\mathbf{R}$ that are contained in $E$.\n\nWith this convention, 7.10 implies that\n\n$$\n\\text { if } 0<p<q<\\infty \\text {, then } \\mathcal{L}^{q}([0,1]) \\subset \\mathcal{L}^{p}([0,1]) \\text { and }\\|f\\|_{p} \\leq\\|f\\|_{q}\n$$\n\nfor $f \\in \\mathcal{L}^{q}([0,1])$. See Exercises 12 and 13 in this section for related results.\n\n## Minkowski's Inequality\n\nThe next result is used as a tool to prove Minkowski's inequality (7.14). Once again, note the crucial role that Hölder's inequality plays in the proof.\n\n### 7.12 formula for $\\|f\\|_{p}$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $1 \\leq p<\\infty$, and $f \\in \\mathcal{L}^{p}(\\mu)$. Then\n\n$$\n\\|f\\|_{p}=\\sup \\left\\{\\left|\\int f h d \\mu\\right|: h \\in \\mathcal{L}^{p^{\\prime}}(\\mu) \\text { and }\\|h\\|_{p^{\\prime}} \\leq 1\\right\\}\n$$\n\nProof If $\\|f\\|_{p}=0$, then both sides of the equation in the conclusion of this result equal 0 . Thus we assume that $\\|f\\|_{p} \\neq 0$.\n\nHölder's inequality (7.9) implies that if $h \\in \\mathcal{L}^{p^{\\prime}}(\\mu)$ and $\\|h\\|_{p^{\\prime}} \\leq 1$, then\n\n$$\n\\left|\\int f h d \\mu\\right| \\leq \\int|f h| d \\mu \\leq\\|f\\|_{p}\\|h\\|_{p^{\\prime}} \\leq\\|f\\|_{p}\n$$\n\nThus $\\sup \\left\\{\\left|\\int f h d \\mu\\right|: h \\in \\mathcal{L}^{p^{\\prime}}(\\mu)\\right.$ and $\\left.\\|h\\|_{p^{\\prime}} \\leq 1\\right\\} \\leq\\|f\\|_{p}$.\n\nTo prove the inequality in the other direction, define $h: X \\rightarrow \\mathbf{F}$ by\n\n$$\nh(x)=\\frac{\\overline{f(x)}|f(x)|^{p-2}}{\\|f\\|_{p}^{p / p^{\\prime}}} \\quad(\\text { set } h(x)=0 \\text { when } f(x)=0) .\n$$\n\nThen $\\int f h d \\mu=\\|f\\|_{p}$ and $\\|h\\|_{p^{\\prime}}=1$, as you should verify (use $p-\\frac{p}{p^{\\prime}}=1$ ). Thus $\\|f\\|_{p} \\leq \\sup \\left\\{\\left|\\int f h d \\mu\\right|: h \\in \\mathcal{L}^{p^{\\prime}}(\\mu)\\right.$ and $\\left.\\|h\\|_{p^{\\prime}} \\leq 1\\right\\}$, as desired.\n\n### 7.13 Example a point with infinite measure\n\nSuppose $X$ is a set with exactly one element $b$ and $\\mu$ is the measure such that $\\mu(\\varnothing)=0$ and $\\mu(\\{b\\})=\\infty$. Then $\\mathcal{L}^{1}(\\mu)$ consists only of the 0 function. Thus if $p=\\infty$ and $f$ is the function whose value at $b$ equals 1 , then $\\|f\\|_{\\infty}=1$ but the right side of the equation in 7.12 equals 0 . Thus 7.12 can fail when $p=\\infty$.\n\nExample 7.13 shows that we cannot take $p=\\infty$ in 7.12. However, if $\\mu$ is a $\\sigma$-finite measure, then 7.12 holds even when $p=\\infty$ (see Exercise 9).\n\nThe next result, which is called Minkowski's inequality, is an improvement for $p \\geq 1$ of the inequality $7.5(a)$.\n\n### 7.14 Minkowski's inequality\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $1 \\leq p \\leq \\infty$, and $f, g \\in \\mathcal{L}^{p}(\\mu)$. Then\n\n$$\n\\|f+g\\|_{p} \\leq\\|f\\|_{p}+\\|g\\|_{p}\n$$\n\nProof Assume that $1 \\leq p<\\infty$ (the case $p=\\infty$ is left as an exercise for the reader). Inequality 7.5(a) implies that $f+g \\in \\mathcal{L}^{p}(\\mu)$.\n\nSuppose $h \\in \\mathcal{L}^{p^{\\prime}}(\\mu)$ and $\\|h\\|_{p^{\\prime}} \\leq 1$. Then\n\n$$\n\\begin{aligned}\n\\left|\\int(f+g) h d \\mu\\right| \\leq \\int|f h| d \\mu+\\int|g h| d \\mu & \\leq\\left(\\|f\\|_{p}+\\|g\\|_{p}\\right)\\|h\\|_{p^{\\prime}} \\\\\n& \\leq\\|f\\|_{p}+\\|g\\|_{p},\n\\end{aligned}\n$$\n\nwhere the second inequality comes from Hölder's inequality (7.9). Now take the supremum of the left side of the inequality above over the set of $h \\in \\mathcal{L}^{p^{\\prime}}(\\mu)$ such that $\\|h\\|_{p^{\\prime}} \\leq 1$. By 7.12, we get $\\|f+g\\|_{p} \\leq\\|f\\|_{p}+\\|g\\|_{p}$, as desired.\n\n## EXERCISES 7A\n\n1 Suppose $\\mu$ is a measure. Prove that\n\n$$\n\\|f+g\\|_{\\infty} \\leq\\|f\\|_{\\infty}+\\|g\\|_{\\infty} \\quad \\text { and } \\quad\\|\\alpha f\\|_{\\infty}=|\\alpha|\\|f\\|_{\\infty}\n$$\n\nfor all $f, g \\in \\mathcal{L}^{\\infty}(\\mu)$ and all $\\alpha \\in \\mathbf{F}$. Conclude that with the usual operations of addition and scalar multiplication of functions, $\\mathcal{L}^{\\infty}(\\mu)$ is a vector space.\n\n2 Suppose $a \\geq 0, b \\geq 0$, and $1<p<\\infty$. Prove that\n\n$$\na b=\\frac{a^{p}}{p}+\\frac{b^{p^{\\prime}}}{p^{\\prime}}\n$$\n\nif and only if $a^{p}=b^{p^{\\prime}}$ [compare to Young's inequality (7.8)].\n\n3 Suppose $a_{1}, \\ldots, a_{n}$ are nonnegative numbers. Prove that\n\n$$\n\\left(a_{1}+\\cdots+a_{n}\\right)^{5} \\leq n^{4}\\left(a_{1}^{5}+\\cdots+a_{n}^{5}\\right) .\n$$\n\n4 Prove Hölder's inequality (7.9) in the cases $p=1$ and $p=\\infty$.\n\n5 Suppose that $(X, \\mathcal{S}, \\mu)$ is a measure space, $1<p<\\infty, f \\in \\mathcal{L}^{p}(\\mu)$, and $h \\in \\mathcal{L}^{p^{\\prime}}(\\mu)$. Prove that Hölder's inequality (7.9) is an equality if and only if there exist nonnegative numbers $a$ and $b$, not both 0 , such that\n\n$$\na|f(x)|^{p}=b|h(x)|^{p^{\\prime}}\n$$\n\nfor almost every $x \\in X$.\n\n6 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $f \\in \\mathcal{L}^{1}(\\mu)$, and $h \\in \\mathcal{L}^{\\infty}(\\mu)$. Prove that $\\|f h\\|_{1}=\\|f\\|_{1}\\|h\\|_{\\infty}$ if and only if\n\n$$\n|h(x)|=\\|h\\|_{\\infty}\n$$\n\nfor almost every $x \\in X$ such that $f(x) \\neq 0$.\n\n7 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f, h: X \\rightarrow \\mathbf{F}$ are $\\mathcal{S}$-measurable. Prove that\n\n$$\n\\|f h\\|_{r} \\leq\\|f\\|_{p}\\|h\\|_{q}\n$$\n\nfor all positive numbers $p, q, r$ such that $\\frac{1}{p}+\\frac{1}{q}=\\frac{1}{r}$.\n\n8 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $n \\in \\mathbf{Z}^{+}$. Prove that\n\n$$\n\\left\\|f_{1} f_{2} \\cdots f_{n}\\right\\|_{1} \\leq\\left\\|f_{1}\\right\\|_{p_{1}}\\left\\|f_{2}\\right\\|_{p_{2}} \\cdots\\left\\|f_{n}\\right\\|_{p_{n}}\n$$\n\nfor all positive numbers $p_{1}, \\ldots, p_{n}$ such that $\\frac{1}{p_{1}}+\\frac{1}{p_{2}}+\\cdots+\\frac{1}{p_{n}}=1$ and all $\\mathcal{S}$-measurable functions $f_{1}, f_{2}, \\ldots, f_{n}: X \\rightarrow \\mathbf{F}$.\n\n9 Show that the formula in 7.12 holds for $p=\\infty$ if $\\mu$ is a $\\sigma$-finite measure.\n\n10 Suppose $0<p<q \\leq \\infty$.\n\n(a) Prove that $\\ell^{p} \\subset \\ell^{q}$.\n\n(b) Prove that $\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{p} \\geq\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{q}$ for every sequence $a_{1}, a_{2}, \\ldots$ of elements of $\\mathbf{F}$.\n\n11 Show that $\\bigcap_{p>1} \\ell^{p} \\neq \\ell^{1}$.\n\n12 Show that $\\bigcap_{p<\\infty} \\mathcal{L}^{p}([0,1]) \\neq \\mathcal{L}^{\\infty}([0,1])$.\n\n13 Show that $\\bigcup_{p>1} \\mathcal{L}^{p}([0,1]) \\neq \\mathcal{L}^{1}([0,1])$.\n\n14 Suppose $p, q \\in(0, \\infty]$, with $p \\neq q$. Prove that neither of the sets $\\mathcal{L}^{p}(\\mathbf{R})$ and $\\mathcal{L}^{q}(\\mathbf{R})$ is a subset of the other.\n\n15 Show that there exists $f \\in \\mathcal{L}^{2}(\\mathbf{R})$ such that $f \\notin \\mathcal{L}^{p}(\\mathbf{R})$ for all $p \\in(0, \\infty] \\backslash\\{2\\}$.\n\n16 Suppose $(X, \\mathcal{S}, \\mu)$ is a finite measure space. Prove that\n\n$$\n\\lim _{p \\rightarrow \\infty}\\|f\\|_{p}=\\|f\\|_{\\infty}\n$$\n\nfor every $\\mathcal{S}$-measurable function $f: X \\rightarrow \\mathbf{F}$.\n\n17 Suppose $\\mu$ is a measure, $0<p \\leq \\infty$, and $f \\in \\mathcal{L}^{p}(\\mu)$. Prove that for every $\\varepsilon>0$, there exists a simple function $g \\in \\mathcal{L}^{p}(\\mu)$ such that $\\|f-g\\|_{p}<\\varepsilon$.\n\n[This exercise extends 3.44.]\n\n18 Suppose $0<p<\\infty$ and $f \\in \\mathcal{L}^{p}(\\mathbf{R})$. Prove that for every $\\varepsilon>0$, there exists a step function $g \\in \\mathcal{L}^{p}(\\mathbf{R})$ such that $\\|f-g\\|_{p}<\\varepsilon$.\n\n[This exercise extends 3.47.]\n\n19 Suppose $0<p<\\infty$ and $f \\in \\mathcal{L}^{p}(\\mathbf{R})$. Prove that for every $\\varepsilon>0$, there exists a continuous function $g: \\mathbf{R} \\rightarrow \\mathbf{F}$ such that $\\|f-g\\|_{p}<\\varepsilon$ and the set $\\{x \\in \\mathbf{R}: g(x) \\neq 0\\}$ is bounded.\n\n[This exercise extends 3.48.]\n\n20 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $1<p<\\infty$, and $f, g \\in \\mathcal{L}^{p}(\\mu)$. Prove that Minkowski's inequality (7.14) is an equality if and only if there exist nonnegative numbers $a$ and $b$, not both 0 , such that\n\n$$\na f(x)=b g(x)\n$$\n\nfor almost every $x \\in X$.\n\n21 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $f, g \\in \\mathcal{L}^{1}(\\mu)$. Prove that\n\n$$\n\\|f+g\\|_{1}=\\|f\\|_{1}+\\|g\\|_{1}\n$$\n\nif and only if $f(x) \\overline{g(x)} \\geq 0$ for almost every $x \\in X$.\n\n22 Suppose $(X, \\mathcal{S}, \\mu)$ and $(Y, \\mathcal{T}, v)$ are $\\sigma$-finite measure spaces and $0<p<\\infty$. Prove that if $f \\in \\mathcal{L}^{p}(\\mu \\times v)$, then\n\n$$\n[f]_{x} \\in \\mathcal{L}^{p}(v) \\text { for almost every } x \\in X\n$$\n\nand\n\n$$\n[f]^{y} \\in \\mathcal{L}^{p}(\\mu) \\text { for almost every } y \\in Y\n$$\n\nwhere $[f]_{x}$ and $[f]^{y}$ are the cross sections of $f$ as defined in 5.7.\n\n23 Suppose $1 \\leq p<\\infty$ and $f \\in \\mathcal{L}^{p}(\\mathbf{R})$.\n\n(a) For $t \\in \\mathbf{R}$, define $f_{t}: \\mathbf{R} \\rightarrow \\mathbf{R}$ by $f_{t}(x)=f(x-t)$. Prove that the function $t \\mapsto\\left\\|f-f_{t}\\right\\|_{p}$ is bounded and uniformly continuous on $\\mathbf{R}$.\n\n(b) For $t>0$, define $f_{t}: \\mathbf{R} \\rightarrow \\mathbf{R}$ by $f_{t}(x)=f(t x)$. Prove that\n\n$$\n\\lim _{t \\rightarrow 1}\\left\\|f-f_{t}\\right\\|_{p}=0\n$$\n\n24 Suppose $1 \\leq p<\\infty$ and $f \\in \\mathcal{L}^{p}(\\mathbf{R})$. Prove that\n\n$$\n\\lim _{t \\downarrow 0} \\frac{1}{2 t} \\int_{b-t}^{b+t}|f-f(b)|^{p}=0\n$$\n\nfor almost every $b \\in \\mathbf{R}$.\n\n## 7B $L^{p}(\\mu)$\n\n## Definition of $L^{p}(\\mu)$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $1 \\leq p \\leq \\infty$. If there exists a nonempty set $E \\in \\mathcal{S}$ such that $\\mu(E)=0$, then $\\left\\|\\chi_{E}\\right\\|_{p}=0$ even though $\\chi_{E} \\neq 0$; thus $\\|\\cdot\\|_{p}$ is not a norm on $\\mathcal{L}^{p}(\\mu)$. The standard way to deal with this problem is to identify functions that differ only on a set of $\\mu$-measure 0 . To help make this process more rigorous, we introduce the following definitions.\n\n7.15 Definition $\\mathcal{Z}(\\mu) ; \\widetilde{f}$\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $0<p \\leq \\infty$.\n\n- $\\mathcal{Z}(\\mu)$ denotes the set of $\\mathcal{S}$-measurable functions from $X$ to $\\mathbf{F}$ that equal 0 almost everywhere.\n- For $f \\in \\mathcal{L}^{p}(\\mu)$, let $\\widetilde{f}$ be the subset of $\\mathcal{L}^{p}(\\mu)$ defined by\n\n$$\n\\widetilde{f}=\\{f+z: z \\in \\mathcal{Z}(\\mu)\\} .\n$$\n\nThe set $\\mathcal{Z}(\\mu)$ is clearly closed under scalar multiplication. Also, $\\mathcal{Z}(\\mu)$ is closed under addition because the union of two sets with $\\mu$-measure 0 is a set with $\\mu$ measure 0 . Thus $\\mathcal{Z}(\\mu)$ is a subspace of $\\mathcal{L}^{p}(\\mu)$, as we had noted in the third bullet point of Example 6.32.\n\nNote that if $f, F \\in \\mathcal{L}^{p}(\\mu)$, then $\\widetilde{f}=\\widetilde{F}$ if and only if $f(x)=F(x)$ for almost every $x \\in X$.\n\n### 7.16 Definition $L^{p}(\\mu)$\n\nSuppose $\\mu$ is a measure and $0<p \\leq \\infty$.\n\n- Let $L^{p}(\\mu)$ denote the collection of subsets of $\\mathcal{L}^{p}(\\mu)$ defined by\n\n$$\nL^{p}(\\mu)=\\left\\{\\widetilde{f}: f \\in \\mathcal{L}^{p}(\\mu)\\right\\} .\n$$\n\n- For $\\widetilde{f}, \\widetilde{g} \\in L^{p}(\\mu)$ and $\\alpha \\in \\mathbf{F}$, define $\\widetilde{f}+\\widetilde{g}$ and $\\alpha \\widetilde{f}$ by\n\n$$\n\\tilde{f}+\\widetilde{g}=(f+g)^{\\sim} \\quad \\text { and } \\quad \\alpha \\widetilde{f}=(\\alpha f)^{\\sim} \\text {. }\n$$\n\nThe last bullet point in the definition above requires a bit of care to verify that it makes sense. The potential problem is that if $\\mathcal{Z}(\\mu) \\neq\\{0\\}$, then $\\widetilde{f}$ is not uniquely represented by $f$. Thus suppose $f, F, g, G \\in \\mathcal{L}^{p}(\\mu)$ and $\\widetilde{f}=\\widetilde{F}$ and $\\widetilde{g}=\\widetilde{G}$. For the definition of addition in $L^{p}(\\mu)$ to make sense, we must verify that $(f+g)^{\\sim}=$ $(F+G)^{\\sim}$. This verification is left to the reader, as is the similar verification that the scalar multiplication defined in the last bullet point above makes sense.\n\nYou might want to think of elements of $L^{p}(\\mu)$ as equivalence classes of functions in $\\mathcal{L}^{p}(\\mu)$, where two functions are equivalent if they agree almost everywhere.\n\nMathematicians often pretend that elements of $L^{p}(\\mu)$ are functions, where two functions are considered to be equal if they differ only on a set of $\\mu$-measure 0 . This fiction is harmless provided that the operations you perform with such \"functions\" produce the same results if the functions are changed on a set of measure 0 .\n\nNote the subtle typographic difference between $\\mathcal{L}^{p}(\\mu)$ and $L^{p}(\\mu)$. An element of the calligraphic $\\mathcal{L}^{p}(\\mu)$ is a function; an element of the italic $L^{p}(\\mu)$ is a set of functions, any two of which agree almost everywhere.\n\n### 7.17 Definition $\\|\\cdot\\|_{p}$ on $L^{p}(\\mu)$\n\nSuppose $\\mu$ is a measure and $0<p \\leq \\infty$. Define $\\|\\cdot\\|_{p}$ on $L^{p}(\\mu)$ by\n\n$$\n\\|\\tilde{f}\\|_{p}=\\|f\\|_{p}\n$$\n\nfor $f \\in \\mathcal{L}^{p}(\\mu)$.\n\nNote that if $f, F \\in \\mathcal{L}^{p}(\\mu)$ and $\\widetilde{f}=\\widetilde{F}$, then $\\|f\\|_{p}=\\|F\\|_{p}$. Thus the definition above makes sense.\n\nIn the result below, the addition and scalar multiplication on $L^{p}(\\mu)$ come from 7.16 and the norm comes from 7.17.\n\n### 7.18 $L^{p}(\\mu)$ is a normed vector space\n\nSuppose $\\mu$ is a measure and $1 \\leq p \\leq \\infty$. Then $L^{p}(\\mu)$ is a vector space and $\\|\\cdot\\|_{p}$ is a norm on $L^{p}(\\mu)$.\n\nThe proof of the result above is left to the reader, who will surely use Minkowski's inequality (7.14) to verify the triangle inequality. Note that the additive identity of $L^{p}(\\mu)$ is $\\widetilde{0}$, which equals $\\mathcal{Z}(\\mu)$.\n\nFor readers familiar with quotients of vector spaces: you may recognize that $L^{p}(\\mu)$ is the quotient space\n\n$$\n\\mathcal{L}^{p}(\\mu) / \\mathcal{Z}(\\mu) .\n$$\n\nFor readers who want to learn about quotients of vector spaces: see a textbook for a second course in linear algebra.\n\nIf $\\mu$ is counting measure on $\\mathbf{Z}^{+}$, then\n\n$$\n\\mathcal{L}^{p}(\\mu)=L^{p}(\\mu)=\\ell^{p}\n$$\n\nbecause counting measure has no sets of measure 0 other than the empty set.\n\nIn the next definition, note that if $E$ is a Borel set then 2.95 implies $L^{p}(E)$ using Borel measurable functions equals $L^{p}(E)$ using Lebesgue measurable functions.\n\n### 7.19 Definition $L^{p}(E)$ for $E \\subset \\mathbf{R}$\n\nIf $E$ is a Borel (or Lebesgue measurable) subset of $\\mathbf{R}$ and $0<p \\leq \\infty$, then $L^{p}(E)$ means $L^{p}\\left(\\lambda_{E}\\right)$, where $\\lambda_{E}$ denotes Lebesgue measure $\\lambda$ restricted to the Borel (or Lebesgue measurable) subsets of $\\mathbf{R}$ that are contained in $E$.\n\n## $L^{p}(\\mu)$ Is a Banach Space\n\nThe proof of the next result does all the hard work we need to prove that $L^{p}(\\mu)$ is a Banach space. However, we state the next result in terms of $\\mathcal{L}^{p}(\\mu)$ instead of $L^{p}(\\mu)$ so that we can work with genuine functions. Moving to $L^{p}(\\mu)$ will then be easy (see 7.24).\n\n### 7.20 Cauchy sequences in $\\mathcal{L}^{p}(\\mu)$ converge\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $1 \\leq p \\leq \\infty$. Suppose $f_{1}, f_{2}, \\ldots$ is a sequence of functions in $\\mathcal{L}^{p}(\\mu)$ such that for every $\\varepsilon>0$, there exists $n \\in \\mathbf{Z}^{+}$ such that\n\n$$\n\\left\\|f_{j}-f_{k}\\right\\|_{p}<\\varepsilon\n$$\n\nfor all $j \\geq n$ and $k \\geq n$. Then there exists $f \\in \\mathcal{L}^{p}(\\mu)$ such that\n\n$$\n\\lim _{k \\rightarrow \\infty}\\left\\|f_{k}-f\\right\\|_{p}=0\n$$\n\nProof The case $p=\\infty$ is left as an exercise for the reader. Thus assume $1 \\leq p<\\infty$.\n\nIt suffices to show that $\\lim _{m \\rightarrow \\infty}\\left\\|f_{k_{m}}-f\\right\\|_{p}=0$ for some $f \\in \\mathcal{L}^{p}(\\mu)$ and some subsequence $f_{k_{1}}, f_{k_{2}}, \\ldots$ (see Exercise 14 of Section $6 \\mathrm{~A}$, whose proof does not require the positive definite property of a norm).\n\nThus dropping to a subsequence (but not relabeling) and setting $f_{0}=0$, we can assume that\n\n$$\n\\sum_{k=1}^{\\infty}\\left\\|f_{k}-f_{k-1}\\right\\|_{p}<\\infty\n$$\n\nDefine functions $g_{1}, g_{2}, \\ldots$ and $g$ from $X$ to $[0, \\infty]$ by\n\n$$\ng_{m}(x)=\\sum_{k=1}^{m}\\left|f_{k}(x)-f_{k-1}(x)\\right| \\quad \\text { and } \\quad g(x)=\\sum_{k=1}^{\\infty}\\left|f_{k}(x)-f_{k-1}(x)\\right|\n$$\n\nMinkowski's inequality (7.14) implies that\n\n$$\n\\left\\|g_{m}\\right\\|_{p} \\leq \\sum_{k=1}^{m}\\left\\|f_{k}-f_{k-1}\\right\\|_{p}\n$$\n\nClearly $\\lim _{m \\rightarrow \\infty} g_{m}(x)=g(x)$ for every $x \\in X$. Thus the Monotone Convergence Theorem (3.11) and 7.21 imply\n\n7.22\n\n$$\n\\int g^{p} d \\mu=\\lim _{m \\rightarrow \\infty} \\int g_{m}^{p} d \\mu \\leq\\left(\\sum_{k=1}^{\\infty}\\left\\|f_{k}-f_{k-1}\\right\\|_{p}\\right)^{p}<\\infty\n$$\n\nThus $g(x)<\\infty$ for almost every $x \\in X$.\n\nBecause every infinite series of real numbers that converges absolutely also converges, for almost every $x \\in X$ we can define $f(x)$ by\n\n$$\nf(x)=\\sum_{k=1}^{\\infty}\\left(f_{k}(x)-f_{k-1}(x)\\right)=\\lim _{m \\rightarrow \\infty} \\sum_{k=1}^{m}\\left(f_{k}(x)-f_{k-1}(x)\\right)=\\lim _{m \\rightarrow \\infty} f_{m}(x) .\n$$\n\nIn particular, $\\lim _{m \\rightarrow \\infty} f_{m}(x)$ exists for almost every $x \\in X$. Define $f(x)$ to be 0 for those $x \\in X$ for which the limit does not exist.\n\nWe now have a function $f$ that is the pointwise limit (almost everywhere) of $f_{1}, f_{2}, \\ldots$. The definition of $f$ shows that $|f(x)| \\leq g(x)$ for almost every $x \\in X$. Thus 7.22 shows that $f \\in \\mathcal{L}^{p}(\\mu)$.\n\nTo show that $\\lim _{k \\rightarrow \\infty}\\left\\|f_{k}-f\\right\\|_{p}=0$, suppose $\\varepsilon>0$ and let $n \\in \\mathbf{Z}^{+}$be such that $\\left\\|f_{j}-f_{k}\\right\\|_{p}<\\varepsilon$ for all $j \\geq n$ and $k \\geq n$. Suppose $k \\geq n$. Then\n\n$$\n\\begin{aligned}\n\\left\\|f_{k}-f\\right\\|_{p} & =\\left(\\int\\left|f_{k}-f\\right|^{p} d \\mu\\right)^{1 / p} \\\\\n& \\leq \\liminf _{j \\rightarrow \\infty}\\left(\\int\\left|f_{k}-f_{j}\\right|^{p} d \\mu\\right)^{1 / p} \\\\\n& =\\liminf _{j \\rightarrow \\infty}\\left\\|f_{k}-f_{j}\\right\\|_{p} \\\\\n& \\leq \\varepsilon\n\\end{aligned}\n$$\n\nwhere the second line above comes from Fatou's Lemma (Exercise 17 in Section 3A). Thus $\\lim _{k \\rightarrow \\infty}\\left\\|f_{k}-f\\right\\|_{p}=0$, as desired.\n\nThe proof that we have just completed contains within it the proof of a useful result that is worth stating separately. A sequence can converge in $p$-norm without converging pointwise anywhere (see, for example, Exercise 12). However, the next result guarantees that some subsequence converges pointwise almost everywhere.\n\n### 7.23 convergent sequences in $\\mathcal{L}^{p}$ have pointwise convergent subsequences\n\nSuppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $1 \\leq p \\leq \\infty$. Suppose $f \\in \\mathcal{L}^{p}(\\mu)$ and $f_{1}, f_{2}, \\ldots$ is a sequence of functions in $\\mathcal{L}^{p}(\\mu)$ such that $\\lim _{k \\rightarrow \\infty}\\left\\|f_{k}-f\\right\\|_{p}=0$.\n\nThen there exists a subsequence $f_{k_{1}}, f_{k_{2}}, \\ldots$ such that\n\n$$\n\\lim _{m \\rightarrow \\infty} f_{k_{m}}(x)=f(x)\n$$\n\nfor almost every $x \\in X$.\n\nProof Suppose $f_{k_{1}}, f_{k_{2}}, \\ldots$ is a subsequence such that\n\n$$\n\\sum_{m=2}^{\\infty}\\left\\|f_{k_{m}}-f_{k_{m-1}}\\right\\|_{p}<\\infty\n$$\n\nAn examination of the proof of 7.20 shows that $\\lim _{m \\rightarrow \\infty} f_{k_{m}}(x)=f(x)$ for almost every $x \\in X$.\n\n### 7.24 $L^{p}(\\mu)$ is a Banach space\n\nSuppose $\\mu$ is a measure and $1 \\leq p \\leq \\infty$. Then $L^{p}(\\mu)$ is a Banach space.\n\nProof This result follows immediately from 7.20 and the appropriate definitions.\n\n## Duality\n\nRecall that the dual space of a normed vector space $V$ is denoted by $V^{\\prime}$ and is defined to be the Banach space of bounded linear functionals on $V$ (see 6.71).\n\nIn the statement and proof of the next result, an element of an $L^{p}$ space is denoted by a symbol that makes it look like a function rather than like a collection of functions that agree except on a set of measure 0 . However, because integrals and $L^{p}$-norms are unchanged when functions change only on a set of measure 0 , this notational convenience causes no problems.\n\n7.25 natural map of $L^{p^{\\prime}}(\\mu)$ into $\\left(L^{p}(\\mu)\\right)^{\\prime}$ preserves norms\n\nSuppose $\\mu$ is a measure and $1<p \\leq \\infty$. For $h \\in L^{p^{\\prime}}(\\mu)$, define $\\varphi_{h}: L^{p}(\\mu) \\rightarrow \\mathbf{F}$ by\n\n$$\n\\varphi_{h}(f)=\\int f h d \\mu\n$$\n\nThen $h \\mapsto \\varphi_{h}$ is a one-to-one linear map from $L^{p^{\\prime}}(\\mu)$ to $\\left(L^{p}(\\mu)\\right)^{\\prime}$. Furthermore, $\\left\\|\\varphi_{h}\\right\\|=\\|h\\|_{p^{\\prime}}$ for all $h \\in L^{p^{\\prime}}(\\mu)$.\n\nProof Suppose $h \\in L^{p^{\\prime}}(\\mu)$ and $f \\in L^{p}(\\mu)$. Then Hölder's inequality (7.9) tells us that $f h \\in L^{1}(\\mu)$ and that\n\n$$\n\\|f h\\|_{1} \\leq\\|h\\|_{p^{\\prime}}\\|f\\|_{p}\n$$\n\nThus $\\varphi_{h}$, as defined above, is a bounded linear map from $L^{p}(\\mu)$ to $\\mathbf{F}$. Also, the map $h \\mapsto \\varphi_{h}$ is clearly a linear map of $L^{p^{\\prime}}(\\mu)$ into $\\left(L^{p}(\\mu)\\right)^{\\prime}$. Now 7.12 (with the roles of $p$ and $p^{\\prime}$ reversed) shows that\n\n$$\n\\left\\|\\varphi_{h}\\right\\|=\\sup \\left\\{\\left|\\varphi_{h}(f)\\right|: f \\in L^{p}(\\mu) \\text { and }\\|f\\|_{p} \\leq 1\\right\\}=\\|h\\|_{p^{\\prime}} \\text {. }\n$$\n\nIf $h_{1}, h_{2} \\in L^{p^{\\prime}}(\\mu)$ and $\\varphi_{h_{1}}=\\varphi_{h_{2}}$, then\n\n$$\n\\left\\|h_{1}-h_{2}\\right\\|_{p^{\\prime}}=\\left\\|\\varphi_{h_{1}-h_{2}}\\right\\|=\\left\\|\\varphi_{h_{1}}-\\varphi_{h_{2}}\\right\\|=\\|0\\|=0\n$$\n\nwhich implies $h_{1}=h_{2}$. Thus $h \\mapsto \\varphi_{h}$ is a one-to-one map from $L^{p^{\\prime}}(\\mu)$ to $\\left(L^{p}(\\mu)\\right)^{\\prime}$.\n\nThe result in 7.25 fails for some measures $\\mu$ if $p=1$. However, if $\\mu$ is a $\\sigma$-finite measure, then 7.25 holds even if $p=1$ (see Exercise 14).\n\nIs the range of the map $h \\mapsto \\varphi_{h}$ in 7.25 all of $\\left(L^{p}(\\mu)\\right)^{\\prime}$ ? The next result provides an affirmative answer to this question in the special case of $\\ell^{p}$ for $1 \\leq p<\\infty$. We will deal with this question for more general measures later (see 9.42; also see Exercise 25 in Section 8B).\n\nWhen thinking of $\\ell^{p}$ as a normed vector space, as in the next result, unless stated otherwise you should always assume that the norm on $\\ell^{p}$ is the usual norm $\\|\\cdot\\|_{p}$ that is associated with $\\mathcal{L}^{p}(\\mu)$, where $\\mu$ is counting measure on $\\mathbf{Z}^{+}$. In other words, if $1 \\leq p<\\infty$, then\n\n$$\n\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|_{p}=\\left(\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|^{p}\\right)^{1 / p}\n$$\n\n### 7.26 dual space of $\\ell^{p}$ can be identified with $\\ell^{p^{\\prime}}$\n\nSuppose $1 \\leq p<\\infty$. For $b=\\left(b_{1}, b_{2}, \\ldots\\right) \\in \\ell^{p^{\\prime}}$, define $\\varphi_{b}: \\ell^{p} \\rightarrow \\mathbf{F}$ by\n\n$$\n\\varphi_{b}(a)=\\sum_{k=1}^{\\infty} a_{k} b_{k}\n$$\n\nwhere $a=\\left(a_{1}, a_{2}, \\ldots\\right)$. Then $b \\mapsto \\varphi_{b}$ is a one-to-one linear map from $\\ell^{p^{\\prime}}$ onto $\\left(\\ell^{p}\\right)^{\\prime}$. Furthermore, $\\left\\|\\varphi_{b}\\right\\|=\\|b\\|_{p^{\\prime}}$ for all $b \\in \\ell^{p^{\\prime}}$.\n\nProof For $k \\in \\mathbf{Z}^{+}$, let $e_{k} \\in \\ell^{p}$ be the sequence in which each term is 0 except that the $k^{\\text {th }}$ term is 1 ; thus $e_{k}=(0, \\ldots, 0,1,0, \\ldots)$.\n\nSuppose $\\varphi \\in\\left(\\ell^{p}\\right)^{\\prime}$. Define a sequence $b=\\left(b_{1}, b_{2}, \\ldots\\right)$ of numbers in $\\mathbf{F}$ by\n\n$$\nb_{k}=\\varphi\\left(e_{k}\\right) .\n$$\n\nSuppose $a=\\left(a_{1}, a_{2}, \\ldots\\right) \\in \\ell^{p}$. Then\n\n$$\na=\\sum_{k=1}^{\\infty} a_{k} e_{k}\n$$\n\nwhere the infinite sum converges in the norm of $\\ell^{p}$ (the proof would fail here if we allowed $p$ to be $\\infty$ ). Because $\\varphi$ is a bounded linear functional on $\\ell^{p}$, applying $\\varphi$ to both sides of the equation above shows that\n\n$$\n\\varphi(a)=\\sum_{k=1}^{\\infty} a_{k} b_{k}\n$$\n\nWe still need to prove that $b \\in \\ell^{p^{\\prime}}$. To do this, for $n \\in \\mathbf{Z}^{+}$let $\\mu_{n}$ be counting measure on $\\{1,2, \\ldots, n\\}$. We can think of $L^{p}\\left(\\mu_{n}\\right)$ as a subspace of $\\ell^{p}$ by identifying each $\\left(a_{1}, \\ldots, a_{n}\\right) \\in L^{p}\\left(\\mu_{n}\\right)$ with $\\left(a_{1}, \\ldots, a_{n}, 0,0, \\ldots\\right) \\in \\ell^{p}$. Restricting the linear functional $\\varphi$ to $L^{p}\\left(\\mu_{n}\\right)$ gives the linear functional on $L^{p}\\left(\\mu_{n}\\right)$ that satisfies the following equation:\n\n$$\n\\left.\\varphi\\right|_{L^{p}\\left(\\mu_{n}\\right)}\\left(a_{1}, \\ldots, a_{n}\\right)=\\sum_{k=1}^{n} a_{k} b_{k}\n$$\n\nNow 7.25 [also see Exercise 14(b) for the case where $p=1$ ] gives\n\n$$\n\\begin{aligned}\n\\left\\|\\left(b_{1}, \\ldots, b_{n}\\right)\\right\\|_{p^{\\prime}} & =\\left\\|\\left.\\varphi\\right|_{L^{p}\\left(\\mu_{n}\\right)}\\right\\| \\\\\n& \\leq\\|\\varphi\\| .\n\\end{aligned}\n$$\n\nBecause $\\lim _{n \\rightarrow \\infty}\\left\\|\\left(b_{1}, \\ldots, b_{n}\\right)\\right\\|_{p^{\\prime}}=\\|b\\|_{p^{\\prime}}$, the inequality above implies the inequality $\\|b\\|_{p^{\\prime}} \\leq\\|\\varphi\\|$. Thus $b \\in \\ell^{p^{\\prime}}$, which implies that $\\varphi=\\varphi_{b}$, completing the proof.\n\nThe previous result does not hold when $p=\\infty$. In other words, the dual space of $\\ell^{\\infty}$ cannot be identified with $\\ell^{1}$. However, see Exercise 15, which shows that the dual space of a natural subspace of $\\ell^{\\infty}$ can be identified with $\\ell^{1}$.\n\n## EXERCISES 7B\n\n1 Suppose $n>1$ and $0<p<1$. Prove that if $\\|\\cdot\\|$ is defined on $\\mathbf{F}^{n}$ by\n\n$$\n\\left\\|\\left(a_{1}, \\ldots, a_{n}\\right)\\right\\|=\\left(\\left|a_{1}\\right|^{p}+\\cdots+\\left|a_{n}\\right|^{p}\\right)^{1 / p}\n$$\n\nthen $\\|\\cdot\\|$ is not a norm on $\\mathbf{F}^{n}$.\n\n2 (a) Suppose $1 \\leq p<\\infty$. Prove that there is a countable subset of $\\ell^{p}$ whose closure equals $\\ell^{p}$.\n\n(b) Prove that there does not exist a countable subset of $\\ell^{\\infty}$ whose closure equals $\\ell^{\\infty}$.\n\n3 (a) Suppose $1 \\leq p<\\infty$. Prove that there is a countable subset of $L^{p}(\\mathbf{R})$ whose closure equals $L^{p}(\\mathbf{R})$.\n\n(b) Prove that there does not exist a countable subset of $L^{\\infty}(\\mathbf{R})$ whose closure equals $L^{\\infty}(\\mathbf{R})$.\n\n4 Suppose $(X, \\mathcal{S}, \\mu)$ is a $\\sigma$-finite measure space and $1 \\leq p \\leq \\infty$. Prove that if $f: X \\rightarrow \\mathbf{F}$ is an $\\mathcal{S}$-measurable function such that $f h \\in \\mathcal{L}^{1}(\\mu)$ for every $h \\in \\mathcal{L}^{p^{\\prime}}(\\mu)$, then $f \\in \\mathcal{L}^{p}(\\mu)$.\n\n5 (a) Prove that if $\\mu$ is a measure, $1<p<\\infty$, and $f, g \\in L^{p}(\\mu)$ are such that\n\n$$\n\\|f\\|_{p}=\\|g\\|_{p}=\\left\\|\\frac{f+g}{2}\\right\\|_{p}\n$$\n\nthen $f=g$.\n\n(b) Give an example to show that the result in part (a) can fail if $p=1$.\n\n(c) Give an example to show that the result in part (a) can fail if $p=\\infty$.\n\n6 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space and $0<p<1$. Show that\n\n$$\n\\|f+g\\|_{p}^{p} \\leq\\|f\\|_{p}^{p}+\\|g\\|_{p}^{p}\n$$\n\nfor all $\\mathcal{S}$-measurable functions $f, g: X \\rightarrow \\mathbf{F}$.\n\n7 Prove that $L^{p}(\\mu)$, with addition and scalar multiplication as defined in 7.16 and norm defined as in 7.17, is a normed vector space. In other words, prove 7.18.\n\n8 Prove 7.20 for the case $p=\\infty$.\n\n9 Prove that 7.20 also holds for $p \\in(0,1)$.\n\n10 Prove that 7.23 also holds for $p \\in(0,1)$.\n\n11 Suppose $1 \\leq p \\leq \\infty$. Prove that\n\n$$\n\\left\\{\\left(a_{1}, a_{2}, \\ldots\\right) \\in \\ell^{p}: a_{k} \\neq 0 \\text { for every } k \\in \\mathbf{Z}^{+}\\right\\}\n$$\n\nis not an open subset of $\\ell^{p}$.\n\n12 Show that there exists a sequence $f_{1}, f_{2}, \\ldots$ of functions in $\\mathcal{L}^{1}([0,1])$ such that $\\lim _{k \\rightarrow \\infty}\\left\\|f_{k}\\right\\|_{1}=0$ but\n\n$$\n\\sup \\left\\{f_{k}(x): k \\in \\mathbf{Z}^{+}\\right\\}=\\infty\n$$\n\nfor every $x \\in[0,1]$.\n\n[This exercise shows that the conclusion of 7.23 cannot be improved to conclude that $\\lim _{k \\rightarrow \\infty} f_{k}(x)=f(x)$ for almost every $x \\in X$.]\n\n13 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $1 \\leq p \\leq \\infty, f \\in \\mathcal{L}^{p}(\\mu)$, and $f_{1}, f_{2}, \\ldots$ is a sequence in $\\mathcal{L}^{p}(\\mu)$ such that $\\lim _{k \\rightarrow \\infty}\\left\\|f_{k}-f\\right\\|_{p}=0$. Show that if $g: X \\rightarrow \\mathbf{F}$ is a function such that $\\lim _{k \\rightarrow \\infty} f_{k}(x)=g(x)$ for almost every $x \\in X$, then $f(x)=g(x)$ for almost every $x \\in X$.\n\n14 (a) Give an example of a measure $\\mu$ such that 7.25 fails for $p=1$.\n\n(b) Show that if $\\mu$ is a $\\sigma$-finite measure, then 7.25 holds for $p=1$.\n\n15 Let\n\n$$\nc_{0}=\\left\\{\\left(a_{1}, a_{2}, \\ldots\\right) \\in \\ell^{\\infty}: \\lim _{k \\rightarrow \\infty} a_{k}=0\\right\\} .\n$$\n\nGive $c_{0}$ the norm that it inherits as a subspace of $\\ell^{\\infty}$.\n\n(a) Prove that $c_{0}$ is a Banach space.\n\n(b) Prove that the dual space of $c_{0}$ can be identified with $\\ell^{1}$.\n\n16 Suppose $1 \\leq p \\leq 2$.\n\n(a) Prove that if $w, z \\in \\mathbf{C}$, then\n\n$$\n\\frac{|w+z|^{p}+|w-z|^{p}}{2} \\leq|w|^{p}+|z|^{p} \\leq \\frac{|w+z|^{p}+|w-z|^{p}}{2^{p-1}} .\n$$\n\n(b) Prove that if $\\mu$ is a measure and $f, g \\in \\mathcal{L}^{p}(\\mu)$, then\n\n$$\n\\frac{\\|f+g\\|_{p}^{p}+\\|f-g\\|_{p}^{p}}{2} \\leq\\|f\\|_{p}^{p}+\\|g\\|_{p}^{p} \\leq \\frac{\\|f+g\\|_{p}^{p}+\\|f-g\\|_{p}^{p}}{2^{p-1}} .\n$$\n\n17 Suppose $2 \\leq p<\\infty$.\n\n(a) Prove that if $w, z \\in \\mathbf{C}$, then\n\n$$\n\\frac{|w+z|^{p}+|w-z|^{p}}{2^{p-1}} \\leq|w|^{p}+|z|^{p} \\leq \\frac{|w+z|^{p}+|w-z|^{p}}{2}\n$$\n\n(b) Prove that if $\\mu$ is a measure and $f, g \\in \\mathcal{L}^{p}(\\mu)$, then\n\n$$\n\\frac{\\|f+g\\|_{p}^{p}+\\|f-g\\|_{p}^{p}}{2^{p-1}} \\leq\\|f\\|_{p}^{p}+\\|g\\|_{p}^{p} \\leq \\frac{\\|f+g\\|_{p}^{p}+\\|f-g\\|_{p}^{p}}{2} .\n$$\n\n[The inequalities in the two previous exercises are called Clarkson's inequalities. They were discovered by James Clarkson in 1936.]\n\n18 Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space, $1 \\leq p, q \\leq \\infty$, and $h: X \\rightarrow \\mathbf{F}$ is an $\\mathcal{S}$-measurable function such that $h f \\in L^{q}(\\mu)$ for every $f \\in L^{p}(\\mu)$. Prove that $f \\mapsto h f$ is a continuous linear map from $L^{p}(\\mu)$ to $L^{q}(\\mu)$.\n\nA Banach space is called reflexive if the canonical isometry of the Banach space into its double dual space is surjective (see Exercise 20 in Section $6 D$ for the definitions of the double dual space and the canonical isometry).\n\n19 Prove that if $1<p<\\infty$, then $\\ell^{p}$ is reflexive.\n\n20 Prove that $\\ell^{1}$ is not reflexive.\n\n21 Show that with the natural identifications, the canonical isometry of $c_{0}$ into its double dual space is the inclusion map of $c_{0}$ into $\\ell^{\\infty}$ (see Exercise 15 for the definition of $c_{0}$ and an identification of its dual space).\n\n22 Suppose $1 \\leq p<\\infty$ and $V, W$ are Banach spaces. Show that $V \\times W$ is a Banach space if the norm on $V \\times W$ is defined by\n\n$$\n\\|(f, g)\\|=\\left(\\|f\\|^{p}+\\|g\\|^{p}\\right)^{1 / p}\n$$\n\nfor $f \\in V$ and $g \\in W$.\n\n## Chapter 8\n\n## Hilbert Spaces\n\nNormed vector spaces and Banach spaces, which were introduced in Chapter 6, capture the notion of distance. In this chapter we introduce inner product spaces, which capture the notion of angle. The concept of orthogonality, which corresponds to right angles in the familiar context of $\\mathbf{R}^{2}$ or $\\mathbf{R}^{3}$, plays a particularly important role in inner product spaces.\n\nJust as a Banach space is defined to be a normed vector space in which every Cauchy sequence converges, a Hilbert space is defined to be an inner product space that is a Banach space. Hilbert spaces are named in honor of David Hilbert (18621943), who helped develop parts of the theory in the early twentieth century.\n\nIn this chapter, we will see a clean description of the bounded linear functionals on a Hilbert space. We will also see that every Hilbert space has an orthonormal basis, which make Hilbert spaces look much like standard Euclidean spaces but with infinite sums replacing finite sums.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-226.jpg?height=525&width=1160&top_left_y=1062&top_left_x=67)\n\nThe Mathematical Institute at the University of Göttingen, Germany. This building was opened in 1930, when Hilbert was near the end of his career there. Other prominent mathematicians who taught at the University of Göttingen and made major contributions to mathematics include Richard Courant (1888-1972), Richard Dedekind (1831-1916), Gustav Lejeune Dirichlet (1805-1859), Carl Friedrich Gauss (1777-1855), Hermann Minkowski (1864-1909), Emmy Noether (1882-1935), and Bernhard Riemann (1826-1866).\n\nCC-BY-SA Daniel Schwen\n\n## 8A Inner Product Spaces\n\n## Inner Products\n\nIf $p=2$, then the dual exponent $p^{\\prime}$ also equals 2. In this special case Hölder's inequality (7.9) implies that if $\\mu$ is a measure, then\n\n$$\n\\left|\\int f g d \\mu\\right| \\leq\\|f\\|_{2}\\|g\\|_{2}\n$$\n\nfor all $f, g \\in \\mathcal{L}^{2}(\\mu)$. Thus we can associate with each pair of functions $f, g \\in \\mathcal{L}^{2}(\\mu)$ a number $\\int f g d \\mu$. An inner product is almost a generalization of this pairing, with a slight twist to get a closer connection to the $L^{2}(\\mu)$-norm.\n\nIf $g=f$ and $\\mathbf{F}=\\mathbf{R}$, then the left side of the inequality above is $\\|f\\|_{2}^{2}$. However, if $g=f$ and $\\mathbf{F}=\\mathbf{C}$, then the left side of the inequality above need not equal $\\|f\\|_{2}^{2}$. Instead, we should take $g=\\bar{f}$ to get $\\|f\\|_{2}^{2}$ above.\n\nThe observations above suggest that we should consider the pairing that takes $f, g$ to $\\int f \\bar{g} d \\mu$. Then pairing $f$ with itself gives $\\|f\\|_{2}^{2}$.\n\nNow we are ready to define inner products, which abstract the key properties of the pairing $f, g \\mapsto \\int f \\bar{g} d \\mu$ on $L^{2}(\\mu)$, where $\\mu$ is a measure.\n\n### 8.1 Definition inner product; inner product space\n\nAn inner product on a vector space $V$ is a function that takes each ordered pair $f, g$ of elements of $V$ to a number $\\langle f, g\\rangle \\in \\mathbf{F}$ and has the following properties:\n\n## - positivity\n\n$\\langle f, f\\rangle \\in[0, \\infty)$ for all $f \\in V$;\n\n## - definiteness\n\n$\\langle f, f\\rangle=0$ if and only if $f=0$;\n\n## - linearity in first slot\n\n$\\langle f+g, h\\rangle=\\langle f, h\\rangle+\\langle g, h\\rangle$ and $\\langle\\alpha f, g\\rangle=\\alpha\\langle f, g\\rangle$ for all $f, g, h \\in V$ and all $\\alpha \\in \\mathbf{F}$\n\n## - conjugate symmetry\n\n$\\langle f, g\\rangle=\\overline{\\langle g, f\\rangle}$ for all $f, g \\in V$.\n\nA vector space with an inner product on it is called an inner product space. The terminology real inner product space indicates that $\\mathbf{F}=\\mathbf{R}$; the terminology complex inner product space indicates that $\\mathbf{F}=\\mathbf{C}$.\n\nIf $\\mathbf{F}=\\mathbf{R}$, then the complex conjugate above can be ignored and the conjugate symmetry property above can be rewritten more simply as $\\langle f, g\\rangle=\\langle g, f\\rangle$ for all $f, g \\in V$.\n\nAlthough most mathematicians define an inner product as above, many physicists use a definition that requires linearity in the second slot instead of the first slot.\n\n### 8.2 Example inner product spaces\n\n- For $n \\in \\mathbf{Z}^{+}$, define an inner product on $\\mathbf{F}^{n}$ by\n\n$$\n\\left\\langle\\left(a_{1}, \\ldots, a_{n}\\right),\\left(b_{1}, \\ldots, b_{n}\\right)\\right\\rangle=a_{1} \\overline{b_{1}}+\\cdots+a_{n} \\overline{b_{n}}\n$$\n\nfor $\\left(a_{1}, \\ldots, a_{n}\\right),\\left(b_{1}, \\ldots, b_{n}\\right) \\in \\mathbf{F}^{n}$. When thinking of $\\mathbf{F}^{n}$ as an inner product space, we always mean this inner product unless the context indicates some other inner product.\n\n- Define an inner product on $\\ell^{2}$ by\n\n$$\n\\left\\langle\\left(a_{1}, a_{2}, \\ldots\\right),\\left(b_{1}, b_{2}, \\ldots\\right)\\right\\rangle=\\sum_{k=1}^{\\infty} a_{k} \\overline{b_{k}}\n$$\n\nfor $\\left(a_{1}, a_{2}, \\ldots\\right),\\left(b_{1}, b_{2}, \\ldots\\right) \\in \\ell^{2}$. Hölder's inequality (7.9), as applied to counting measure on $\\mathbf{Z}^{+}$and taking $p=2$, implies that the infinite sum above converges absolutely and hence converges to an element of $\\mathbf{F}$. When thinking of $\\ell^{2}$ as an inner product space, we always mean this inner product unless the context indicates some other inner product.\n\n- Define an inner product on $C([0,1])$, which is the vector space of continuous functions from $[0,1]$ to $\\mathbf{F}$, by\n\n$$\n\\langle f, g\\rangle=\\int_{0}^{1} f \\bar{g}\n$$\n\nfor $f, g \\in C([0,1])$. The definiteness requirement for an inner product is satisfied because if $f:[0,1] \\rightarrow \\mathbf{F}$ is a continuous function such that $\\int_{0}^{1}|f|^{2}=0$, then the function $f$ is identically 0 .\n\n- Suppose $(X, \\mathcal{S}, \\mu)$ is a measure space. Define an inner product on $L^{2}(\\mu)$ by\n\n$$\n\\langle f, g\\rangle=\\int f \\bar{g} d \\mu\n$$\n\nfor $f, g \\in L^{2}(\\mu)$. Hölder's inequality (7.9) with $p=2$ implies that the integral above makes sense as an element of $\\mathbf{F}$. When thinking of $L^{2}(\\mu)$ as an inner product space, we always mean this inner product unless the context indicates some other inner product.\n\nHere we use $L^{2}(\\mu)$ rather than $\\mathcal{L}^{2}(\\mu)$ because the definiteness requirement fails on $\\mathcal{L}^{2}(\\mu)$ if there exist nonempty sets $E \\in \\mathcal{S}$ such that $\\mu(E)=0$ (consider $\\left\\langle\\chi_{E^{\\prime}} \\chi_{E}\\right\\rangle$ to see the problem).\n\nThe first two bullet points in this example are special cases of $L^{2}(\\mu)$, taking $\\mu$ to be counting measure on either $\\{1, \\ldots, n\\}$ or $\\mathbf{Z}^{+}$.\n\nAs we will see, even though the main examples of inner product spaces are $L^{2}(\\mu)$ spaces, working with the inner product structure is often cleaner and simpler than working with measures and integrals.\n\n## 8.3 basic properties of an inner product\n\nSuppose $V$ is an inner product space. Then\n\n(a) $\\langle 0, g\\rangle=\\langle g, 0\\rangle=0$ for every $g \\in V$;\n\n(b) $\\langle f, g+h\\rangle=\\langle f, g\\rangle+\\langle f, h\\rangle$ for all $f, g, h \\in V$;\n\n(c) $\\langle f, \\alpha g\\rangle=\\bar{\\alpha}\\langle f, g\\rangle$ for all $\\alpha \\in \\mathbf{F}$ and $f, g \\in V$.\n\n## Proof\n\n(a) For $g \\in V$, the function $f \\mapsto\\langle f, g\\rangle$ is a linear map from $V$ to $\\mathbf{F}$. Because every linear map takes 0 to 0 , we have $\\langle 0, g\\rangle=0$. Now the conjugate symmetry property of an inner product implies that\n\n$$\n\\langle g, 0\\rangle=\\overline{\\langle 0, g\\rangle}=\\overline{0}=0\n$$\n\n(b) Suppose $f, g, h \\in V$. Then\n\n$$\n\\langle f, g+h\\rangle=\\overline{\\langle g+h, f\\rangle}=\\overline{\\langle g, f\\rangle+\\langle h, f\\rangle}=\\overline{\\langle g, f\\rangle}+\\overline{\\langle h, f\\rangle}=\\langle f, g\\rangle+\\langle f, h\\rangle .\n$$\n\n(c) Suppose $\\alpha \\in \\mathbf{F}$ and $f, g \\in V$. Then\n\n$$\n\\langle f, \\alpha g\\rangle=\\overline{\\langle\\alpha g, f\\rangle}=\\overline{\\alpha\\langle g, f\\rangle}=\\bar{\\alpha} \\overline{\\langle g, f\\rangle}=\\bar{\\alpha}\\langle f, g\\rangle,\n$$\n\nas desired.\n\nIf $\\mathbf{F}=\\mathbf{R}$, then parts (b) and (c) of 8.3 imply that for $f \\in V$, the function $g \\mapsto\\langle f, g\\rangle$ is a linear map from $V$ to $\\mathbf{R}$. However, if $\\mathbf{F}=\\mathbf{C}$ and $f \\neq 0$, then the function $g \\mapsto\\langle f, g\\rangle$ is not a linear map from $V$ to $\\mathbf{C}$ because of the complex conjugate in part (c) of 8.3.\n\n## Cauchy-Schwarz Inequality and Triangle Inequality\n\nNow we can define the norm associated with each inner product. We use the word norm (which will turn out to be correct) even though it is not yet clear that all the properties required of a norm are satisfied.\n\n### 8.4 Definition norm associated with an inner product; $\\|\\cdot\\|$\n\nSuppose $V$ is an inner product space. For $f \\in V$, define the norm of $f$, denoted $\\|f\\|$, by\n\n$$\n\\|f\\|=\\sqrt{\\langle f, f\\rangle} .\n$$\n\n### 8.5 Example norms on inner product spaces\n\nIn each of the following examples, the inner product is the standard inner product as defined in Example 8.2.\n\n- If $n \\in \\mathbf{Z}^{+}$and $\\left(a_{1}, \\ldots, a_{n}\\right) \\in \\mathbf{F}^{n}$, then\n\n$$\n\\left\\|\\left(a_{1}, \\ldots, a_{n}\\right)\\right\\|=\\sqrt{\\left|a_{1}\\right|^{2}+\\cdots+\\left|a_{n}\\right|^{2}} .\n$$\n\nThus the norm on $\\mathbf{F}^{n}$ associated with the standard inner product is the usual Euclidean norm.\n\n- If $\\left(a_{1}, a_{2}, \\ldots\\right) \\in \\ell^{2}$, then\n\n$$\n\\left\\|\\left(a_{1}, a_{2}, \\ldots\\right)\\right\\|=\\left(\\sum_{k=1}^{\\infty}\\left|a_{k}\\right|^{2}\\right)^{1 / 2}\n$$\n\nThus the norm associated with the inner product on $\\ell^{2}$ is just the standard norm $\\|\\cdot\\|_{2}$ on $\\ell^{2}$ as defined in Example 7.2.\n\n- If $\\mu$ is a measure and $f \\in L^{2}(\\mu)$, then\n\n$$\n\\|f\\|=\\left(\\int|f|^{2} d \\mu\\right)^{1 / 2} .\n$$\n\nThus the norm associated with the inner product on $L^{2}(\\mu)$ is just the standard norm $\\|\\cdot\\|_{2}$ on $L^{2}(\\mu)$ as defined in 7.17.\n\nThe definition of an inner product (8.1) implies that if $V$ is an inner product space and $f \\in V$, then\n\n- $\\|f\\| \\geq 0$\n- $\\|f\\|=0$ if and only if $f=0$.\n\nThe proof of the next result illustrates a frequently used property of the norm on an inner product space: working with the square of the norm is often easier than working directly with the norm.\n\n## 8.6 homogeneity of the norm\n\nSuppose $V$ is an inner product space, $f \\in V$, and $\\alpha \\in \\mathbf{F}$. Then\n\n$$\n\\|\\alpha f\\|=|\\alpha|\\|f\\| \\text {. }\n$$\n\nProof We have\n\n$$\n\\|\\alpha f\\|^{2}=\\langle\\alpha f, \\alpha f\\rangle=\\alpha\\langle f, \\alpha f\\rangle=\\alpha \\bar{\\alpha}\\langle f, f\\rangle=|\\alpha|^{2}\\|f\\|^{2} .\n$$\n\nTaking square roots now gives the desired equality.\n\nThe next definition plays a crucial role in the study of inner product spaces.\n\n### 8.7 Definition orthogonal\n\nTwo elements of an inner product space are called orthogonal if their inner product equals 0 .\n\nIn the definition above, the order of the two elements of the inner product space does not matter because $\\langle f, g\\rangle=0$ if and only if $\\langle g, f\\rangle=0$. Instead of saying that $f$ and $g$ are orthogonal, sometimes we say that $f$ is orthogonal to $g$.\n\n### 8.8 Example orthogonal elements of an inner product space\n\n- In $\\mathbf{C}^{3},(2,3,5 i)$ and $(6,1,-3 i)$ are orthogonal because\n\n$$\n\\langle(2,3,5 i),(6,1,-3 i)\\rangle=2 \\cdot 6+3 \\cdot 1+5 i \\cdot(3 i)=12+3-15=0 .\n$$\n\n- The elements of $L^{2}((-\\pi, \\pi])$ represented by $\\sin (3 t)$ and $\\cos (8 t)$ are orthogonal because\n\n$$\n\\int_{-\\pi}^{\\pi} \\sin (3 t) \\cos (8 t) d t=\\left[\\frac{\\cos (5 t)}{10}-\\frac{\\cos (11 t)}{22}\\right]_{t=-\\pi}^{t=\\pi}=0\n$$\n\nwhere $d t$ denotes integration with respect to Lebesgue measure on $(-\\pi, \\pi]$.\n\nExercise 8 asks you to prove that if $a$ and $b$ are nonzero elements in $\\mathbf{R}^{2}$, then\n\n$$\n\\langle a, b\\rangle=\\|a\\|\\|b\\| \\cos \\theta\n$$\n\nwhere $\\theta$ is the angle between $a$ and $b$ (thinking of $a$ as the vector whose initial point is the origin and whose end point is $a$, and similarly for $b$ ). Thus two elements of $\\mathbf{R}^{2}$ are orthogonal if and only if the cosine of the angle between them is 0 , which happens if and only if the vectors are perpendicular in the usual sense of plane geometry. Thus you can think of the word orthogonal as a fancy word meaning perpendicular.\n\n## Law professor Richard Friedman presenting a case before the U.S. Supreme Court in 2010\n\nMr. Friedman: I think that issue is entirely orthogonal to the issue here because the Commonwealth is acknowledging-\n\nChief Justice Roberts: I'm sorry. Entirely what?\n\nMr. Friedman: Orthogonal. Right angle. Unrelated. Irrelevant.\n\nChief Justice Roberts: Oh.\n\nJustice Scalia: What was that adjective? I liked that.\n\nMr. Friedman: Orthogonal.\n\nChief Justice Roberts: Orthogonal.\n\nMr. Friedman: Right, right.\n\nJustice Scalia: Orthogonal, ooh. (Laughter.)\n\nJustice Kennedy: I knew this case presented us a problem. (Laughter.)\n\nThe next theorem is over 2500 years old, although it was not originally stated in the context of inner product spaces.\n\n### 8.9 Pythagorean Theorem\n\nSuppose $f$ and $g$ are orthogonal elements of an inner product space. Then\n\n$$\n\\|f+g\\|^{2}=\\|f\\|^{2}+\\|g\\|^{2} \\text {. }\n$$\n\nProof We have\n\n$$\n\\begin{aligned}\n\\|f+g\\|^{2} & =\\langle f+g, f+g\\rangle \\\\\n& =\\langle f, f\\rangle+\\langle f, g\\rangle+\\langle g, f\\rangle+\\langle g, g\\rangle \\\\\n& =\\|f\\|^{2}+\\|g\\|^{2},\n\\end{aligned}\n$$\n\nas desired.\n\nExercise 3 shows that whether or not the converse of the Pythagorean Theorem holds depends upon whether $\\mathbf{F}=\\mathbf{R}$ or $\\mathbf{F}=\\mathbf{C}$.\n\nSuppose $f$ and $g$ are elements of an inner product space $V$, with $g \\neq 0$. Frequently it is useful to write $f$ as some number $c$ times $g$ plus an element $h$ of $V$ that is orthogonal to $g$. The figure here suggests that such a decomposition should be possible. To find the appropriate choice for $c$, note that if $f=c g+h$ for some $c \\in \\mathbf{F}$ and some $h \\in V$ with $\\langle h, g\\rangle=0$, then we must have\n\n$$\n\\langle f, g\\rangle=\\langle c g+h, g\\rangle=c\\|g\\|^{2},\n$$\n\nwhich implies that $c=\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}}$, which then implies that $h=f-\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}} g$. Hence we are led to the following result.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-232.jpg?height=366&width=243&top_left_y=930&top_left_x=983)\n\nHere $f=c g+h$, where $h$ is orthogonal to $g$.\n\n### 8.10 orthogonal decomposition\n\nSuppose $f$ and $g$ are elements of an inner product space, with $g \\neq 0$. Then there exists $h \\in V$ such that\n\n$$\n\\langle h, g\\rangle=0 \\quad \\text { and } \\quad f=\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}} g+h\n$$\n\nProof Set $h=f-\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}} g$. Then\n\n$$\n\\langle h, g\\rangle=\\left\\langle f-\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}} g, g\\right\\rangle=\\langle f, g\\rangle-\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}}\\langle g, g\\rangle=0\n$$\n\ngiving the first equation in the conclusion. The second equation in the conclusion follows immediately from the definition of $h$.\n\nThe orthogonal decomposition 8.10 is the main ingredient in our proof of the next result, which is one of the most important inequalities in mathematics.\n\n### 8.11 Cauchy-Schwarz inequality\n\nSuppose $f$ and $g$ are elements of an inner product space. Then\n\n$$\n|\\langle f, g\\rangle| \\leq\\|f\\|\\|g\\|\n$$\n\nwith equality if and only if one of $f, g$ is a scalar multiple of the other.\n\nProof If $g=0$, then both sides of the desired inequality equal 0 . Thus we can assume $g \\neq 0$. Consider the orthogonal decomposition\n\n$$\nf=\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}} g+h\n$$\n\ngiven by 8.10 , where $h$ is orthogonal to $g$. The Pythagorean Theorem (8.9) implies\n\n$$\n\\begin{aligned}\n\\|f\\|^{2} & =\\left\\|\\frac{\\langle f, g\\rangle}{\\|g\\|^{2}} g\\right\\|^{2}+\\|h\\|^{2} \\\\\n& =\\frac{|\\langle f, g\\rangle|^{2}}{\\|g\\|^{2}}+\\|h\\|^{2} \\\\\n& \\geq \\frac{|\\langle f, g\\rangle|^{2}}{\\|g\\|^{2}} .\n\\end{aligned}\n$$\n\nMultiplying both sides of this inequality by $\\|g\\|^{2}$ and then taking square roots gives the desired inequality.\n\nThe proof above shows that the Cauchy-Schwarz inequality is an equality if and only if 8.12 is an equality. This happens if and only if $h=0$. But $h=0$ if and only if $f$ is a scalar multiple of $g$ (see 8.10). Thus the Cauchy-Schwarz inequality is an equality if and only if $f$ is a scalar multiple of $g$ or $g$ is a scalar multiple of $f$ (or both; the phrasing has been chosen to cover cases in which either $f$ or $g$ equals 0 ).\n\n### 8.13 Example Cauchy-Schwarz inequality for $\\mathbf{F}^{n}$\n\nApplying the Cauchy-Schwarz inequality with the standard inner product on $\\mathbf{F}^{n}$ to $\\left(\\left|a_{1}\\right|, \\ldots,\\left|a_{n}\\right|\\right)$ and $\\left(\\left|b_{1}\\right|, \\ldots,\\left|b_{n}\\right|\\right)$ gives the inequality\n\n$$\n\\left|a_{1} b_{1}\\right|+\\cdots+\\left|a_{n} b_{n}\\right| \\leq \\sqrt{\\left|a_{1}\\right|^{2}+\\cdots+\\left|a_{n}\\right|^{2}} \\sqrt{\\left|b_{1}\\right|^{2}+\\cdots+\\left|b_{n}\\right|^{2}}\n$$\n\nfor all $\\left(a_{1}, \\ldots, a_{n}\\right),\\left(b_{1}, \\ldots, b_{n}\\right) \\in \\mathbf{F}^{n}$.\n\nThus we have a new and clean proof of Hölder's inequality (7.9) for the special case where $\\mu$ is counting measure on\n\nThe inequality in this example was first proved by Cauchy in 1821. $\\{1, \\ldots, n\\}$ and $p=p^{\\prime}=2$.\n\n### 8.14 Example Cauchy-Schwarz inequality for $L^{2}(\\mu)$\n\nSuppose $\\mu$ is a measure and $f, g \\in L^{2}(\\mu)$. Applying the Cauchy-Schwarz inequality with the standard inner product on $L^{2}(\\mu)$ to $|f|$ and $|g|$ gives the inequality\n\n$$\n\\int|f g| d \\mu \\leq\\left(\\int|f|^{2} d \\mu\\right)^{1 / 2}\\left(\\int|g|^{2} d \\mu\\right)^{1 / 2} .\n$$\n\nThe inequality above is equivalent to Hölder's inequality (7.9) for the special case where $p=p^{\\prime}=2$. However, the proof of the inequality above via the Cauchy-Schwarz inequality still depends upon Hölder's inequality to show that the definition of the standard inner product on $L^{2}(\\mu)$ makes sense. See Exercise 18 in this section for a derivation of the in-\n\nIn 1859 Viktor Bunyakovsky (1804-1889), who had been Cauchy's student in Paris, first proved integral inequalities like the one above. Similar discoveries by Hermann Schwarz (1843-1921) in 1885 attracted more attention and led to the name of this inequality. equality above that is truly independent of Hölder's inequality.\n\nIf we think of the norm determined by an inner product as a length, then the triangle inequality has the geometric interpretation that the length of each side of a triangle is less than the sum of the lengths of the other two sides.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-234.jpg?height=244&width=434&top_left_y=944&top_left_x=758)\n\n### 8.15 triangle inequality\n\nSuppose $f$ and $g$ are elements of an inner product space. Then\n\n$$\n\\|f+g\\| \\leq\\|f\\|+\\|g\\|,\n$$\n\nwith equality if and only if one of $f, g$ is a nonnegative multiple of the other.\n\nProof We have\n\n$$\n\\begin{aligned}\n\\|f+g\\|^{2} & =\\langle f+g, f+g\\rangle \\\\\n& =\\langle f, f\\rangle+\\langle g, g\\rangle+\\langle f, g\\rangle+\\langle g, f\\rangle \\\\\n& =\\langle f, f\\rangle+\\langle g, g\\rangle+\\langle f, g\\rangle+\\overline{\\langle f, g\\rangle} \\\\\n& =\\|f\\|^{2}+\\|g\\|^{2}+2 \\operatorname{Re}\\langle f, g\\rangle\n\\end{aligned}\n$$\n\n8.16\n\n$$\n\\leq\\|f\\|^{2}+\\|g\\|^{2}+2|\\langle f, g\\rangle|\n$$\n\n8.17\n\n$$\n\\begin{aligned}\n& \\leq\\|f\\|^{2}+\\|g\\|^{2}+2\\|f\\|\\|g\\| \\\\\n& =(\\|f\\|+\\|g\\|)^{2}\n\\end{aligned}\n$$\n\nwhere 8.17 follows from the Cauchy-Schwarz inequality (8.11). Taking square roots of both sides of the inequality above gives the desired inequality.\n\nThe proof above shows that the triangle inequality is an equality if and only if we have equality in 8.16 and 8.17 . Thus we have equality in the triangle inequality if and only if\n\n8.18\n\n$$\n\\langle f, g\\rangle=\\|f\\|\\|g\\| \\text {. }\n$$\n\nIf one of $f, g$ is a nonnegative multiple of the other, then 8.18 holds, as you should verify. Conversely, suppose 8.18 holds. Then the condition for equality in the CauchySchwarz inequality (8.11) implies that one of $f, g$ is a scalar multiple of the other. Clearly 8.18 forces the scalar in question to be nonnegative, as desired.\n\nApplying the previous result to the inner product space $L^{2}(\\mu)$, where $\\mu$ is a measure, gives a new proof of Minkowski's inequality (7.14) for the case $p=2$.\n\nNow we can prove that what we have been calling a norm on an inner product space is indeed a norm.\n\n## $8.19\\|\\cdot\\|$ is a norm\n\nSuppose $V$ is an inner product space and $\\|f\\|$ is defined as usual by\n\n$$\n\\|f\\|=\\sqrt{\\langle f, f\\rangle}\n$$\n\nfor $f \\in V$. Then $\\|\\cdot\\|$ is a norm on $V$.\n\nProof The definition of an inner product implies that $\\|\\cdot\\|$ satisfies the positive definite requirement for a norm. The homogeneity and triangle inequality requirements for a norm are satisfied because of 8.6 and 8.15.\n\nThe next result has the geometric interpretation that the sum of the squares of the lengths of the diagonals of a parallelogram equals the sum of the squares of the lengths of the four sides.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-235.jpg?height=190&width=591&top_left_y=1277&top_left_x=636)\n\n### 8.20 parallelogram equality\n\nSuppose $f$ and $g$ are elements of an inner product space. Then\n\n$$\n\\|f+g\\|^{2}+\\|f-g\\|^{2}=2\\|f\\|^{2}+2\\|g\\|^{2} .\n$$\n\nProof We have\n\n$$\n\\begin{aligned}\n\\|f+g\\|^{2}+\\|f-g\\|^{2}= & \\langle f+g, f+g\\rangle+\\langle f-g, f-g\\rangle \\\\\n= & \\|f\\|^{2}+\\|g\\|^{2}+\\langle f, g\\rangle+\\langle g, f\\rangle \\\\\n& +\\|f\\|^{2}+\\|g\\|^{2}-\\langle f, g\\rangle-\\langle g, f\\rangle \\\\\n= & 2\\|f\\|^{2}+2\\|g\\|^{2},\n\\end{aligned}\n$$\n\nas desired.\n\n## EXERCISES 8A\n\n1 Let $V$ denote the vector space of bounded continuous functions from $\\mathbf{R}$ to $\\mathbf{F}$. Let $r_{1}, r_{2}, \\ldots$ be a list of the rational numbers. For $f, g \\in V$, define\n\n$$\n\\langle f, g\\rangle=\\sum_{k=1}^{\\infty} \\frac{f\\left(r_{k}\\right) \\overline{g\\left(r_{k}\\right)}}{2^{k}}\n$$\n\nShow that $\\langle\\cdot, \\cdot\\rangle$ is an inner product on $V$.\n\n2 Prove that if $\\mu$ is a measure and $f, g \\in L^{2}(\\mu)$, then\n\n$$\n\\|f\\|^{2}\\|g\\|^{2}-|\\langle f, g\\rangle|^{2}=\\frac{1}{2} \\iint|f(x) g(y)-g(x) f(y)|^{2} d \\mu(y) d \\mu(x) .\n$$\n\n3 Suppose $f$ and $g$ are elements of an inner product space and\n\n$$\n\\|f+g\\|^{2}=\\|f\\|^{2}+\\|g\\|^{2} \\text {. }\n$$\n\n(a) Prove that if $\\mathbf{F}=\\mathbf{R}$, then $f$ and $g$ are orthogonal.\n\n(b) Give an example to show that if $\\mathbf{F}=\\mathbf{C}$, then $f$ and $g$ can satisfy the equation above without being orthogonal.\n\n4 Find $a, b \\in \\mathbf{R}^{3}$ such that $a$ is a scalar multiple of $(1,6,3), b$ is orthogonal to $(1,6,3)$, and $(5,4,-2)=a+b$.\n\n5 Prove that\n\n$$\n16 \\leq(a+b+c+d)\\left(\\frac{1}{a}+\\frac{1}{b}+\\frac{1}{c}+\\frac{1}{d}\\right)\n$$\n\nfor all positive numbers $a, b, c, d$, with equality if and only if $a=b=c=d$.\n\n6 Prove that the square of the average of each finite list of real numbers containing at least two distinct real numbers is less than the average of the squares of the numbers in that list.\n\n7 Suppose $f$ and $g$ are elements of an inner product space and $\\|f\\| \\leq 1$ and $\\|g\\| \\leq 1$. Prove that\n\n$$\n\\sqrt{1-\\|f\\|^{2}} \\sqrt{1-\\|g\\|^{2}} \\leq 1-|\\langle f, g\\rangle|\n$$\n\n8 Suppose $a$ and $b$ are nonzero elements of $\\mathbf{R}^{2}$. Prove that\n\n$$\n\\langle a, b\\rangle=\\|a\\|\\|b\\| \\cos \\theta,\n$$\n\nwhere $\\theta$ is the angle between $a$ and $b$ (thinking of $a$ as the vector whose initial point is the origin and whose end point is $a$, and similarly for $b$ ).\n\nHint: Draw the triangle formed by $a, b$, and $a-b$; then use the law of cosines.\n\n9 The angle between two vectors (thought of as arrows with initial point at the origin) in $\\mathbf{R}^{2}$ or $\\mathbf{R}^{3}$ can be defined geometrically. However, geometry is not as clear in $\\mathbf{R}^{n}$ for $n>3$. Thus the angle between two nonzero vectors $a, b \\in \\mathbf{R}^{n}$ is defined to be\n\n$$\n\\arccos \\frac{\\langle a, b\\rangle}{\\|a\\|\\|b\\|}\n$$\n\nwhere the motivation for this definition comes from the previous exercise. Explain why the Cauchy-Schwarz inequality is needed to show that this definition makes sense.\n\n10 (a) Suppose $f$ and $g$ are elements of a real inner product space. Prove that $f$ and $g$ have the same norm if and only if $f+g$ is orthogonal to $f-g$.\n\n(b) Use part (a) to show that the diagonals of a parallelogram are perpendicular to each other if and only if the parallelogram is a rhombus.\n\n11 Suppose $f$ and $g$ are elements of an inner product space. Prove that $\\|f\\|=\\|g\\|$ if and only if $\\|s f+t g\\|=\\|t f+s g\\|$ for all $s, t \\in \\mathbf{R}$.\n\n12 Suppose $f$ and $g$ are elements of an inner product space and $\\|f\\|=\\|g\\|=1$ and $\\langle f, g\\rangle=1$. Prove that $f=g$.\n\n13 Suppose $f$ and $g$ are elements of a real inner product space. Prove that\n\n$$\n\\langle f, g\\rangle=\\frac{\\|f+g\\|^{2}-\\|f-g\\|^{2}}{4}\n$$\n\n14 Suppose $f$ and $g$ are elements of a complex inner product space. Prove that\n\n$$\n\\langle f, g\\rangle=\\frac{\\|f+g\\|^{2}-\\|f-g\\|^{2}+\\|f+i g\\|^{2} i-\\|f-i g\\|^{2} i}{4} .\n$$\n\n15 Suppose $f, g, h$ are elements of an inner product space. Prove that\n\n$$\n\\left\\|h-\\frac{1}{2}(f+g)\\right\\|^{2}=\\frac{\\|h-f\\|^{2}+\\|h-g\\|^{2}}{2}-\\frac{\\|f-g\\|^{2}}{4} \\text {. }\n$$\n\n16 Prove that a norm satisfying the parallelogram equality comes from an inner product. In other words, show that if $V$ is a normed vector space whose norm $\\|\\cdot\\|$ satisfies the parallelogram equality, then there is an inner product $\\langle\\cdot, \\cdot\\rangle$ on $V$ such that $\\|f\\|=\\langle f, f\\rangle^{1 / 2}$ for all $f \\in V$.\n\n17 Let $\\lambda$ denote Lebesgue measure on $[1, \\infty)$.\n\n(a) Prove that if $f:[1, \\infty) \\rightarrow[0, \\infty)$ is Borel measurable, then\n\n$$\n\\left(\\int_{1}^{\\infty} f(x) d \\lambda(x)\\right)^{2} \\leq \\int_{1}^{\\infty} x^{2}(f(x))^{2} d \\lambda(x)\n$$\n\n(b) Describe the set of Borel measurable functions $f:[1, \\infty) \\rightarrow[0, \\infty)$ such that the inequality in part (a) is an equality.\n\n18 Suppose $\\mu$ is a measure. For $f, g \\in L^{2}(\\mu)$, define $\\langle f, g\\rangle$ by\n\n$$\n\\langle f, g\\rangle=\\int f \\bar{g} d \\mu\n$$\n\n(a) Using the inequality\n\n$$\n|f(x) \\overline{g(x)}| \\leq \\frac{1}{2}\\left(|f(x)|^{2}+|g(x)|^{2}\\right)\n$$\n\nverify that the integral above makes sense and the map sending $f, g$ to $\\langle f, g\\rangle$ defines an inner product on $L^{2}(\\mu)$ (without using Hölder's inequality).\n\n(b) Show that the Cauchy-Schwarz inequality implies that\n\n$$\n\\|f g\\|_{1} \\leq\\|f\\|_{2}\\|g\\|_{2}\n$$\n\nfor all $f, g \\in L^{2}(\\mu)$ (again, without using Hölder's inequality).\n\n19 Suppose $V_{1}, \\ldots, V_{m}$ are inner product spaces. Show that the equation\n\n$$\n\\left\\langle\\left(f_{1}, \\ldots, f_{m}\\right),\\left(g_{1}, \\ldots, g_{m}\\right)\\right\\rangle=\\left\\langle f_{1}, g_{1}\\right\\rangle+\\cdots+\\left\\langle f_{m}, g_{m}\\right\\rangle\n$$\n\ndefines an inner product on $V_{1} \\times \\cdots \\times V_{m}$.\n\n[Each of the inner product spaces $V_{1}, \\ldots, V_{m}$ may have a different inner product, even though the same inner product notation is used on all these spaces.]\n\n20 Suppose $V$ is an inner product space. Make $V \\times V$ an inner product space as in the exercise above. Prove that the function that takes an ordered pair $(f, g) \\in V \\times V$ to the inner product $\\langle f, g\\rangle \\in \\mathbf{F}$ is a continuous function from $V \\times V$ to $\\mathbf{F}$.\n\n21 Suppose $1 \\leq p \\leq \\infty$.\n\n(a) Show the norm on $\\ell^{p}$ comes from an inner product if and only if $p=2$.\n\n(b) Show the norm on $L^{p}(\\mathbf{R})$ comes from an inner product if and only if $p=2$.\n\n22 Use inner products to prove Apollonius's identity: In a triangle with sides of length $a, b$, and $c$, let $d$ be the length of the line segment from the midpoint of the side of length $c$ to the opposite vertex. Then\n\n$$\na^{2}+b^{2}=\\frac{1}{2} c^{2}+2 d^{2} .\n$$\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-238.jpg?height=388&width=421&top_left_y=1685&top_left_x=760)\n\n## 8B Orthogonality\n\n## Orthogonal Projections\n\nThe previous section developed inner product spaces following a standard linear algebra approach. Linear algebra focuses mainly on finite-dimensional vector spaces. Many interesting results about infinite-dimensional inner product spaces require an additional hypothesis, which we now introduce.\n\n### 8.21 Definition Hilbert space\n\nA Hilbert space is an inner product space that is a Banach space with the norm determined by the inner product.\n\n### 8.22 Example Hilbert spaces\n\n- Suppose $\\mu$ is a measure. Then $L^{2}(\\mu)$ with its usual inner product is a Hilbert space (by 7.24).\n- As a special case of the first bullet point, if $n \\in \\mathbf{Z}^{+}$then taking $\\mu$ to be counting measure on $\\{1, \\ldots, n\\}$ shows that $\\mathbf{F}^{n}$ with its usual inner product is a Hilbert space.\n- As another special case of the first bullet point, taking $\\mu$ to be counting measure on $\\mathbf{Z}^{+}$shows that $\\ell^{2}$ with its usual inner product is a Hilbert space.\n- Every closed subspace of a Hilbert space is a Hilbert space [by 6.16(b)].\n\n### 8.23 Example not Hilbert spaces\n\n- The inner product space $\\ell^{1}$, where $\\left\\langle\\left(a_{1}, a_{2}, \\ldots\\right),\\left(b_{1}, b_{2}, \\ldots\\right)\\right\\rangle=\\sum_{k=1}^{\\infty} a_{k} \\overline{b_{k}}$, is not a Hilbert space because the associated norm is not complete on $\\ell$.\n- The inner product space $C([0,1])$ of continuous $\\mathbf{F}$-valued functions on the interval $[0,1]$, where $\\langle f, g\\rangle=\\int_{0}^{1} f \\bar{g}$, is not a Hilbert space because the associated norm is not complete on $C([0,1])$.\n\nThe next definition makes sense in the context of normed vector spaces.\n\n### 8.24 Definition distance from a point to a set\n\nSuppose $U$ is a nonempty subset of a normed vector space $V$ and $f \\in V$. The distance from $f$ to $U$, denoted distance $(f, U)$, is defined by\n\n$$\n\\text { distance }(f, U)=\\inf \\{\\|f-g\\|: g \\in U\\} \\text {. }\n$$\n\nNotice that distance $(f, U)=0$ if and only if $f \\in \\bar{U}$.\n\n### 8.25 Definition convex\n\n- A subset of a vector space is called convex if the subset contains the line segment connecting each pair of points in it.\n- More precisely, suppose $V$ is a vector space and $U \\subset V$. Then $U$ is called convex if\n\n$$\n(1-t) f+t g \\in U \\text { for all } t \\in[0,1] \\text { and all } f, g \\in U \\text {. }\n$$\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-240.jpg?height=220&width=419&top_left_y=589&top_left_x=153)\n\nConvex subset of $\\mathbf{R}^{2}$.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-240.jpg?height=220&width=416&top_left_y=589&top_left_x=724)\n\nNonconvex subset of $\\mathbf{R}^{2}$.\n\n### 8.26 Example convex sets\n\n- Every subspace of a vector space is convex, as you should verify.\n- If $V$ is a normed vector space, $f \\in V$, and $r>0$, then the open ball centered at $f$ with radius $r$ is convex, as you should verify.\n\nThe next example shows that the distance from an element of a Banach space to a closed subspace is not necessarily attained by some element of the closed subspace. After this example, we will prove that this behavior cannot happen in a Hilbert space.\n\n### 8.27 Example no closest element to a closed subspace of a Banach space\n\nIn the Banach space $C([0,1])$ with norm $\\|g\\|=\\sup |g|$, let\n\n$$\nU=\\left\\{g \\in C([0,1]): \\int_{0}^{1} g=0 \\text { and } g(1)=0\\right\\}\n$$\n\nThen $U$ is a closed subspace of $C([0,1])$.\n\nLet $f \\in C([0,1])$ be defined by $f(x)=1-x$. For $k \\in \\mathbf{Z}^{+}$, let\n\n$$\ng_{k}(x)=\\frac{1}{2}-x+\\frac{x^{k}}{2}+\\frac{x-1}{k+1}\n$$\n\nThen $g_{k} \\in U$ and $\\lim _{k \\rightarrow \\infty}\\left\\|f-g_{k}\\right\\|=\\frac{1}{2}$, which implies that distance $(f, U) \\leq \\frac{1}{2}$.\n\nIf $g \\in U$, then $\\int_{0}^{1}(f-g)=\\frac{1}{2}$ and $(f-g)(1)=0$. These conditions imply that $\\|f-g\\|>\\frac{1}{2}$.\n\nThus distance $(f, U)=\\frac{1}{2}$ but there does not exist $g \\in U$ such that $\\|f-g\\|=\\frac{1}{2}$.\n\nIn the next result, we use for the first time the hypothesis that $V$ is a Hilbert space.\n\n### 8.28 distance to a closed convex set is attained in a Hilbert space\n\n- The distance from an element of a Hilbert space to a nonempty closed convex set is attained by a unique element of the nonempty closed convex set.\n- More specifically, suppose $V$ is a Hilbert space, $f \\in V$, and $U$ is a nonempty closed convex subset of $V$. Then there exists a unique $g \\in U$ such that\n\n$$\n\\|f-g\\|=\\operatorname{distance}(f, U) \\text {. }\n$$\n\nProof First we prove the existence of an element of $U$ that attains the distance to $f$. To do this, suppose $g_{1}, g_{2}, \\ldots$ is a sequence of elements of $U$ such that\n\n8.29\n\n$$\n\\lim _{k \\rightarrow \\infty}\\left\\|f-g_{k}\\right\\|=\\operatorname{distance}(f, U)\n$$\n\nThen for $j, k \\in \\mathbf{Z}^{+}$we have\n\n$$\n\\begin{aligned}\n\\left\\|g_{j}-g_{k}\\right\\|^{2} & =\\left\\|\\left(f-g_{k}\\right)-\\left(f-g_{j}\\right)\\right\\|^{2} \\\\\n& =2\\left\\|f-g_{k}\\right\\|^{2}+2\\left\\|f-g_{j}\\right\\|^{2}-\\left\\|2 f-\\left(g_{k}+g_{j}\\right)\\right\\|^{2} \\\\\n& =2\\left\\|f-g_{k}\\right\\|^{2}+2\\left\\|f-g_{j}\\right\\|^{2}-4\\left\\|f-\\frac{g_{k}+g_{j}}{2}\\right\\|^{2}\n\\end{aligned}\n$$\n\n8.30\n\n$$\n\\leq 2\\left\\|f-g_{k}\\right\\|^{2}+2\\left\\|f-g_{j}\\right\\|^{2}-4(\\operatorname{distance}(f, U))^{2},\n$$\n\nwhere the second equality comes from the parallelogram equality (8.20) and the last line holds because the convexity of $U$ implies that $\\left(g_{k}+g_{j}\\right) / 2 \\in U$. Now the inequality above and 8.29 imply that $g_{1}, g_{2}, \\ldots$ is a Cauchy sequence. Thus there exists $g \\in V$ such that\n\n8.31\n\n$$\n\\lim _{k \\rightarrow \\infty}\\left\\|g_{k}-g\\right\\|=0\n$$\n\nBecause $U$ is a closed subset of $V$ and each $g_{k} \\in U$, we know that $g \\in U$. Now 8.29 and 8.31 imply that\n\n$$\n\\|f-g\\|=\\operatorname{distance}(f, U) \\text {, }\n$$\n\nwhich completes the existence proof of the existence part of this result.\n\nTo prove the uniqueness part of this result, suppose $g$ and $\\widetilde{g}$ are elements of $U$ such that\n\n$$\n\\|f-g\\|=\\|f-\\widetilde{g}\\|=\\operatorname{distance}(f, U) \\text {. }\n$$\n\nThen\n\n$$\n\\begin{aligned}\n\\|g-\\widetilde{g}\\|^{2} & \\leq 2\\|f-g\\|^{2}+2\\|f-\\widetilde{g}\\|^{2}-4(\\text { distance }(f, U))^{2} \\\\\n& =0,\n\\end{aligned}\n$$\n\nwhere the first line above follows from 8.30 (with $g_{j}$ replaced by $g$ and $g_{k}$ replaced by $\\widetilde{g}$ ) and the last line above follows from 8.32. Now 8.33 implies that $g=\\widetilde{g}$, completing the proof of uniqueness.\n\nExample 8.27 showed that the existence part of the previous result can fail in a Banach space. Exercise 13 shows that the uniqueness part can also fail in a Banach space. These observations highlight the advantages of working in a Hilbert space.\n\n### 8.34 Definition orthogonal projection; $P_{U}$\n\nSuppose $U$ is a nonempty closed convex subset of a Hilbert space $V$. The orthogonal projection of $V$ onto $U$ is the function $P_{U}: V \\rightarrow V$ defined by setting $P_{U}(f)$ equal to the unique element of $U$ that is closest to $f$.\n\nThe definition above makes sense because of 8.28 . We will often use the notation $P_{U} f$ instead of $P_{U}(f)$. To test your understanding of the definition above, make sure that you can show that if $U$ is a nonempty closed convex subset of a Hilbert space $V$, then\n\n- $P_{U} f=f$ if and only if $f \\in U$;\n- $P_{U} \\circ P_{U}=P_{U}$.\n\n### 8.35 Example orthogonal projection onto closed unit ball\n\nSuppose $U$ is the closed unit ball $\\{g \\in V:\\|g\\| \\leq 1\\}$ in a Hilbert space $V$. Then\n\n$$\nP_{U} f= \\begin{cases}f & \\text { if }\\|f\\| \\leq 1 \\\\ \\frac{f}{\\|f\\|} & \\text { if }\\|f\\|>1\\end{cases}\n$$\n\nas you should verify.\n\n### 8.36 Example orthogonal projection onto a closed subspace\n\nSuppose $U$ is the closed subspace of $\\ell^{2}$ consisting of the elements of $\\ell^{2}$ whose even coordinates are all 0 :\n\n$$\nU=\\left\\{\\left(a_{1}, 0, a_{3}, 0, a_{5}, 0, \\ldots\\right): \\text { each } a_{k} \\in \\mathbf{F} \\text { and } \\sum_{k=1}^{\\infty}\\left|a_{2 k-1}\\right|^{2}<\\infty\\right\\}\n$$\n\nThen for $b=\\left(b_{1}, b_{2}, b_{3}, b_{4}, b_{5}, b_{6}, \\ldots\\right) \\in \\ell^{2}$, we have\n\n$$\nP_{U} b=\\left(b_{1}, 0, b_{3}, 0, b_{5}, 0, \\ldots\\right),\n$$\n\nas you should verify.\n\nNote that in this example the function $P_{U}$ is a linear map from $\\ell^{2}$ to $\\ell^{2}$ (unlike the behavior in Example 8.35).\n\nAlso, notice that $b-P_{U} b=\\left(0, b_{2}, 0, b_{4}, 0, b_{6}, \\ldots\\right)$ and thus $b-P_{U} b$ is orthogonal to every element of $U$.\n\nThe next result shows that the properties stated in the last two paragraphs of the example above hold whenever $U$ is a closed subspace of a Hilbert space.\n\n### 8.37 orthogonal projection onto closed subspace\n\nSuppose $U$ is a closed subspace of a Hilbert space $V$ and $f \\in V$. Then\n\n(a) $f-P_{U} f$ is orthogonal to $g$ for every $g \\in U$;\n\n(b) if $h \\in U$ and $f-h$ is orthogonal to $g$ for every $g \\in U$, then $h=P_{U} f$;\n\n(c) $P_{U}: V \\rightarrow V$ is a linear map;\n\n(d) $\\left\\|P_{U} f\\right\\| \\leq\\|f\\|$, with equality if and only if $f \\in U$.\n\nProof The figure below illustrates (a). To prove (a), suppose $g \\in U$. Then for all $\\alpha \\in \\mathbf{F}$ we have\n\n$$\n\\begin{aligned}\n\\left\\|f-P_{U} f\\right\\|^{2} & \\leq\\left\\|f-P_{U} f+\\alpha g\\right\\|^{2} \\\\\n& =\\left\\langle f-P_{U} f+\\alpha g, f-P_{U} f+\\alpha g\\right\\rangle \\\\\n& =\\left\\|f-P_{U} f\\right\\|^{2}+|\\alpha|^{2}\\|g\\|^{2}+2 \\operatorname{Re} \\bar{\\alpha}\\left\\langle f-P_{U} f, g\\right\\rangle .\n\\end{aligned}\n$$\n\nLet $\\alpha=-t\\left\\langle f-P_{U} f, g\\right\\rangle$ for $t>0$. A tiny bit of algebra applied to the inequality above implies\n\n$$\n2\\left|\\left\\langle f-P_{U} f, g\\right\\rangle\\right|^{2} \\leq t\\left|\\left\\langle f-P_{U} f, g\\right\\rangle\\right|^{2}\\|g\\|^{2}\n$$\n\nfor all $t>0$. Thus $\\left\\langle f-P_{U} f, g\\right\\rangle=0$, completing the proof of (a).\n\nTo prove (b), suppose $h \\in U$ and $f-h$ is orthogonal to $g$ for every $g \\in U$. If $g \\in U$, then $h-g \\in U$ and hence $f-h$ is orthogonal to $h-g$. Thus\n\n$$\n\\begin{aligned}\n\\|f-h\\|^{2} & \\leq\\|f-h\\|^{2}+\\|h-g\\|^{2} \\\\\n& =\\|(f-h)+(h-g)\\|^{2} \\\\\n& =\\|f-g\\|^{2},\n\\end{aligned}\n$$\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-243.jpg?height=250&width=574&top_left_y=1167&top_left_x=619)\n\n$f-P_{U} f$ is orthogonal to each element of $U$.\n\nwhere the first equality above follows from the Pythagorean Theorem (8.9). Thus\n\n$$\n\\|f-h\\| \\leq\\|f-g\\|\n$$\n\nfor all $g \\in U$. Hence $h$ is the element of $U$ that minimizes the distance to $f$, which implies that $h=P_{U} f$, completing the proof of (b).\n\nTo prove (c), suppose $f_{1}, f_{2} \\in V$. If $g \\in U$, then (a) implies that $\\left\\langle f_{1}-P_{U} f_{1}, g\\right\\rangle=$ $\\left\\langle f_{2}-P_{U} f_{2}, g\\right\\rangle=0$, and thus\n\n$$\n\\left\\langle\\left(f_{1}+f_{2}\\right)-\\left(P_{U} f_{1}+P_{U} f_{2}\\right), g\\right\\rangle=0 .\n$$\n\nThe equation above and (b) now imply that\n\n$$\nP_{U}\\left(f_{1}+f_{2}\\right)=P_{U} f_{1}+P_{U} f_{2} .\n$$\n\nThe equation above and the equation $P_{U}(\\alpha f)=\\alpha P_{U} f$ for $\\alpha \\in \\mathbf{F}$ (whose proof is left to the reader) show that $P_{U}$ is a linear map, proving (c).\n\nThe proof of (d) is left as an exercise for the reader.\n\n## Orthogonal Complements\n\n### 8.38 Definition orthogonal complement; $U^{\\perp}$\n\nSuppose $U$ is a subset of an inner product space $V$. The orthogonal complement of $U$ is denoted by $U^{\\perp}$ and is defined by\n\n$$\nU^{\\perp}=\\{h \\in V:\\langle g, h\\rangle=0 \\text { for all } g \\in U\\}\n$$\n\nIn other words, the orthogonal complement of a subset $U$ of an inner product space $V$ is the set of elements of $V$ that are orthogonal to every element of $U$.\n\n### 8.39 Example orthogonal complement\n\nSuppose $U$ is the set of elements of $\\ell^{2}$ whose even coordinates are all 0 :\n\n$$\nU=\\left\\{\\left(a_{1}, 0, a_{3}, 0, a_{5}, 0, \\ldots\\right): \\text { each } a_{k} \\in \\mathbf{F} \\text { and } \\sum_{k=1}^{\\infty}\\left|a_{2 k-1}\\right|^{2}<\\infty\\right\\}\n$$\n\nThen $U^{\\perp}$ is the set of elements of $\\ell^{2}$ whose odd coordinates are all 0 :\n\nas you should verify.\n\n$$\n\\left.U^{\\perp}=\\left\\{0, a_{2}, 0, a_{4}, 0, a_{6}, \\ldots\\right): \\text { each } a_{k} \\in \\mathbf{F} \\text { and } \\sum_{k=1}^{\\infty}\\left|a_{2 k}\\right|^{2}<\\infty\\right\\}\n$$\n\n### 8.40 properties of orthogonal complement\n\nSuppose $U$ is a subset of an inner product space $V$. Then\n\n(a) $U^{\\perp}$ is a closed subspace of $V$;\n\n(b) $U \\cap U^{\\perp} \\subset\\{0\\}$;\n\n(c) if $W \\subset U$, then $U^{\\perp} \\subset W^{\\perp}$;\n\n(d) $\\bar{U}^{\\perp}=U^{\\perp}$;\n\n(e) $U \\subset\\left(U^{\\perp}\\right)^{\\perp}$.\n\nProof To prove (a), suppose $h_{1}, h_{2}, \\ldots$ is a sequence in $U^{\\perp}$ that converges to some $h \\in V$. If $g \\in U$, then\n\n$$\n|\\langle g, h\\rangle|=\\left|\\left\\langle g, h-h_{k}\\right\\rangle\\right| \\leq\\|g\\|\\left\\|h-h_{k}\\right\\| \\quad \\text { for each } k \\in \\mathbf{Z}^{+} \\text {; }\n$$\n\nhence $\\langle g, h\\rangle=0$, which implies that $h \\in U^{\\perp}$. Thus $U^{\\perp}$ is closed. The proof of (a) is completed by showing that $U^{\\perp}$ is a subspace of $V$, which is left to the reader.\n\nTo prove (b), suppose $g \\in U \\cap U^{\\perp}$. Then $\\langle g, g\\rangle=0$, which implies that $g=0$, proving (b).\n\nTo prove (e), suppose $g \\in U$. Thus $\\langle g, h\\rangle=0$ for all $h \\in U^{\\perp}$, which implies that $g \\in\\left(U^{\\perp}\\right)^{\\perp}$. Hence $U \\subset\\left(U^{\\perp}\\right)^{\\perp}$, proving (e).\n\nThe proofs of (c) and (d) are left to the reader.\n\nThe results in the rest of this subsection have as a hypothesis that $V$ is a Hilbert space. These results do not hold when $V$ is only an inner product space.\n\n### 8.41 orthogonal complement of the orthogonal complement\n\nSuppose $U$ is a subspace of a Hilbert space $V$. Then\n\n$$\n\\bar{U}=\\left(U^{\\perp}\\right)^{\\perp} .\n$$\n\nProof Applying 8.40(a) to $U^{\\perp}$, we see that $\\left(U^{\\perp}\\right)^{\\perp}$ is a closed subspace of $V$. Now taking closures of both sides of the inclusion $U \\subset\\left(U^{\\perp}\\right)^{\\perp}$ [8.40(e)] shows that $\\bar{U} \\subset\\left(U^{\\perp}\\right)^{\\perp}$.\n\nTo prove the inclusion in the other direction, suppose $f \\in\\left(U^{\\perp}\\right)^{\\perp}$. Because $f \\in\\left(U^{\\perp}\\right)^{\\perp}$ and $P_{\\bar{U}} f \\in \\bar{U} \\subset\\left(U^{\\perp}\\right)^{\\perp}$ (by the previous paragraph), we see that\n\n$$\nf-P_{\\bar{U}} f \\in\\left(U^{\\perp}\\right)^{\\perp}\n$$\n\nAlso,\n\n$$\nf-P_{\\bar{U}} f \\in U^{\\perp}\n$$\n\nby $8.37(a)$ and $8.40(d)$. Hence\n\n$$\nf-P_{\\bar{U}} f \\in U^{\\perp} \\cap\\left(U^{\\perp}\\right)^{\\perp} .\n$$\n\nNow 8.40(b) (applied to $U^{\\perp}$ in place of $U$ ) implies that $f-P_{\\bar{U}} f=0$, which implies that $f \\in \\bar{U}$. Thus $\\left(U^{\\perp}\\right)^{\\perp} \\subset \\bar{U}$, completing the proof.\n\nAs a special case, the result above implies that if $U$ is a closed subspace of a Hilbert space $V$, then $U=\\left(U^{\\perp}\\right)^{\\perp}$.\n\nAnother special case of the result above is sufficiently useful to deserve stating separately, as we do in the next result.\n\n### 8.42 necessary and sufficient condition for a subspace to be dense\n\nSuppose $U$ is a subspace of a Hilbert space $V$. Then\n\n$$\n\\bar{U}=V \\text { if and only if } U^{\\perp}=\\{0\\}\n$$\n\nProof First suppose $\\bar{U}=V$. Then using $8.40(\\mathrm{~d})$, we have\n\n$$\nU^{\\perp}=\\bar{U}^{\\perp}=V^{\\perp}=\\{0\\} .\n$$\n\nTo prove the other direction, now suppose $U^{\\perp}=\\{0\\}$. Then 8.41 implies that\n\n$$\n\\bar{U}=\\left(U^{\\perp}\\right)^{\\perp}=\\{0\\}^{\\perp}=V\n$$\n\ncompleting the proof.\n\nThe next result states that if $U$ is a closed subspace of a Hilbert space $V$, then $V$ is the direct sum of $U$ and $U^{\\perp}$, often written $V=U \\oplus U^{\\perp}$, although we do not need to use this terminology or notation further.\n\nThe key point to keep in mind is that the next result shows that the picture here represents what happens in general for a closed subspace $U$ of a Hilbert space $V$ : every element of $V$ can be uniquely written as an element of $U$ plus an element of $U^{\\perp}$.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-246.jpg?height=527&width=518&top_left_y=155&top_left_x=688)\n\n### 8.43 orthogonal decomposition\n\nSuppose $U$ is a closed subspace of a Hilbert space $V$. Then every element $f \\in V$ can be uniquely written in the form\n\n$$\nf=g+h,\n$$\n\nwhere $g \\in U$ and $h \\in U^{\\perp}$. Furthermore, $g=P_{U} f$ and $h=f-P_{U} f$.\n\nProof Suppose $f \\in V$. Then\n\n$$\nf=P_{U} f+\\left(f-P_{U} f\\right)\n$$\n\nwhere $P_{U} f \\in U$ [by definition of $P_{U} f$ as the element of $U$ that is closest to $f$ ] and $f-P_{U} f \\in U^{\\perp}$ [by 8.37(a)]. Thus we have the desired decomposition of $f$ as the sum of an element of $U$ and an element of $U^{\\perp}$.\n\nTo prove the uniqueness of this decomposition, suppose\n\n$$\nf=g_{1}+h_{1}=g_{2}+h_{2}\n$$\n\nwhere $g_{1}, g_{2} \\in U$ and $h_{1}, h_{2} \\in U^{\\perp}$. Then $g_{1}-g_{2}=h_{2}-h_{1} \\in U \\cap U^{\\perp}$, which implies that $g_{1}=g_{2}$ and $h_{1}=h_{2}$, as desired.\n\nIn the next definition, the function $I$ depends upon the vector space $V$. Thus a notation such as $I_{V}$ might be more precise. However, the domain of $I$ should always be clear from the context.\n\n### 8.44 Definition identity map; I\n\nSuppose $V$ is a vector space. The identity map $I$ is the linear map from $V$ to $V$ defined by I $f=f$ for $f \\in V$.\n\nThe next result highlights the close relationship between orthogonal projections and orthogonal complements.\n\n### 8.45 range and null space of orthogonal projections\n\nSuppose $U$ is a closed subspace of a Hilbert space $V$. Then\n\n(a) range $P_{U}=U$ and null $P_{U}=U^{\\perp}$;\n\n(b) range $P_{U^{\\perp}}=U^{\\perp}$ and null $P_{U^{\\perp}}=U$;\n\n(c) $P_{U^{\\perp}}=I-P_{U}$.\n\nProof The definition of $P_{U} f$ as the closest point in $U$ to $f$ implies range $P_{U} \\subset U$. Because $P_{U} g=g$ for all $g \\in U$, we also have $U \\subset$ range $P_{U}$. Thus range $P_{U}=U$.\n\nIf $f \\in$ null $P_{U}$, then $f \\in U^{\\perp}$ [by 8.37(a)]. Thus null $P_{U} \\subset U^{\\perp}$. Conversely, if $f \\in U^{\\perp}$, then 8.37(b) (with $h=0$ ) implies that $P_{U} f=0$; hence $U^{\\perp} \\subset$ null $P_{U}$. Thus null $P_{U}=U^{\\perp}$, completing the proof of (a).\n\nReplace $U$ by $U^{\\perp}$ in (a), getting range $P_{U^{\\perp}}=U^{\\perp}$ and null $P_{U^{\\perp}}=\\left(U^{\\perp}\\right)^{\\perp}=U$ (where the last equality comes from 8.41), completing the proof of (b).\n\nFinally, if $f \\in U$, then\n\n$$\nP_{U^{\\perp}} f=0=f-P_{U} f=\\left(I-P_{U}\\right) f\n$$\n\nwhere the first equality above holds because null $P_{U^{\\perp}}=U$ [by (b)].\n\nIf $f \\in U^{\\perp}$, then\n\n$$\nP_{U \\perp} f=f=f-P_{U} f=\\left(I-P_{U}\\right) f,\n$$\n\nwhere the second equality above holds because null $P_{U}=U^{\\perp}$ [by (a)].\n\nThe last two displayed equations show that $P_{U^{\\perp}}$ and $I-P_{U}$ agree on $U$ and agree on $U^{\\perp}$. Because $P_{U^{\\perp}}$ and $I-P_{U}$ are both linear maps and because each element of $V$ equals some element of $U$ plus some element of $U^{\\perp}$ (by 8.43), this implies that $P_{U^{\\perp}}=I-P_{U}$, completing the proof of (c).\n\n### 8.46 Example $P_{U^{\\perp}}=I-P_{U}$\n\nSuppose $U$ is the closed subspace of $L^{2}(\\mathbf{R})$ defined by\n\n$$\nU=\\left\\{f \\in L^{2}(\\mathbf{R}): f(x)=0 \\text { for almost every } x<0\\right\\} \\text {. }\n$$\n\nThen, as you should verify,\n\n$$\nU^{\\perp}=\\left\\{g \\in L^{2}(\\mathbf{R}): g(x)=0 \\text { for almost every } x \\geq 0\\right\\} \\text {. }\n$$\n\nFurthermore, you should also verify that if $h \\in L^{2}(\\mathbf{R})$, then\n\n$$\nP_{U} h=h \\chi_{[0, \\infty)} \\quad \\text { and } \\quad P_{U^{\\perp}} h=h \\chi_{(-\\infty, 0)} .\n$$\n\nThus $P_{U^{\\perp}} h=h\\left(1-\\chi_{[0, \\infty)}\\right)=\\left(I-P_{U}\\right) h$ and hence $P_{U^{\\perp}}=I-P_{U}$, as asserted in 8.45(c).\n\n## Riesz Representation Theorem\n\nSuppose $h$ is an element of a Hilbert space $V$. Define $\\varphi: V \\rightarrow \\mathbf{F}$ by $\\varphi(f)=\\langle f, h\\rangle$ for $f \\in V$. The properties of an inner product imply that $\\varphi$ is a linear functional. The Cauchy-Schwarz inequality (8.11) implies that $|\\varphi(f)| \\leq\\|f\\|\\|h\\|$ for all $f \\in V$, which implies that $\\varphi$ is a bounded linear functional on $V$. The next result states that every bounded linear functional on $V$ arises in this fashion.\n\nTo motivate the proof of the next result, note that if $\\varphi$ is as in the paragraph above, then null $\\varphi=\\{h\\}^{\\perp}$. Thus $h \\in(\\text { null } \\varphi)^{\\perp}$ [by $\\left.8.40(\\mathrm{e})\\right]$. Hence in the proof of the next result, to find $h$ we start with an element of $(\\text { null } \\varphi)^{\\perp}$ and then multiply it by a scalar to make everything come out right.\n\n### 8.47 Riesz Representation Theorem\n\nSuppose $\\varphi$ is a bounded linear functional on a Hilbert space $V$. Then there exists a unique $h \\in V$ such that\n\n$$\n\\varphi(f)=\\langle f, h\\rangle\n$$\n\nfor all $f \\in V$. Furthermore, $\\|\\varphi\\|=\\|h\\|$.\n\nProof If $\\varphi=0$, take $h=0$. Thus we can assume $\\varphi \\neq 0$. Hence null $\\varphi$ is a closed subspace of $V$ not equal to $V$ (see 6.52). The subspace (null $\\varphi)^{\\perp}$ is not $\\{0\\}$ (by 8.42). Thus there exists $g \\in(\\text { null } \\varphi)^{\\perp}$ with $\\|g\\|=1$. Let\n\n$$\nh=\\overline{\\varphi(g)} g\n$$\n\nTaking the norm of both sides of the equation above, we get $\\|h\\|=|\\varphi(g)|$. Thus\n\n$$\n\\varphi(h)=|\\varphi(g)|^{2}=\\|h\\|^{2} .\n$$\n\nNow suppose $f \\in V$. Then\n\n$$\n\\begin{aligned}\n\\langle f, h\\rangle & =\\left\\langle f-\\frac{\\varphi(f)}{\\|h\\|^{2}} h, h\\right\\rangle+\\left\\langle\\frac{\\varphi(f)}{\\|h\\|^{2}} h, h\\right\\rangle \\\\\n& =\\left\\langle\\frac{\\varphi(f)}{\\|h\\|^{2}} h, h\\right\\rangle \\\\\n& =\\varphi(f),\n\\end{aligned}\n$$\n\nwhere 8.49 holds because $f-\\frac{\\varphi(f)}{\\|h\\|^{2}} h \\in$ null $\\varphi$ (by 8.48) and $h$ is orthogonal to all elements of null $\\varphi$.\n\nWe have now proved the existence of $h \\in V$ such that $\\varphi(f)=\\langle f, h\\rangle$ for all $f \\in V$. To prove uniqueness, suppose $\\widetilde{h} \\in V$ has the same property. Then\n\n$$\n\\langle h-\\widetilde{h}, h-\\widetilde{h}\\rangle=\\langle h-\\widetilde{h}, h\\rangle-\\langle h-\\widetilde{h}, \\widetilde{h}\\rangle=\\varphi(h-\\widetilde{h})-\\varphi(h-\\widetilde{h})=0,\n$$\n\nwhich implies that $h=\\widetilde{h}$, which proves uniqueness.\n\nThe Cauchy-Schwarz inequality implies that $|\\varphi(f)|=|\\langle f, h\\rangle| \\leq\\|f\\|\\|h\\|$ for all $f \\in V$, which implies that $\\|\\varphi\\| \\leq\\|h\\|$. Because $\\varphi(h)=\\langle h, h\\rangle=\\|h\\|^{2}$, we also have $\\|\\varphi\\| \\geq\\|h\\|$. Thus $\\|\\varphi\\|=\\|h\\|$, completing the proof.\n\nSuppose that $\\mu$ is a measure and $1<p \\leq \\infty$. In 7.25 we considered the natural map of $L^{p^{\\prime}}(\\mu)$ into $\\left(L^{p}(\\mu)\\right)^{\\prime}$, and\n\nFrigyes Riesz (1880-1956) proved 8.47 in 1907. we showed that this maps preserves norms. In the special case where $p=p^{\\prime}=2$, the Riesz Representation Theorem (8.47) shows that this map is surjective. In other words, if $\\varphi$ is a bounded linear functional on $L^{2}(\\mu)$, then there exists $h \\in L^{2}(\\mu)$ such that\n\n$$\n\\varphi(f)=\\int f h d \\mu\n$$\n\nfor all $f \\in L^{2}(\\mu)$ (take $h$ to be the complex conjugate of the function given by 8.47). Hence we can identify the dual of $L^{2}(\\mu)$ with $L^{2}(\\mu)$. In 9.42 we will deal with other values of $p$. Also see Exercise 25 in this section.\n\n## EXERCISES 8B\n\n1 Show that each of the inner product spaces in Example 8.23 is not a Hilbert space.\n\n2 Prove or disprove: The inner product space in Exercise 1 in Section 8A is a Hilbert space.\n\n3 Suppose $V_{1}, V_{2}, \\ldots$ are Hilbert spaces. Let\n\n$$\nV=\\left\\{\\left(f_{1}, f_{2}, \\ldots\\right) \\in V_{1} \\times V_{2} \\times \\cdots: \\sum_{k=1}^{\\infty}\\left\\|f_{k}\\right\\|^{2}<\\infty\\right\\}\n$$\n\nShow that the equation\n\n$$\n\\left\\langle\\left(f_{1}, f_{2}, \\ldots\\right),\\left(g_{1}, g_{2}, \\ldots\\right)\\right\\rangle=\\sum_{k=1}^{\\infty}\\left\\langle f_{k}, g_{k}\\right\\rangle\n$$\n\ndefines an inner product on $V$ that makes $V$ a Hilbert space. [Each of the Hilbert spaces $V_{1}, V_{2}, \\ldots$ may have a different inner product, even though the same notation is used for the norm and inner product on all these Hilbert spaces.]\n\n4 Suppose $V$ is a real Hilbert space. The complexification of $V$ is the complex vector space $V_{\\mathrm{C}}$ defined by $V_{\\mathrm{C}}=V \\times V$, but we write a typical element of $V_{\\mathrm{C}}$ as $f+i g$ instead of $(f, g)$. Addition and scalar multiplication are defined on $V_{\\mathrm{C}}$ by\n\n$$\n\\left(f_{1}+i g_{1}\\right)+\\left(f_{2}+i g_{2}\\right)=\\left(f_{1}+f_{2}\\right)+i\\left(g_{1}+g_{2}\\right)\n$$\n\nand\n\n$$\n(\\alpha+i \\beta)(f+i g)=(\\alpha f-\\beta g)+i(\\alpha g+\\beta f)\n$$\n\nfor $f_{1}, f_{2}, f, g_{1}, g_{2}, g \\in V$ and $\\alpha, \\beta \\in \\mathbf{R}$. Show that\n\n$$\n\\left\\langle f_{1}+i g_{1}, f_{2}+i g_{2}\\right\\rangle=\\left\\langle f_{1}, f_{2}\\right\\rangle+\\left\\langle g_{1}, g_{2}\\right\\rangle+i\\left(\\left\\langle g_{1}, f_{2}\\right\\rangle-\\left\\langle f_{1}, g_{2}\\right\\rangle\\right)\n$$\n\ndefines an inner product on $V_{\\mathbf{C}}$ that makes $V_{\\mathbf{C}}$ into a complex Hilbert space.\n\n5 Prove that if $V$ is a normed vector space, $f \\in V$, and $r>0$, then the open ball $B(f, r)$ centered at $f$ with radius $r$ is convex.\n\n6 (a) Suppose $V$ is an inner product space and $B$ is the open unit ball in $V$ (thus $B=\\{f \\in V:\\|f\\|<1\\}$ ). Prove that if $U$ is a subset of $V$ such that $B \\subset U \\subset \\bar{B}$, then $U$ is convex.\n\n(b) Give an example to show that the result in part (a) can fail if the phrase inner product space is replaced by Banach space.\n\n7 Suppose $V$ is a normed vector space and $U$ is a closed subset of $V$. Prove that $U$ is convex if and only if\n\n$$\n\\frac{f+g}{2} \\in U \\text { for all } f, g \\in U\n$$\n\n8 Prove that if $U$ is a convex subset of a normed vector space, then $\\bar{U}$ is also convex.\n\n9 Prove that if $U$ is a convex subset of a normed vector space, then the interior of $U$ is also convex.\n\n[The interior of $U$ is the set $\\{f \\in U: B(f, r) \\subset U$ for some $r>0\\}$.]\n\n10 Suppose $V$ is a Hilbert space, $U$ is a nonempty closed convex subset of $V$, and $g \\in U$ is the unique element of $U$ with smallest norm (obtained by taking $f=0$ in 8.28). Prove that\n\n$$\n\\operatorname{Re}\\langle g, h\\rangle \\geq\\|g\\|^{2}\n$$\n\nfor all $h \\in U$.\n\n11 Suppose $V$ is a Hilbert space. A closed half-space of $V$ is a set of the form\n\n$$\n\\{g \\in V: \\operatorname{Re}\\langle g, h\\rangle \\geq c\\}\n$$\n\nfor some $h \\in V$ and some $c \\in \\mathbf{R}$. Prove that every closed convex subset of $V$ is the intersection of all the closed half-spaces that contain it.\n\n12 Give an example of a nonempty closed subset $U$ of the Hilbert space $\\ell^{2}$ and $a \\in \\ell^{2}$ such that there does not exist $b \\in U$ with $\\|a-b\\|=$ distance $(a, U)$. [By 8.28, $U$ cannot be a convex subset of $\\ell^{2}$.]\n\n13 In the real Banach space $\\mathbf{R}^{2}$ with norm defined by $\\|(x, y)\\|_{\\infty}=\\max \\{|x|,|y|\\}$, give an example of a closed convex set $U \\subset \\mathbf{R}^{2}$ and $z \\in \\mathbf{R}^{2}$ such that there exist infinitely many choices of $w \\in U$ with $\\|z-w\\|_{\\infty}=\\operatorname{distance}(z, U)$.\n\n14 Suppose $f$ and $g$ are elements of an inner product space. Prove that $\\langle f, g\\rangle=0$ if and only if\n\n$$\n\\|f\\| \\leq\\|f+\\alpha g\\|\n$$\n\nfor all $\\alpha \\in \\mathbf{F}$.\n\n15 Suppose $U$ is a closed subspace of a Hilbert space $V$ and $f \\in V$. Prove that $\\left\\|P_{U} f\\right\\| \\leq\\|f\\|$, with equality if and only if $f \\in U$.\n\n[This exercise asks you to prove $8.37(d)$.]\n\n16 Suppose $V$ is a Hilbert space and $P: V \\rightarrow V$ is a linear map such that $P^{2}=P$ and $\\|P f\\| \\leq\\|f\\|$ for every $f \\in V$. Prove that there exists a closed subspace $U$ of $V$ such that $P=P_{U}$.\n\n17 Suppose $U$ is a subspace of a Hilbert space $V$. Suppose also that $W$ is a Banach space and $S: U \\rightarrow W$ is a bounded linear map. Prove that there exists a bounded linear map $T: V \\rightarrow W$ such that $\\left.T\\right|_{U}=S$ and $\\|T\\|=\\|S\\|$.\n\n[If $W=\\mathbf{F}$, then this result is just the Hahn-Banach Theorem (6.69) for Hilbert spaces. The result here is stronger because it allows $W$ to be an arbitrary Banach space instead of requiring $W$ to be $\\mathbf{F}$. Also, the proof in this Hilbert space context does not require use of Zorn's Lemma or the Axiom of Choice.]\n\n18 Suppose $U$ and $W$ are subspaces of a Hilbert space $V$. Prove that $\\bar{U}=\\bar{W}$ if and only if $U^{\\perp}=W^{\\perp}$.\n\n19 Suppose $U$ and $W$ are closed subspaces of a Hilbert space. Prove that $P_{U} P_{W}=0$ if and only if $\\langle f, g\\rangle=0$ for all $f \\in U$ and all $g \\in W$.\n\n20 Verify the assertions in Example 8.46.\n\n21 Show that every inner product space is a subspace of some Hilbert space.\n\nHint: See Exercise 13 in Section 6C.\n\n22 Prove that if $V$ is a Hilbert space and $T: V \\rightarrow V$ is a bounded linear map such that the dimension of range $T$ is 1 , then there exist $g, h \\in V$ such that\n\n$$\nT f=\\langle f, g\\rangle h\n$$\n\nfor all $f \\in V$.\n\n23 (a) Give an example of a Banach space $V$ and a bounded linear functional $\\varphi$ on $V$ such that $|\\varphi(f)|<\\|\\varphi\\|\\|f\\|$ for all $f \\in V \\backslash\\{0\\}$.\n\n(b) Show there does not exist an example in part (a) where $V$ is a Hilbert space.\n\n24 (a) Suppose $\\varphi$ and $\\psi$ are bounded linear functionals on a Hilbert space $V$ such that $\\|\\varphi+\\psi\\|=\\|\\varphi\\|+\\|\\psi\\|$. Prove that one of $\\varphi, \\psi$ is a scalar multiple of the other.\n\n(b) Give an example to show that part (a) can fail if the hypothesis that $V$ is a Hilbert space is replaced by the hypothesis that $V$ is a Banach space.\n\n25 (a) Suppose that $\\mu$ is a finite measure, $1 \\leq p \\leq 2$, and $\\varphi$ is a bounded linear functional on $L^{p}(\\mu)$. Prove that there exists $h \\in L^{p^{\\prime}}(\\mu)$ such that $\\varphi(f)=\\int f h d \\mu$ for every $f \\in L^{p}(\\mu)$.\n\n(b) Same as part (a), but with the hypothesis that $\\mu$ is a finite measure replaced by the hypothesis that $\\mu$ is a measure, and assume that $1<p \\leq 2$.\n\n[See 7.25, which along with this exercise shows that we can identify the dual of $L^{p}(\\mu)$ with $L^{p^{\\prime}}(\\mu)$ for $1<p \\leq 2$. See 9.42 for an extension to all $p \\in(1, \\infty)$.]\n\n26 Prove that if $V$ is an infinite-dimensional Hilbert space, then the Banach space $\\mathcal{B}(V, V)$ is nonseparable.\n\n## $8 \\mathrm{C}$ Orthonormal Bases\n\n## Bessel's Inequality\n\nRecall that a family $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ in a set $V$ is a function $e$ from a set $\\Gamma$ to $V$, with the value of the function $e$ at $k \\in \\Gamma$ denoted by $e_{k}$ (see 6.53).\n\n### 8.50 Definition orthonormal family\n\nA family $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ in an inner product space is called an orthonormal family if\n\n$$\n\\left\\langle e_{j}, e_{k}\\right\\rangle= \\begin{cases}0 & \\text { if } j \\neq k \\\\ 1 & \\text { if } j=k\\end{cases}\n$$\n\nfor all $j, k \\in \\Gamma$.\n\nIn other words, a family $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family if $e_{j}$ and $e_{k}$ are orthogonal for all distinct $j, k \\in \\Gamma$ and $\\left\\|e_{k}\\right\\|=1$ for all $k \\in \\Gamma$.\n\n### 8.51 Example orthonormal families\n\n- For $k \\in \\mathbf{Z}^{+}$, let $e_{k}$ be the element of $\\ell^{2}$ all of whose coordinates are 0 except for the $k^{\\text {th }}$ coordinate, which is 1 :\n\n$$\ne_{k}=(0, \\ldots, 0,1,0, \\ldots)\n$$\n\nThen $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}$is an orthonormal family in $\\ell^{2}$. In this case, our family is a sequence; thus we can call $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}$an orthonormal sequence.\n\n- More generally, suppose $\\Gamma$ is a nonempty set. The Hilbert space $L^{2}(\\mu)$, where $\\mu$ is counting measure on $\\Gamma$, is often denoted by $\\ell^{2}(\\Gamma)$. For $k \\in \\Gamma$, define a function $e_{k}: \\Gamma \\rightarrow \\mathbf{F}$ by\n\n$$\ne_{k}(j)= \\begin{cases}1 & \\text { if } j=k \\\\ 0 & \\text { if } j \\neq k\\end{cases}\n$$\n\nThen $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family in $\\ell^{2}(\\Gamma)$.\n\n- For $k \\in \\mathbf{Z}$, define $e_{k}:(-\\pi, \\pi] \\rightarrow \\mathbf{R}$ by\n\n$$\ne_{k}(t)= \\begin{cases}\\frac{1}{\\sqrt{\\pi}} \\sin (k t) & \\text { if } k>0 \\\\ \\frac{1}{\\sqrt{2 \\pi}} & \\text { if } k=0 \\\\ \\frac{1}{\\sqrt{\\pi}} \\cos (k t) & \\text { if } k<0\\end{cases}\n$$\n\nThen $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}}$ is an orthonormal family in $L^{2}((-\\pi, \\pi])$, as you should verify (see Exercise 1 for useful formulas that will help with this verification).\n\nThis orthonormal family $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}}$ leads to the classical theory of Fourier series, as we will see in more depth in Chapter 11.\n\n- For $k$ a nonnegative integer, define $e_{k}:[0,1) \\rightarrow \\mathbf{F}$ by\n\n$$\ne_{k}(x)= \\begin{cases}1 & \\text { if } x \\in\\left[\\frac{n-1}{2^{k}}, \\frac{n}{2^{k}}\\right) \\text { for some odd integer } n \\\\ -1 & \\text { if } x \\in\\left[\\frac{n-1}{2^{k}}, \\frac{n}{2^{k}}\\right) \\text { for some even integer } n\\end{cases}\n$$\n\nThe figure below shows the graphs of $e_{0}, e_{1}, e_{2}$, and $e_{3}$. The pattern of these graphs should convince you that $\\left\\{e_{k}\\right\\}_{k \\in\\{0,1, \\ldots\\}}$ is an orthonormal fam-\n\nThis orthonormal family was invented by Hans Rademacher (1892-1969). ily in $L^{2}([0,1))$.\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-253.jpg?height=228&width=1158&top_left_y=662&top_left_x=68)\n\nThe graph of $e_{0} . \\quad$ The graph of $e_{1}$\n\nThe graph of $e_{2}$\n\nThe graph of $e_{3}$.\n\n- Now we modify the example in the previous bullet point by translating the functions in the previous bullet point by arbitrary integers. Specifically, for $k$ a nonnegative integer and $m \\in \\mathbf{Z}$, define $e_{k, m}: \\mathbf{R} \\rightarrow \\mathbf{F}$ by\n\n$e_{k, m}(x)= \\begin{cases}1 & \\text { if } x \\in\\left[m+\\frac{n-1}{2^{k}}, m+\\frac{n}{2^{k}}\\right) \\text { for some odd integer } n \\in\\left[1,2^{k}\\right], \\\\ -1 & \\text { if } x \\in\\left[m+\\frac{n-1}{2^{k}}, m+\\frac{n}{2^{k}}\\right) \\text { for some even integer } n \\in\\left[1,2^{k}\\right], \\\\ 0 & \\text { if } x \\notin[m, m+1) .\\end{cases}$\n\nThen $\\left\\{e_{k, m}\\right\\}_{(k, m) \\in\\{0,1, \\ldots\\} \\times \\mathbf{Z}}$ is an orthonormal family in $L^{2}(\\mathbf{R})$.\n\nThis example illustrates the usefulness of considering families that are not sequences. Although $\\{0,1, \\ldots\\} \\times \\mathbf{Z}$ is a countable set and hence we could rewrite $\\left\\{e_{k, m}\\right\\}_{(k, m) \\in\\{0,1, \\ldots\\}} \\times \\mathbf{Z}$ as a sequence, doing so would be awkward and would be less clean than the $e_{k, m}$ notation.\n\nThe next result gives our first indication of why orthonormal families are so useful.\n\n### 8.52 finite orthonormal families\n\nSuppose $\\Omega$ is a finite set and $\\left\\{e_{j}\\right\\}_{j \\in \\Omega}$ is an orthonormal family in an inner product space. Then\n\n$$\n\\left\\|\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}\\right\\|^{2}=\\sum_{j \\in \\Omega}\\left|\\alpha_{j}\\right|^{2}\n$$\n\nfor every family $\\left\\{\\alpha_{j}\\right\\}_{j \\in \\Omega}$ in $\\mathbf{F}$.\n\nProof Suppose $\\left\\{\\alpha_{j}\\right\\}_{j \\in \\Omega}$ is a family in F. Standard properties of inner products show that\n\n$$\n\\begin{aligned}\n\\left\\|\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}\\right\\|^{2} & =\\left\\langle\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}, \\sum_{k \\in \\Omega} \\alpha_{k} e_{k}\\right\\rangle \\\\\n& =\\sum_{j, k \\in \\Omega} \\alpha_{j} \\overline{\\alpha_{k}}\\left\\langle e_{j}, e_{k}\\right\\rangle \\\\\n& =\\sum_{j \\in \\Omega}\\left|\\alpha_{j}\\right|^{2},\n\\end{aligned}\n$$\n\nas desired.\n\nSuppose $\\Omega$ is a finite set and $\\left\\{e_{j}\\right\\}_{j \\in \\Omega}$ is an orthonormal family in an inner product space. The result above implies that if $\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}=0$, then $\\alpha_{j}=0$ for every $j \\in \\Omega$.\n\nLinear algebra, and algebra more generally, deals with sums of only finitely many terms. However, in analysis we often want to sum infinitely many terms. For example, earlier we defined the infinite sum of a sequence $g_{1}, g_{2}, \\ldots$ in a normed vector space to be the limit as $n \\rightarrow \\infty$ of the partial sums $\\sum_{k=1}^{n} g_{k}$ if that limit exists (see 6.40).\n\nThe next definition captures a more powerful method of dealing with infinite sums. The sum defined below is called an unordered sum because the set $\\Gamma$ is not assumed to come with any ordering. A finite unordered sum is defined in the obvious way.\n\n### 8.53 Definition unordered sum; $\\sum_{k \\in \\Gamma} f_{k}$\n\nSuppose $\\left\\{f_{k}\\right\\}_{k \\in \\Gamma}$ is a family in a normed vector space $V$. The unordered sum $\\sum_{k \\in \\Gamma} f_{k}$ is said to converge if there exists $g \\in V$ such that for every $\\varepsilon>0$, there exists a finite subset $\\Omega$ of $\\Gamma$ such that\n\n$$\n\\left\\|g-\\sum_{j \\in \\Omega^{\\prime}} f_{j}\\right\\|<\\varepsilon\n$$\n\nfor all finite sets $\\Omega^{\\prime}$ with $\\Omega \\subset \\Omega^{\\prime} \\subset \\Gamma$. If this happens, we set $\\sum_{k \\in \\Gamma} f_{k}=g$. If there is no such $g \\in V$, then $\\sum_{k \\in \\Gamma} f_{k}$ is left undefined.\n\nExercises at the end of this section ask you to develop basic properties of unordered sums, including the following:\n\n- Suppose $\\left\\{a_{k}\\right\\}_{k \\in \\Gamma}$ is a family in $\\mathbf{R}$ and $a_{k} \\geq 0$ for each $k \\in \\Gamma$. Then the unordered sum $\\sum_{k \\in \\Gamma} a_{k}$ converges if and only if\n\n$$\n\\sup \\left\\{\\sum_{j \\in \\Omega} a_{j}: \\Omega \\text { is a finite subset of } \\Gamma\\right\\}<\\infty\n$$\n\nFurthermore, if $\\sum_{k \\in \\Gamma} a_{k}$ converges then it equals the supremum above. If $\\sum_{k \\in \\Gamma} a_{k}$ does not converge, then the supremum above is $\\infty$ and we write $\\sum_{k \\in \\Gamma} a_{k}=\\infty$ (this notation should be used only when $a_{k} \\geq 0$ for each $k \\in \\Gamma$ ).\n\n- Suppose $\\left\\{a_{k}\\right\\}_{k \\in \\Gamma}$ is a family in $\\mathbf{R}$. Then the unordered sum $\\sum_{k \\in \\Gamma} a_{k}$ converges if and only if $\\sum_{k \\in \\Gamma}\\left|a_{k}\\right|<\\infty$. Thus convergence of an unordered summation in $\\mathbf{R}$ is the same as absolute convergence. As we are about to see, the situation in more general Hilbert spaces is quite different.\n\nNow we can extend 8.52 to infinite sums.\n\n### 8.54 linear combinations of an orthonormal family\n\nSuppose $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family in a Hilbert space $V$. Suppose $\\left\\{\\alpha_{k}\\right\\}_{k \\in \\Gamma}$ is a family in $\\mathbf{F}$. Then\n\n(a) the unordered sum $\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}$ converges $\\Longleftrightarrow \\sum_{k \\in \\Gamma}\\left|\\alpha_{k}\\right|^{2}<\\infty$.\n\nFurthermore, if $\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}$ converges, then\n\n(b)\n\n$$\n\\left\\|\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}\\right\\|^{2}=\\sum_{k \\in \\Gamma}\\left|\\alpha_{k}\\right|^{2}\n$$\n\nProof First suppose $\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}$ converges, with $\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}=g$. Suppose $\\varepsilon>0$. Then there exists a finite set $\\Omega \\subset \\Gamma$ such that\n\n$$\n\\left\\|g-\\sum_{j \\in \\Omega^{\\prime}} \\alpha_{j} e_{j}\\right\\|<\\varepsilon\n$$\n\nfor all finite sets $\\Omega^{\\prime}$ with $\\Omega \\subset \\Omega^{\\prime} \\subset \\Gamma$. If $\\Omega^{\\prime}$ is a finite set with $\\Omega \\subset \\Omega^{\\prime} \\subset \\Gamma$, then the inequality above implies that\n\n$$\n\\|g\\|-\\varepsilon<\\left\\|\\sum_{j \\in \\Omega^{\\prime}} \\alpha_{j} e_{j}\\right\\|<\\|g\\|+\\varepsilon\n$$\n\nwhich (using 8.52) implies that\n\n$$\n\\|g\\|-\\varepsilon<\\left(\\sum_{j \\in \\Omega^{\\prime}}\\left|\\alpha_{j}\\right|^{2}\\right)^{1 / 2}<\\|g\\|+\\varepsilon\n$$\n\nThus $\\|g\\|=\\left(\\sum_{k \\in \\Gamma}\\left|\\alpha_{k}\\right|^{2}\\right)^{1 / 2}$, completing the proof of one direction of (a) and the proof of (b).\n\nTo prove the other direction of (a), now suppose $\\sum_{k \\in \\Gamma}\\left|\\alpha_{k}\\right|^{2}<\\infty$. Thus there exists an increasing sequence $\\Omega_{1} \\subset \\Omega_{2} \\subset \\cdots$ of finite subsets of $\\Gamma$ such that for each $m \\in \\mathbf{Z}^{+}$,\n\n$$\n\\sum_{j \\in \\Omega^{\\prime} \\backslash \\Omega_{m}}\\left|\\alpha_{j}\\right|^{2}<\\frac{1}{m^{2}}\n$$\n\nfor every finite set $\\Omega^{\\prime}$ such that $\\Omega_{m} \\subset \\Omega^{\\prime} \\subset \\Gamma$. For each $m \\in \\mathbf{Z}^{+}$, let\n\n$$\ng_{m}=\\sum_{j \\in \\Omega_{m}} \\alpha_{j} e_{j} .\n$$\n\nIf $n>m$, then 8.52 implies that\n\n$$\n\\left\\|g_{n}-g_{m}\\right\\|^{2}=\\sum_{j \\in \\Omega_{n} \\backslash \\Omega_{m}}\\left|\\alpha_{j}\\right|^{2}<\\frac{1}{m^{2}}\n$$\n\nThus $g_{1}, g_{2}, \\ldots$ is a Cauchy sequence and hence converges to some element $g$ of $V$.\n\nTemporarily fixing $m \\in \\mathbf{Z}^{+}$and taking the limit of the equation above as $n \\rightarrow \\infty$, we see that\n\n$$\n\\left\\|g-g_{m}\\right\\| \\leq \\frac{1}{m}\n$$\n\nTo show that $\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}=g$, suppose $\\varepsilon>0$. Let $m \\in \\mathbf{Z}^{+}$be such that $\\frac{2}{m}<\\varepsilon$. Suppose $\\Omega^{\\prime}$ is a finite set with $\\Omega_{m} \\subset \\Omega^{\\prime} \\subset \\Gamma$. Then\n\n$$\n\\begin{aligned}\n\\left\\|g-\\sum_{j \\in \\Omega^{\\prime}} \\alpha_{j} e_{j}\\right\\| & \\leq\\left\\|g-g_{m}\\right\\|+\\left\\|g_{m}-\\sum_{j \\in \\Omega^{\\prime}} \\alpha_{j} e_{j}\\right\\| \\\\\n& \\leq \\frac{1}{m}+\\left\\|\\sum_{j \\in \\Omega^{\\prime} \\backslash \\Omega_{m}} \\alpha_{j} e_{j}\\right\\| \\\\\n& =\\frac{1}{m}+\\left(\\sum_{j \\in \\Omega^{\\prime} \\backslash \\Omega_{m}}\\left|\\alpha_{j}\\right|^{2}\\right)^{1 / 2} \\\\\n& <\\varepsilon,\n\\end{aligned}\n$$\n\nwhere the third line comes from 8.52 and the last line comes from 8.55 . Thus $\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}=g$, completing the proof.\n\n### 8.56 Example a convergent unordered sum need not converge absolutely\n\nSuppose $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}$is the orthogonal family in $\\ell^{2}$ defined by setting $e_{k}$ equal to the sequence that is 0 everywhere except for a 1 in the $k^{\\text {th }}$ slot. Then by 8.54 , the unordered sum\n\n$$\n\\sum_{k \\in \\mathbf{Z}^{+}} \\frac{1}{k} e_{k}\n$$\n\nconverges in $\\ell^{2}$ (because $\\sum_{k \\in \\mathbf{Z}^{+}} \\frac{1}{k^{2}}<\\infty$ ) even though $\\sum_{k \\in \\mathbf{Z}^{+}}\\left\\|\\frac{1}{k} e_{k}\\right\\|=\\infty$. Note that $\\sum_{k \\in \\mathbf{Z}^{+}} \\frac{1}{k} e_{k}=\\left(1, \\frac{1}{2}, \\frac{1}{3}, \\ldots\\right) \\in \\ell^{2}$.\n\nNow we prove an important inequality.\n\n### 8.57 Bessel's inequality\n\nSuppose $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family in an inner product space $V$ and $f \\in V$. Then\n\n$$\n\\sum_{k \\in \\Gamma}\\left|\\left\\langle f, e_{k}\\right\\rangle\\right|^{2} \\leq\\|f\\|^{2}\n$$\n\nProof Suppose $\\Omega$ is a finite subset of $\\Gamma$. Then\n\n$f=\\sum_{j \\in \\Omega}\\left\\langle f, e_{j}\\right\\rangle e_{j}+\\left(f-\\sum_{j \\in \\Omega}\\left\\langle f, e_{j}\\right\\rangle e_{j}\\right)$,\n\nwhere the first sum above is orthogonal to the term in parentheses above (as you should verify).\n\nApplying the Pythagorean Theorem (8.9) to the equation above gives\n\n$$\n\\begin{aligned}\n\\|f\\|^{2} & =\\left\\|\\sum_{j \\in \\Omega}\\left\\langle f, e_{j}\\right\\rangle e_{j}\\right\\|^{2}+\\left\\|f-\\sum_{j \\in \\Omega}\\left\\langle f, e_{j}\\right\\rangle e_{j}\\right\\|^{2} \\\\\n& \\geq\\left\\|\\sum_{j \\in \\Omega}\\left\\langle f, e_{j}\\right\\rangle e_{j}\\right\\|^{2} \\\\\n& =\\sum_{j \\in \\Omega}\\left|\\left\\langle f, e_{j}\\right\\rangle\\right|^{2},\n\\end{aligned}\n$$\n\nwhere the last equality follows from 8.52. Because the inequality above holds for every finite set $\\Omega \\subset \\Gamma$, we conclude that $\\|f\\|^{2} \\geq \\sum_{k \\in \\Gamma}\\left|\\left\\langle f, e_{k}\\right\\rangle\\right|^{2}$, as desired.\n\nRecall that the span of a family $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ in a vector space is the set of finite sums of the form\n\n$$\n\\sum_{j \\in \\Omega} \\alpha_{j} e_{j},\n$$\n\nwhere $\\Omega$ is a finite subset of $\\Gamma$ and $\\left\\{\\alpha_{j}\\right\\}_{j \\in \\Omega}$ is a family in $\\mathbf{F}$ (see 6.54). Bessel's inequality now allows us to prove the following beautiful result showing that the closure of the span of an orthonormal family is a set of infinite sums.\n\n### 8.58 closure of the span of an orthonormal family\n\nSuppose $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family in a Hilbert space $V$. Then\n\n(a) $\\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}}=\\left\\{\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}:\\left\\{\\alpha_{k}\\right\\}_{k \\in \\Gamma}\\right.$ is a family in $\\mathbf{F}$ and $\\left.\\sum_{k \\in \\Gamma}\\left|\\alpha_{k}\\right|^{2}<\\infty\\right\\}$.\n\nFurthermore,\n\n$$\nf=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k}\n$$\n\nfor every $f \\in \\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}}$.\n\nProof The right side of (a) above makes sense because of 8.54(a). Furthermore, the right side of (a) above is a subspace of $V$ because $\\ell^{2}(\\Gamma)$ [which equals $\\mathcal{L}^{2}(\\mu)$, where $\\mu$ is counting measure on $\\Gamma]$ is closed under addition and scalar multiplication by 7.5.\n\nSuppose first $\\left\\{\\alpha_{k}\\right\\}_{k \\in \\Gamma}$ is a family in $\\mathbf{F}$ and $\\sum_{k \\in \\Gamma}\\left|\\alpha_{k}\\right|^{2}<\\infty$. Let $\\varepsilon>0$. Then there is a finite subset $\\Omega$ of $\\Gamma$ such that\n\n$$\n\\sum_{j \\in \\Gamma \\backslash \\Omega}\\left|\\alpha_{j}\\right|^{2}<\\varepsilon^{2}\n$$\n\nThe inequality above and 8.54 (b) imply that\n\n$$\n\\left\\|\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k}-\\sum_{j \\in \\Omega} \\alpha_{j} e_{j}\\right\\|<\\varepsilon\n$$\n\nThe definition of the closure (see 6.7) now implies that $\\sum_{k \\in \\Gamma} \\alpha_{k} e_{k} \\in \\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}}$, showing that the right side of (a) is contained in the left side of (a).\n\nTo prove the inclusion in the other direction, now suppose $f \\in \\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}}$. Let\n\n$$\ng=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k}\n$$\n\nwhere the sum above converges by Bessel's inequality (8.57) and by 8.54(a). The direction of the inclusion that we just proved implies that $g \\in \\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}}$. Thus\n\n$$\ng-f \\in \\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}} .\n$$\n\nEquation 8.59 implies that $\\left\\langle g, e_{j}\\right\\rangle=\\left\\langle f, e_{j}\\right\\rangle$ for each $j \\in \\Gamma$, as you should verify (which will require using the Cauchy-Schwarz inequality if done rigorously). Hence\n\n$$\n\\left\\langle g-f, e_{k}\\right\\rangle=0 \\quad \\text { for every } k \\in \\Gamma .\n$$\n\nThis implies that\n\n$$\ng-f \\in\\left(\\operatorname{span}\\left\\{e_{j}\\right\\}_{j \\in \\Gamma}\\right)^{\\perp}=\\left(\\overline{\\operatorname{span}\\left\\{e_{j}\\right\\}_{j \\in \\Gamma}}\\right)^{\\perp}\n$$\n\nwhere the equality above comes from 8.40 (d). Now 8.60 and the inclusion above imply that $f=g$ [see $8.40(\\mathrm{~b})$ ], which along with 8.59 implies that $f$ is in the right side of (a), completing the proof of (a).\n\nThe equations $f=g$ and 8.59 also imply (b).\n\n## Parseval's Identity\n\nNote that 8.52 implies that every orthonormal family in an inner product space is linearly independent (see 6.54 to review the definition of linearly independent and basis). Linear algebra deals mainly with finite-dimensional vector spaces, but infinitedimensional vector spaces frequently appear in analysis. The notion of a basis is not so useful when doing analysis with infinite-dimensional vector spaces because the definition of span does not take advantage of the possibility of summing an infinite number of elements.\n\nHowever, 8.58 tells us that taking the closure of the span of an orthonormal family can capture the sum of infinitely many elements. Thus we make the following definition.\n\n### 8.61 Definition orthonormal basis\n\nAn orthonormal family $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ in a Hilbert space $V$ is called an orthonormal basis of $V$ if\n\n$$\n\\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}}=V .\n$$\n\nIn addition to requiring orthonormality (which implies linear independence), the definition above differs from the definition of a basis by considering the closure of the span rather than the span. An important point to keep in mind is that despite the terminology, an orthonormal basis is not necessarily a basis in the sense of 6.54. In fact, if $\\Gamma$ is an infinite set and $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal basis of $V$, then $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is not a basis of $V$ (see Exercise 9).\n\n### 8.62 Example orthonormal bases\n\n- For $n \\in \\mathbf{Z}^{+}$and $k \\in\\{1, \\ldots, n\\}$, let $e_{k}$ be the element of $\\mathbf{F}^{n}$ all of whose coordinates are 0 except the $k^{\\text {th }}$ coordinate, which is 1 :\n\n$$\ne_{k}=(0, \\ldots, 0,1,0, \\ldots, 0) .\n$$\n\nThen $\\left\\{e_{k}\\right\\}_{k \\in\\{1, \\ldots, n\\}}$ is an orthonormal basis of $\\mathbf{F}^{n}$.\n\n- Let $e_{1}=\\left(\\frac{1}{\\sqrt{3}}, \\frac{1}{\\sqrt{3}}, \\frac{1}{\\sqrt{3}}\\right), e_{2}=\\left(-\\frac{1}{\\sqrt{2}}, \\frac{1}{\\sqrt{2}}, 0\\right)$, and $e_{3}=\\left(\\frac{1}{\\sqrt{6}}, \\frac{1}{\\sqrt{6}},-\\frac{2}{\\sqrt{6}}\\right)$. Then $\\left\\{e_{k}\\right\\}_{k \\in\\{1,2,3\\}}$ is an orthonormal basis of $\\mathbf{F}^{3}$, as you should verify.\n- The first three bullet points in 8.51 are examples of orthonormal families that are orthonormal bases. The exercises ask you to verify that we have an orthonormal basis in the first and second bullet points of 8.51. For the third bullet point (trigonometric functions), see Exercise 11 in Section 10D or see Chapter 11.\n\nThe next result shows why orthonormal bases are so useful—a Hilbert space with an orthonormal basis $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ behaves like $\\ell^{2}(\\Gamma)$.\n\n### 8.63 Parseval's identity\n\nSuppose $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal basis of a Hilbert space $V$ and $f, g \\in V$. Then\n\n(a) $f=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k}$\n\n(b) $\\langle f, g\\rangle=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle \\overline{\\left\\langle g, e_{k}\\right\\rangle}$;\n\n(c) $\\|f\\|^{2}=\\sum_{k \\in \\Gamma}\\left|\\left\\langle f, e_{k}\\right\\rangle\\right|^{2}$.\n\nProof The equation in (a) follows immediately from 8.58(b) and the definition of an orthonormal basis.\n\nTo prove (b), note that\n\n$$\n\\begin{aligned}\n\\langle f, g\\rangle & =\\left\\langle\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k}, g\\right\\rangle \\\\\n& =\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle\\left\\langle e_{k}, g\\right\\rangle \\\\\n& =\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle \\overline{\\left\\langle g, e_{k}\\right\\rangle},\n\\end{aligned}\n$$\n\nEquation (c) is called Parseval's identity in honor of Marc-Antoine Parseval (1755-1836), who discovered a special case in 1799.\n\nwhere the first equation follows from (a) and the second equation follows from the definition of an unordered sum and the Cauchy-Schwarz inequality.\n\nEquation (c) follows from setting $g=f$ in (b). An alternative proof: equation (c) follows from 8.54(b) and the equation $f=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k}$ from (a).\n\n## Gram-Schmidt Process and Existence of Orthonormal Bases\n\n### 8.64 Definition separable\n\nA normed vector space is called separable if it has a countable subset whose closure equals the whole space.\n\n### 8.65 Example separable normed vector spaces\n\n- Suppose $n \\in \\mathbf{Z}^{+}$. Then $\\mathbf{F}^{n}$ with the usual Hilbert space norm is separable because the closure of the countable set\n\n$$\n\\left\\{\\left(c_{1}, \\ldots, c_{n}\\right) \\in \\mathbf{F}^{n}: \\text { each } c_{j} \\text { is rational }\\right\\}\n$$\n\nequals $\\mathbf{F}^{n}$ (in case $\\mathbf{F}=\\mathbf{C}$ : to say that a complex number is rational in this context means that both the real and imaginary parts of the complex number are rational numbers in the usual sense).\n\n- The Hilbert space $\\ell^{2}$ is separable because the closure of the countable set\n\n$$\n\\bigcup_{n=1}^{\\infty}\\left\\{\\left(c_{1}, \\ldots, c_{n}, 0,0, \\ldots\\right) \\in \\ell^{2}: \\text { each } c_{j} \\text { is rational }\\right\\}\n$$\n\nis $\\ell^{2}$.\n\n- The Hilbert spaces $L^{2}([0,1])$ and $L^{2}(\\mathbf{R})$ are separable, as Exercise 13 asks you to verify [hint: consider finite linear combinations with rational coefficients of functions of the form $\\chi_{(c, d)}$, where $c$ and $d$ are rational numbers].\n\nA moment's thought about the definition of closure (see 6.7) shows that a normed vector space $V$ is separable if and only if there exists a countable subset $C$ of $V$ such that every open ball in $V$ contains at least one element of $C$.\n\n### 8.66 Example nonseparable normed vector spaces\n\n- Suppose $\\Gamma$ is an uncountable set. Then the Hilbert space $\\ell^{2}(\\Gamma)$ is not separable. To see this, note that $\\left\\|\\chi_{\\{j\\}}-\\chi_{\\{k\\}}\\right\\|=\\sqrt{2}$ for all $j, k \\in \\Gamma$ with $j \\neq k$. Hence\n\n$$\n\\left\\{B\\left(\\chi_{\\{k\\}}, \\frac{\\sqrt{2}}{2}\\right): k \\in \\Gamma\\right\\}\n$$\n\nis an uncountable collection of disjoint open balls in $\\ell^{2}(\\Gamma)$; no countable set can have at least one element in each of these balls.\n\n- The Banach space $L^{\\infty}([0,1])$ is not separable. Here $\\left\\|\\chi_{[0, s]}-\\chi_{[0, t]}\\right\\|=1$ for all $s, t \\in[0,1]$ with $s \\neq t$. Thus\n\n$$\n\\left\\{B\\left(\\chi_{[0, t]}, \\frac{1}{2}\\right): t \\in[0,1]\\right\\}\n$$\n\nis an uncountable collection of disjoint open balls in $L^{\\infty}([0,1])$.\n\nWe present two proofs of the existence of orthonormal bases of Hilbert spaces. The first proof works only for separable Hilbert spaces, but it gives a useful algorithm, called the Gram-Schmidt process, for constructing orthonormal sequences. The second proof works for all Hilbert spaces, but it uses a result that depends upon the Axiom of Choice.\n\nWhich proof should you read? In practice, the Hilbert spaces you will encounter will almost certainly be separable. Thus the first proof suffices, and it has the additional benefit of introducing you to a widely used algorithm. The second proof uses an entirely different approach and has the advantage of applying to separable and nonseparable Hilbert spaces. For maximum learning, read both proofs!\n\n8.67 existence of orthonormal bases for separable Hilbert spaces\n\nEvery separable Hilbert space has an orthonormal basis.\n\nProof Suppose $V$ is a separable Hilbert space and $\\left\\{f_{1}, f_{2}, \\ldots\\right\\}$ is a countable subset of $V$ whose closure equals $V$. We will inductively define an orthonormal sequence $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}$such that\n\n$$\n\\operatorname{span}\\left\\{f_{1}, \\ldots, f_{n}\\right\\} \\subset \\operatorname{span}\\left\\{e_{1}, \\ldots, e_{n}\\right\\}\n$$\n\nfor each $n \\in \\mathbf{Z}^{+}$. This will imply that $\\overline{\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}}=V$, which will mean that $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}$is an orthonormal basis of $V$.\n\nTo get started with the induction, set $e_{1}=f_{1} /\\left\\|f_{1}\\right\\|$ (we can assume that $f_{1} \\neq 0$ ).\n\nNow suppose $n \\in \\mathbf{Z}^{+}$and $e_{1}, \\ldots, e_{n}$ have been chosen so that $\\left\\{e_{k}\\right\\}_{k \\in\\{1, \\ldots, n\\}}$ is an orthonormal family in $V$ and 8.68 holds. If $f_{k} \\in \\operatorname{span}\\left\\{e_{1}, \\ldots, e_{n}\\right\\}$ for every $k \\in \\mathbf{Z}^{+}$, then $\\left\\{e_{k}\\right\\}_{k \\in\\{1, \\ldots, n\\}}$ is an orthonormal basis of $V$ (completing the proof) and the process should be stopped. Otherwise, let $m$ be the smallest positive integer such that\n\n8.69\n\n$$\nf_{m} \\notin \\operatorname{span}\\left\\{e_{1}, \\ldots, e_{n}\\right\\} .\n$$\n\nDefine $e_{n+1}$ by\n\n8.70\n\n$$\ne_{n+1}=\\frac{f_{m}-\\left\\langle f_{m}, e_{1}\\right\\rangle e_{1}-\\cdots-\\left\\langle f_{m}, e_{n}\\right\\rangle e_{n}}{\\left\\|f_{m}-\\left\\langle f_{m}, e_{1}\\right\\rangle e_{1}-\\cdots-\\left\\langle f_{m}, e_{n}\\right\\rangle e_{n}\\right\\|}\n$$\n\nClearly $\\left\\|e_{n+1}\\right\\|=1$ (8.69 guarantees there is no division by 0 ). If $k \\in\\{1, \\ldots, n\\}$, then the equation above implies that $\\left\\langle e_{n+1}, e_{k}\\right\\rangle=0$. Thus $\\left\\{e_{k}\\right\\}_{k \\in\\{1, \\ldots, n+1\\}}$ is an orthonormal fam-\n\nJørgen Gram (1850-1916) and Erhard Schmidt (1876-1959) popularized this process that constructs orthonormal sequences. ily in $V$. Also, 8.68 and the choice of $m$ as the smallest positive integer satisfying 8.69 imply that\n\n$$\n\\operatorname{span}\\left\\{f_{1}, \\ldots, f_{n+1}\\right\\} \\subset \\operatorname{span}\\left\\{e_{1}, \\ldots, e_{n+1}\\right\\}\n$$\n\ncompleting the induction and completing the proof.\n\nBefore considering nonseparable Hilbert spaces, we take a short detour to illustrate how the Gram-Schmidt process used in the previous proof can be used to find closest elements to subspaces. We begin with a result connecting the orthogonal projection onto a closed subspace with an orthonormal basis of that subspace.\n\n### 8.71 orthogonal projection in terms of an orthonormal basis\n\nSuppose that $U$ is a closed subspace of a Hilbert space $V$ and $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal basis of $U$. Then\n\n$$\nP_{U} f=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k}\n$$\n\nfor all $f \\in V$.\n\nProof Let $f \\in V$. If $k \\in \\Gamma$, then\n\n8.72\n\n$$\n\\left\\langle f, e_{k}\\right\\rangle=\\left\\langle f-P_{U} f, e_{k}\\right\\rangle+\\left\\langle P_{U} f, e_{k}\\right\\rangle=\\left\\langle P_{U} f, e_{k}\\right\\rangle,\n$$\n\nwhere the last equality follows from 8.37 (a). Now\n\n$$\nP_{U} f=\\sum_{k \\in \\Gamma}\\left\\langle P_{U} f, e_{k}\\right\\rangle e_{k}=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k},\n$$\n\nwhere the first equality follows from Parseval's identity [8.63(a)] as applied to $U$ and its orthonormal basis $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$, and the second equality follows from 8.72.\n\n### 8.73 Example best approximation\n\nFind the polynomial $g$ of degree at most 10 that minimizes\n\n$$\n\\int_{-1}^{1}|\\sqrt{|x|}-g(x)|^{2} d x\n$$\n\nSolution We will work in the real Hilbert space $L^{2}([-1,1])$ with the usual inner product $\\langle g, h\\rangle=\\int_{-1}^{1} g h$. For $k \\in\\{0,1, \\ldots, 10\\}$, let $f_{k} \\in L^{2}([-1,1])$ be defined by $f_{k}(x)=x^{k}$. Let $U$ be the subspace of $L^{2}([-1,1])$ defined by\n\n$$\nU=\\operatorname{span}\\left\\{f_{k}\\right\\}_{k \\in\\{0, \\ldots, 10\\}}\n$$\n\nApply the Gram-Schmidt process from the proof of 8.67 to $\\left\\{f_{k}\\right\\}_{k \\in\\{0, \\ldots, 10\\}}$, producing an orthonormal basis $\\left\\{e_{k}\\right\\}_{k \\in\\{0, \\ldots, 10\\}}$ of $U$, which is a closed subspace of $L^{2}([-1,1])$ (see Exercise 8). The point here is that $\\left\\{e_{k}\\right\\}_{k \\in\\{0, \\ldots, 10\\}}$ can be computed explicitly and exactly by using 8.70 and evaluating some integrals (using software that can do exact rational arithmetic will make the process easier), getting $e_{0}(x)=1 / \\sqrt{2}$, $e_{1}(x)=\\sqrt{6} x / 2, \\ldots$ up to\n\n$e_{10}(x)=\\frac{\\sqrt{42}}{512}\\left(-63+3465 x^{2}-30030 x^{4}+90090 x^{6}-109395 x^{8}+46189 x^{10}\\right)$.\n\nDefine $f \\in L^{2}([-1,1])$ by $f(x)=\\sqrt{|x|}$. Because $U$ is the subspace of $L^{2}([-1,1])$ consisting of polynomials of degree at most 10 and $P_{U} f$ equals the element of $U$ closest to $f$ (see 8.34), the formula in 8.71 tells us that the solution $g$ to our minimization problem is given by the formula\n\n$$\ng=\\sum_{k=0}^{10}\\left\\langle f, e_{k}\\right\\rangle e_{k}\n$$\n\nUsing the explicit expressions for $e_{0}, \\ldots, e_{10}$ and again evaluating some integrals, this gives\n\n$$\ng(x)=\\frac{693+15015 x^{2}-64350 x^{4}+139230 x^{6}-138567 x^{8}+51051 x^{10}}{2944} .\n$$\n\nThe figure here shows the graph of $f(x)=\\sqrt{|x|}$ (red) and the graph of its closest polynomial $g$ (blue) of degree at most 10 ; here closest means as measured in the norm of $L^{2}([-1,1])$.\n\nThe approximation of $f$ by $g$ is pretty good, especially considering that $f$ is not differentiable at 0 and thus a Taylor series expansion for $f$ does not make sense.\n\n![](https://cdn.mathpix.com/cropped/2024_01_20_2970d025e1911bd8290dg-263.jpg?height=418&width=580&top_left_y=1683&top_left_x=633)\n\nRecall that a subset $\\Gamma$ of a set $V$ can be thought of as a family in $V$ by considering $\\left\\{e_{f}\\right\\}_{f \\in \\Gamma}$, where $e_{f}=f$. With this convention, a subset $\\Gamma$ of an inner product space $V$ is an orthonormal subset of $V$ if $\\|f\\|=1$ for all $f \\in \\Gamma$ and $\\langle f, g\\rangle=0$ for all $f, g \\in \\Gamma$ with $f \\neq g$.\n\nThe next result characterizes the orthonormal bases as the maximal elements among the collection of orthonormal subsets of a Hilbert space. Recall that a set $\\Gamma \\in \\mathcal{A}$ in a collection of subsets of a set $V$ is a maximal element of $\\mathcal{A}$ if there does not exist $\\Gamma^{\\prime} \\in \\mathcal{A}$ such that $\\Gamma \\varsubsetneqq \\Gamma^{\\prime}$ (see 6.55).\n\n### 8.74 orthonormal bases as maximal elements\n\nSuppose $V$ is a Hilbert space, $\\mathcal{A}$ is the collection of all orthonormal subsets of $V$, and $\\Gamma$ is an orthonormal subset of $V$. Then $\\Gamma$ is an orthonormal basis of $V$ if and only if $\\Gamma$ is a maximal element of $\\mathcal{A}$.\n\nProof First suppose $\\Gamma$ is an orthonormal basis of $V$. Parseval's identity [8.63(a)] implies that the only element of $V$ that is orthogonal to every element of $\\Gamma$ is 0 . Thus there does not exist an orthonormal subset of $V$ that strictly contains $\\Gamma$. In other words, $\\Gamma$ is a maximal element of $\\mathcal{A}$.\n\nTo prove the other direction, suppose now that $\\Gamma$ is a maximal element of $\\mathcal{A}$. Let $U$ denote the span of $\\Gamma$. Then\n\n$$\nU^{\\perp}=\\{0\\}\n$$\n\nbecause if $f$ is a nonzero element of $U^{\\perp}$, then $\\Gamma \\cup\\{f /\\|f\\|\\}$ is an orthonormal subset of $V$ that strictly contains $\\Gamma$. Hence $\\bar{U}=V$ (by 8.42), which implies that $\\Gamma$ is an orthonormal basis of $V$.\n\nNow we are ready to prove that every Hilbert space has an orthonormal basis. Before reading the next proof, you may want to review the definition of a chain (6.58), which is a collection of sets such that for each pair of sets in the collection, one of them is contained in the other. You should also review Zorn's Lemma (6.60), which gives a way to show that a collection of sets contains a maximal element.\n\n### 8.75 existence of orthonormal bases for all Hilbert spaces\n\nEvery Hilbert space has an orthonormal basis.\n\nProof Suppose $V$ is a Hilbert space. Let $\\mathcal{A}$ be the collection of all orthonormal subsets of $V$. Suppose $\\mathcal{C} \\subset \\mathcal{A}$ is a chain. Let $L$ be the union of all the sets in $\\mathcal{C}$. If $f \\in L$, then $\\|f\\|=1$ because $f$ is an element of some orthonormal subset of $V$ that is contained in $\\mathcal{C}$.\n\nIf $f, g \\in L$ with $f \\neq g$, then there exist orthonormal subsets $\\Omega$ and $\\Gamma$ in $\\mathcal{C}$ such that $f \\in \\Omega$ and $g \\in \\Gamma$. Because $\\mathcal{C}$ is a chain, either $\\Omega \\subset \\Gamma$ or $\\Gamma \\subset \\Omega$. Either way, there is an orthonormal subset of $V$ that contains both $f$ and $g$. Thus $\\langle f, g\\rangle=0$.\n\nWe have shown that $L$ is an orthonormal subset of $V$; in other words, $L \\in \\mathcal{A}$. Thus Zorn's Lemma (6.60) implies that $\\mathcal{A}$ has a maximal element. Now 8.74 implies that $V$ has an orthonormal basis.\n\n## Riesz Representation Theorem, Revisited\n\nNow that we know that every Hilbert space has an orthonormal basis, we can give a completely different proof of the Riesz Representation Theorem (8.47) than the proof we gave earlier.\n\nNote that the new proof below of the Riesz Representation Theorem gives the formula 8.77 for $h$ in terms of an orthonormal basis. One interesting feature of this formula is that $h$ is uniquely determined by $\\varphi$ and thus $h$ does not depend upon the choice of an orthonormal basis. Hence despite its appearance, the right side of 8.77 is independent of the choice of an orthonormal basis.\n\n### 8.76 Riesz Representation Theorem\n\nSuppose $\\varphi$ is a bounded linear functional on a Hilbert space $V$ and $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal basis of $V$. Let\n\n8.77\n\n$$\nh=\\sum_{k \\in \\Gamma} \\overline{\\varphi\\left(e_{k}\\right)} e_{k}\n$$\n\nThen\n\n8.78\n\n$$\n\\varphi(f)=\\langle f, h\\rangle\n$$\n\nfor all $f \\in V$. Furthermore, $\\|\\varphi\\|=\\left(\\sum_{k \\in \\Gamma}\\left|\\varphi\\left(e_{k}\\right)\\right|^{2}\\right)^{1 / 2}$.\n\nProof First we must show that the sum defining $h$ makes sense. To do this, suppose $\\Omega$ is a finite subset of $\\Gamma$. Then\n\n$$\n\\sum_{j \\in \\Omega}\\left|\\varphi\\left(e_{j}\\right)\\right|^{2}=\\varphi\\left(\\sum_{j \\in \\Omega} \\overline{\\varphi\\left(e_{j}\\right)} e_{j}\\right) \\leq\\|\\varphi\\|\\left\\|\\sum_{j \\in \\Omega} \\overline{\\varphi\\left(e_{j}\\right)} e_{j}\\right\\|=\\|\\varphi\\|\\left(\\sum_{j \\in \\Omega}\\left|\\varphi\\left(e_{j}\\right)\\right|^{2}\\right)^{1 / 2},\n$$\n\nwhere the last equality follows from 8.52. Dividing by $\\left(\\sum_{j \\in \\Omega}\\left|\\varphi\\left(e_{j}\\right)\\right|^{2}\\right)^{1 / 2}$ gives\n\n$$\n\\left(\\sum_{j \\in \\Omega}\\left|\\varphi\\left(e_{j}\\right)\\right|^{2}\\right)^{1 / 2} \\leq\\|\\varphi\\|\n$$\n\nBecause the inequality above holds for every finite subset $\\Omega$ of $\\Gamma$, we conclude that\n\n$$\n\\sum_{k \\in \\Gamma}\\left|\\varphi\\left(e_{k}\\right)\\right|^{2} \\leq\\|\\varphi\\|^{2}\n$$\n\nThus the sum defining $h$ makes sense (by 8.54) in equation 8.77.\n\nNow 8.77 shows that $\\left\\langle h, e_{j}\\right\\rangle=\\overline{\\varphi\\left(e_{j}\\right)}$ for each $j \\in \\Gamma$. Thus if $f \\in V$ then\n\n$$\n\\varphi(f)=\\varphi\\left(\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle e_{k}\\right)=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle \\varphi\\left(e_{k}\\right)=\\sum_{k \\in \\Gamma}\\left\\langle f, e_{k}\\right\\rangle \\overline{\\left\\langle h, e_{k}\\right\\rangle}=\\langle f, h\\rangle,\n$$\n\nwhere the first and last equalities follow from 8.63 and the second equality follows from the boundedness/continuity of $\\varphi$. Thus 8.78 holds.\n\nFinally, the Cauchy-Schwarz inequality, equation 8.78 , and the equation $\\varphi(h)=$ $\\langle h, h\\rangle$ show that $\\|\\varphi\\|=\\|h\\|=\\left(\\sum_{k \\in \\Gamma}\\left|\\varphi\\left(e_{k}\\right)\\right|^{2}\\right)^{1 / 2}$.\n\n## EXERCISES 8C\n\n1 Verify that the family $\\left\\{e_{k}\\right\\}_{k \\in \\mathbf{Z}}$ as defined in the third bullet point of Example 8.51 is an orthonormal family in $L^{2}((-\\pi, \\pi])$. The following formulas should help:\n\n$$\n\\begin{aligned}\n& (\\sin x)(\\cos y)=\\frac{\\sin (x-y)+\\sin (x+y)}{2}, \\\\\n& (\\sin x)(\\sin y)=\\frac{\\cos (x-y)-\\cos (x+y)}{2}, \\\\\n& (\\cos x)(\\cos y)=\\frac{\\cos (x-y)+\\cos (x+y)}{2} .\n\\end{aligned}\n$$\n\n2 Suppose $\\left\\{a_{k}\\right\\}_{k \\in \\Gamma}$ is a family in $\\mathbf{R}$ and $a_{k} \\geq 0$ for each $k \\in \\Gamma$. Prove the unordered sum $\\sum_{k \\in \\Gamma} a_{k}$ converges if and only if\n\n$$\n\\sup \\left\\{\\sum_{j \\in \\Omega} a_{j}: \\Omega \\text { is a finite subset of } \\Gamma\\right\\}<\\infty\n$$\n\nFurthermore, prove that if $\\sum_{k \\in \\Gamma} a_{k}$ converges then it equals the supremum above.\n\n3 Suppose $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family in an inner product space $V$. Prove that if $f \\in V$, then $\\left\\{k \\in \\Gamma:\\left\\langle f, e_{k}\\right\\rangle \\neq 0\\right\\}$ is a countable set.\n\n4 Suppose $\\left\\{f_{k}\\right\\}_{k \\in \\Gamma}$ and $\\left\\{g_{k}\\right\\}_{k \\in \\Gamma}$ are families in a normed vector space such that $\\sum_{k \\in \\Gamma} f_{k}$ and $\\sum_{k \\in \\Gamma} g_{k}$ converge. Prove that $\\sum_{k \\in \\Gamma}\\left(f_{k}+g_{k}\\right)$ converges and\n\n$$\n\\sum_{k \\in \\Gamma}\\left(f_{k}+g_{k}\\right)=\\sum_{k \\in \\Gamma} f_{k}+\\sum_{k \\in \\Gamma} g_{k}\n$$\n\n5 Suppose $\\left\\{f_{k}\\right\\}_{k \\in \\Gamma}$ is a family in a normed vector space such that $\\sum_{k \\in \\Gamma} f_{k}$ converges. Prove that if $c \\in \\mathbf{F}$, then $\\sum_{k \\in \\Gamma}\\left(c f_{k}\\right)$ converges and\n\n$$\n\\sum_{k \\in \\Gamma}\\left(c f_{k}\\right)=c \\sum_{k \\in \\Gamma} f_{k}\n$$\n\n6 Suppose $\\left\\{a_{k}\\right\\}_{k \\in \\Gamma}$ is a family in $\\mathbf{R}$. Prove that the unordered sum $\\sum_{k \\in \\Gamma} a_{k}$ converges if and only if $\\sum_{k \\in \\Gamma}\\left|a_{k}\\right|<\\infty$.\n\n7 Suppose $\\left\\{f_{k}\\right\\}_{k \\in \\mathbf{Z}^{+}}$is a family in a normed vector space $V$ and $f \\in V$. Prove that the unordered sum $\\sum_{k \\in \\mathbf{Z}^{+}} f_{k}$ equals $f$ if and only if the usual ordered sum $\\sum_{k=1}^{\\infty} f_{p(k)}$ equals $f$ for every injective and surjective function $p: \\mathbf{Z}^{+} \\rightarrow \\mathbf{Z}^{+}$.\n\n8 Explain why 8.58 implies that if $\\Gamma$ is a finite set and $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family in a Hilbert space $V$, then $\\operatorname{span}\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is a closed subspace of $V$.\n\n9 Suppose $V$ is an infinite-dimensional Hilbert space. Prove that there does not exist a basis of $V$ that is an orthonormal family.\n\n10 (a) Show that the orthonormal family given in the first bullet point of Example 8.51 is an orthonormal basis of $\\ell^{2}$.\n\n(b) Show that the orthonormal family given in the second bullet point of Example 8.51 is an orthonormal basis of $\\ell^{2}(\\Gamma)$.\n\n(c) Show that the orthonormal family given in the fourth bullet point of Example 8.51 is not an orthonormal basis of $L^{2}([0,1))$.\n\n(d) Show that the orthonormal family given in the fifth bullet point of Example 8.51 is not an orthonormal basis of $L^{2}(\\mathbf{R})$.\n\n11 Suppose $\\mu$ is a $\\sigma$-finite measure on $(X, \\mathcal{S})$ and $v$ is a $\\sigma$-finite measure on $(Y, \\mathcal{T})$. Suppose also that $\\left\\{e_{j}\\right\\}_{j \\in \\Omega}$ is an orthonormal basis of $L^{2}(\\mu)$ and $\\left\\{f_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal basis of $L^{2}(v)$ for some countable set $\\Gamma$. For $j \\in \\Omega$ and $k \\in \\Gamma$, define $g_{j, k}: X \\times Y \\rightarrow \\mathbf{F}$ by\n\n$$\ng_{j, k}(x, y)=e_{j}(x) f_{k}(y)\n$$\n\nProve that $\\left\\{g_{j, k}\\right\\}_{j \\in \\Omega, k \\in \\Gamma}$ is an orthonormal basis of $L^{2}(\\mu \\times v)$.\n\n12 Prove the converse of Parseval's identity. More specifically, prove that if $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal family in a Hilbert space $V$ and\n\n$$\n\\|f\\|^{2}=\\sum_{k \\in \\Gamma}\\left|\\left\\langle f, e_{k}\\right\\rangle\\right|^{2}\n$$\n\nfor every $f \\in V$, then $\\left\\{e_{k}\\right\\}_{k \\in \\Gamma}$ is an orthonormal basis of $V$.\n\n13 (a) Show that the Hilbert space $L^{2}([0,1])$ is separable.\n\n(b) Show that the Hilbert space $L^{2}(\\mathbf{R})$ is separable.\n\n(c) Show that the Banach space $\\ell^{\\infty}$ is not separable.\n\n14 Prove that every subspace of a separable normed vector space is separable.\n\n15 Suppose $V$ is an infinite-dimensional Hilbert space. Prove that there does not exist a translation invariant measure on the Borel subsets of $V$ that assigns positive but finite measure to each open ball in $V$.\n\n[A subset of $V$ is called a Borel set if it is in the smallest $\\sigma$-algebra containing all the open subsets of $V$. A measure $\\mu$ on the Borel subsets of $V$ is called translation invariant if $\\mu(f+E)=\\mu(E)$ for every $f \\in V$ and every Borel set E of $V$.]\n\n16 Find the polynomial $g$ of degree at most 4 that minimizes $\\int_{0}^{1}\\left|x^{5}-g(x)\\right|^{2} d x$.\n\n17 Prove that each orthonormal family in a Hilbert space can be extended to an orthonormal basis of the Hilbert space. Specifically, suppose $\\left\\{e_{j}\\right\\}_{j \\in \\Omega}$ is an orthonormal family in a Hilbert space $V$. Prove that there exists a set $\\Gamma$ containing $\\Omega$ and an orthonormal basis $\\left\\{f_{k}\\right\\}_{k \\in \\Gamma}$ of $V$ such that $f_{j}=e_{j}$ for every $j \\in \\Omega$.\n\n18 Prove that every vector space has a basis.\n\n19 Find the polynomial $g$ of degree at most 4 such that\n\n$$\nf\\left(\\frac{1}{2}\\right)=\\int_{0}^{1} f g\n$$\n\nfor every polynomial $f$ of degree at most 4 .\n\n## Exercises 20-25 are for readers familiar with analytic functions\n\n20 Suppose $G$ is a nonempty open subset of $\\mathbf{C}$. The Bergman space $L_{a}^{2}(G)$ is defined to be the set of analytic functions $f: G \\rightarrow \\mathbf{C}$ such that\n\n$$\n\\int_{G}|f|^{2} d \\lambda_{2}<\\infty,\n$$\n\nwhere $\\lambda_{2}$ is the usual Lebesgue measure on $\\mathbf{R}^{2}$, which is identified with $\\mathbf{C}$. For $f, h \\in L_{a}^{2}(G)$, define $\\langle f, h\\rangle$ to be $\\int_{G} f \\bar{h} d \\lambda_{2}$.\n\n(a) Show that $L_{a}^{2}(G)$ is a Hilbert space.\n\n(b) Show that if $w \\in G$, then $f \\mapsto f(w)$ is a bounded linear functional on $L_{a}^{2}(G)$.\n\n21 Let $\\mathbf{D}$ denote the open unit disk in $\\mathbf{C}$; thus\n\n$$\n\\mathbf{D}=\\{z \\in \\mathbf{C}:|z|<1\\}\n$$\n\n(a) Find an orthonormal basis of $L_{a}^{2}(\\mathbf{D})$.\n\n(b) Suppose $f \\in L_{a}^{2}(\\mathbf{D})$ has Taylor series\n\n$$\nf(z)=\\sum_{k=0}^{\\infty} a_{k} z^{k}\n$$\n\nfor $z \\in \\mathbf{D}$. Find a formula for $\\|f\\|$ in terms of $a_{0}, a_{1}, a_{2}, \\ldots$\n\n(c) Suppose $w \\in$ D. By the previous exercise and the Riesz Representation Theorem (8.47 and 8.76), there exists $\\Gamma_{w} \\in L_{a}^{2}(\\mathbf{D})$ such that\n\n$$\nf(w)=\\left\\langle f, \\Gamma_{w}\\right\\rangle \\text { for all } f \\in L_{a}^{2}(\\mathbf{D}) \\text {. }\n$$\n\nFind an explicit formula for $\\Gamma_{w}$.\n\n22 Suppose $G$ is the annulus defined by\n\n$$\nG=\\{z \\in \\mathbf{C}: 1<|z|<2\\} .\n$$\n\n(a) Find an orthonormal basis of $L_{a}^{2}(G)$.\n\n(b) Suppose $f \\in L_{a}^{2}(G)$ has Laurent series\n\n$$\nf(z)=\\sum_{k=-\\infty}^{\\infty} a_{k} z^{k}\n$$\n\nfor $z \\in G$. Find a formula for $\\|f\\|$ in terms of $\\ldots, a_{-1}, a_{0}, a_{1}, \\ldots$\n\n23 Prove that if $f \\in L_{a}^{2}(\\mathbf{D} \\backslash\\{0\\})$, then $f$ has a removable singularity at 0 (meaning that $f$ can be extended to a function that is analytic on $\\mathbf{D}$ ).\n\n24 The Dirichlet space $\\mathcal{D}$ is defined to be the set of analytic functions $f: \\mathbf{D} \\rightarrow \\mathbf{C}$ such that\n\n$$\n\\int_{\\mathbf{D}}\\left|f^{\\prime}\\right|^{2} d \\lambda_{2}<\\infty\n$$\n\nFor $f, g \\in \\mathcal{D}$, define $\\langle f, g\\rangle$ to be $f(0) \\overline{g(0)}+\\int_{\\mathbf{D}} f^{\\prime} \\overline{g^{\\prime}} \\mathrm{d} \\lambda_{2}$.\n\n(a) Show that $\\mathcal{D}$ is a Hilbert space.\n\n(b) Show that if $w \\in \\mathbf{D}$, then $f \\mapsto f(w)$ is a bounded linear functional on $\\mathcal{D}$.\n\n(c) Find an orthonormal basis of $\\mathcal{D}$.\n\n(d) Suppose $f \\in \\mathcal{D}$ has Taylor series\n\n$$\nf(z)=\\sum_{k=0}^{\\infty} a_{k} z^{k}\n$$\n\nfor $z \\in \\mathbf{D}$. Find a formula for $\\|f\\|$ in terms of $a_{0}, a_{1}, a_{2}, \\ldots$.\n\n(e) Suppose $w \\in$ D. Find an explicit formula for $\\Gamma_{w} \\in \\mathcal{D}$ such that\n\n$$\nf(w)=\\left\\langle f, \\Gamma_{w}\\right\\rangle \\text { for all } f \\in \\mathcal{D} \\text {. }\n$$\n\n25 (a) Prove that the Dirichlet space $\\mathcal{D}$ is contained in the Bergman space $L_{a}^{2}(\\mathbf{D})$.\n\n(b) Prove that there exists a function $f \\in L_{a}^{2}(\\mathbf{D})$ such that $f$ is uniformly continuous on $\\mathbf{D}$ and $f \\notin \\mathcal{D}$.\n"
  },
  {
    "path": "tests/image/math.md",
    "content": "This sentence uses `$` delimiters to show math inline: $\\sqrt{3x-1}+(1+x)^2$\n\nThis sentence $E = mc^2$ uses delimiters to show math inline: $`\\sqrt{3x-1}+(1+x)^2`$\n\n**The Cauchy-Schwarz Inequality**\n$$\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)$$\n\n**The Cauchy-Schwarz Inequality**\n\n```math\n\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)\n```\n\n![image](test copy.png)\n\n$\\int_0^1 x^2 dx$\n\n<!-- snacks: header start\n\\def\\x{5}\nsnacks: header end -->\n\n$ \\x \\leq 17 $\n\n```math\n\n```\n"
  },
  {
    "path": "tests/image/test-mermaid.md",
    "content": "```mermaid\nsequenceDiagram\n    participant dotcom\n    participant iframe\n    participant viewscreen\n    dotcom->>iframe: loads html w/ iframe url\n    iframe->>viewscreen: request template\n    viewscreen->>iframe: html & javascript\n    iframe->>dotcom: iframe ready\n    dotcom->>iframe: set mermaid data on iframe\n    iframe->>iframe: render mermaid\n```\n"
  },
  {
    "path": "tests/image/test.aux",
    "content": "\\relax \n\\@writefile{toc}{\\contentsline {section}{\\numberline {1}Image Tests}{1}{}\\protected@file@percent }\n\\@writefile{lof}{\\contentsline {figure}{\\numberline {1}{\\ignorespaces Test image centered in a figure environment.}}{1}{}\\protected@file@percent }\n\\newlabel{fig:test_image}{{1}{1}{}{figure.1}{}}\n\\@writefile{toc}{\\contentsline {section}{\\numberline {2}Some beautiful mathematical equations}{2}{}\\protected@file@percent }\n\\gdef \\@abspage@last{4}\n"
  },
  {
    "path": "tests/image/test.css",
    "content": ".foo {\n  background: lightblue url(\"./test.png\") no-repeat fixed center;\n  content: url(test.png);\n}\n"
  },
  {
    "path": "tests/image/test.html",
    "content": "<body>\n  <a href=\"/\" class=\"navbar-brand\" aria-label=\"logo\">\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      role=\"img\"\n      width=\"173\"\n      height=\"50\"\n      viewBox=\"0 0 742 214\"\n      aria-label=\"Neovim\"\n    >\n      <title>Neovim</title>\n      <defs>\n        <linearGradient x1=\"50%\" y1=\"0%\" x2=\"50%\" y2=\"100%\" id=\"a\">\n          <stop stop-color=\"#16B0ED\" stop-opacity=\".8\" offset=\"0%\"></stop>\n          <stop stop-color=\"#0F59B2\" stop-opacity=\".837\" offset=\"100%\"></stop>\n        </linearGradient>\n        <linearGradient x1=\"50%\" y1=\"0%\" x2=\"50%\" y2=\"100%\" id=\"b\">\n          <stop stop-color=\"#7DB643\" offset=\"0%\"></stop>\n          <stop stop-color=\"#367533\" offset=\"100%\"></stop>\n        </linearGradient>\n        <linearGradient x1=\"50%\" y1=\"0%\" x2=\"50%\" y2=\"100%\" id=\"c\">\n          <stop stop-color=\"#88C649\" stop-opacity=\".8\" offset=\"0%\"></stop>\n          <stop stop-color=\"#439240\" stop-opacity=\".84\" offset=\"100%\"></stop>\n        </linearGradient>\n      </defs>\n      <g fill=\"none\" fill-rule=\"evenodd\">\n        <path\n          d=\"M.027 45.459L45.224-.173v212.171L.027 166.894V45.459z\"\n          fill=\"url(#a)\"\n          transform=\"translate(1 1)\"\n        ></path>\n        <path\n          d=\"M129.337 45.89L175.152-.149l-.928 212.146-45.197-45.104.31-121.005z\"\n          fill=\"url(#b)\"\n          transform=\"matrix(-1 0 0 1 305 1)\"\n        ></path>\n        <path\n          d=\"M45.194-.137L162.7 179.173l-32.882 32.881L12.25 33.141 45.194-.137z\"\n          fill=\"url(#c)\"\n          transform=\"translate(1 1)\"\n        ></path>\n        <path\n          d=\"M46.234 84.032l-.063 7.063-36.28-53.563 3.36-3.422 32.983 49.922z\"\n          fill-opacity=\".13\"\n          fill=\"#000\"\n        ></path>\n        <g fill=\"#444\">\n          <path\n            d=\"M227 154V64.44h4.655c1.55 0 2.445.75 2.685 2.25l.806 13.502c4.058-5.16 8.786-9.316 14.188-12.466 5.4-3.15 11.413-4.726 18.037-4.726 4.893 0 9.205.781 12.935 2.34 3.729 1.561 6.817 3.811 9.264 6.751 2.448 2.942 4.297 6.48 5.55 10.621 1.253 4.14 1.88 8.821 1.88 14.042V154h-8.504V96.754c0-8.402-1.91-14.987-5.729-19.757-3.82-4.771-9.667-7.156-17.544-7.156-5.851 0-11.28 1.516-16.292 4.545-5.013 3.032-9.489 7.187-13.427 12.467V154H227zM350.624 63c5.066 0 9.755.868 14.069 2.605 4.312 1.738 8.052 4.268 11.219 7.592s5.638 7.412 7.419 12.264C385.11 90.313 386 95.883 386 102.17c0 1.318-.195 2.216-.588 2.696-.393.48-1.01.719-1.851.719h-64.966v1.70c0 6.708.784 12.609 2.353 17.7 1.567 5.09 3.8 9.357 6.695 12.802 2.895 3.445 6.393 6.034 10.495 7.771 4.1 1.738 8.686 2.606 13.752 2.606 4.524 0 8.446-.494 11.762-1.483 3.317-.988 6.108-2.097 8.37-3.324 2.261-1.227 4.056-2.336 5.383-3.324 1.326-.988 2.292-1.482 2.895-1.482.784 0 1.388.3 1.81.898l2.352 2.875c-1.448 1.797-3.362 3.475-5.745 5.031-2.383 1.558-5.038 2.891-7.962 3.998-2.926 1.109-6.062 1.991-9.41 2.65a52.21 52.21 0 01-10.088.989c-6.152 0-11.762-1.064-16.828-3.19-5.067-2.125-9.415-5.225-13.043-9.298-3.63-4.074-6.435-9.06-8.415-14.96C310.99 121.655 310 114.9 310 107.294c0-6.408.92-12.323 2.76-17.744 1.84-5.421 4.493-10.093 7.961-14.016 3.467-3.922 7.72-6.991 12.758-9.209C338.513 64.11 344.229 63 350.624 63zm.573 6c-4.696 0-8.904.702-12.623 2.105-3.721 1.404-6.936 3.421-9.65 6.053-2.713 2.631-4.908 5.79-6.586 9.474S319.55 94.439 319 99h60c0-4.679-.672-8.874-2.013-12.588-1.343-3.712-3.232-6.856-5.67-9.43-2.44-2.571-5.367-4.545-8.782-5.92-3.413-1.374-7.192-2.062-11.338-2.062zM435.546 63c6.526 0 12.368 1.093 17.524 3.28 5.154 2.186 9.5 5.286 13.04 9.298 3.538 4.013 6.238 8.85 8.099 14.51 1.861 5.66 2.791 11.994 2.791 19.002 0 7.008-.932 13.327-2.791 18.957-1.861 5.631-4.561 10.452-8.099 14.465-3.54 4.012-7.886 7.097-13.04 9.254-5.156 2.156-10.998 3.234-17.524 3.234-6.529 0-12.369-1.078-17.525-3.234-5.155-2.157-9.517-5.242-13.085-9.254-3.57-4.013-6.285-8.836-8.145-14.465-1.861-5.63-2.791-11.95-2.791-18.957 0-7.008.93-13.342 2.791-19.002 1.861-5.66 4.576-10.496 8.145-14.51 3.568-4.012 7.93-7.112 13.085-9.299C423.177 64.094 429.017 63 435.546 63zm-.501 86c5.341 0 10.006-.918 13.997-2.757 3.99-1.838 7.32-4.474 9.992-7.909 2.67-3.435 4.664-7.576 5.986-12.428 1.317-4.85 1.98-10.288 1.98-16.316 0-5.965-.66-11.389-1.98-16.27-1.322-4.88-3.316-9.053-5.986-12.519-2.67-3.463-6-6.13-9.992-7.999-3.991-1.867-8.657-2.802-13.997-2.802s-10.008.935-13.997 2.802c-3.991 1.87-7.322 4.536-9.992 8-2.671 3.465-4.68 7.637-6.03 12.518-1.35 4.881-2.026 10.305-2.026 16.27 0 6.026.675 11.465 2.025 16.316 1.35 4.852 3.36 8.993 6.031 12.428 2.67 3.435 6 6.07 9.992 7.91 3.99 1.838 8.656 2.756 13.997 2.756z\"\n            fill=\"currentColor\"\n          ></path>\n          <path\n            d=\"M530.57 152h-20.05L474 60h18.35c1.61 0 2.967.39 4.072 1.166 1.103.778 1.865 1.763 2.283 2.959l17.722 49.138a92.762 92.762 0 012.551 8.429c.686 2.751 1.298 5.5 1.835 8.25.537-2.75 1.148-5.499 1.835-8.25a77.713 77.713 0 012.64-8.429l18.171-49.138c.417-1.196 1.164-2.181 2.238-2.96 1.074-.776 2.356-1.165 3.849-1.165H567l-36.43 92zM572 61h23v92h-23zM610 153V60.443h13.624c2.887 0 4.78 1.354 5.682 4.06l1.443 6.856a52.7 52.7 0 015.097-4.962 32.732 32.732 0 015.683-3.879 30.731 30.731 0 016.496-2.57c2.314-.632 4.855-.948 7.624-.948 5.832 0 10.63 1.579 14.39 4.736 3.758 3.157 6.57 7.352 8.434 12.585 1.444-3.068 3.248-5.698 5.413-7.894 2.165-2.194 4.541-3.984 7.127-5.367a32.848 32.848 0 018.254-3.068 39.597 39.597 0 018.796-.992c5.111 0 9.653.783 13.622 2.345 3.97 1.565 7.307 3.849 10.014 6.857 2.706 3.007 4.766 6.675 6.18 11.005C739.29 83.537 740 88.5 740 94.092V153h-22.284V94.092c0-5.894-1.294-10.329-3.878-13.306-2.587-2.977-6.376-4.465-11.368-4.465-2.286 0-4.404.391-6.358 1.172a15.189 15.189 0 00-5.144 3.383c-1.473 1.474-2.631 3.324-3.474 5.548-.842 2.225-1.263 4.781-1.263 7.668V153h-22.37V94.092c0-6.194-1.249-10.704-3.744-13.532-2.497-2.825-6.18-4.24-11.051-4.24-3.19 0-6.18.798-8.976 2.391-2.799 1.593-5.399 3.775-7.804 6.54V153H610zM572 30h23v19h-23z\"\n            fill=\"currentColor\"\n            fill-opacity=\".8\"\n          ></path>\n        </g>\n      </g>\n    </svg>\n  </a>\n  <a href=\"https://github.com/folke/lazy.nvim/releases/latest\">\n    <img\n      alt=\"Latest release\"\n      src=\"https://img.shields.io/github/v/release/folke/lazy.nvim?style=for-the-badge&logo=starship&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41&include_prerelease&sort=semver\"\n    />\n  </a>\n  <a href=\"https://github.com/folke/lazy.nvim/pulse\">\n    <img\n      alt=\"Last commit\"\n      src=\"https://img.shields.io/github/last-commit/folke/lazy.nvim?style=for-the-badge&logo=starship&color=8bd5ca&logoColor=D9E0EE&labelColor=302D41\"\n    />\n  </a>\n</body>\n\n<style>\n  background: lightblue url(\"./test.png\") no-repeat fixed center;\n  content: url(test.png);\n</style>\n"
  },
  {
    "path": "tests/image/test.jsx",
    "content": "\nexport const Modal = (props) => {\n\n  return (\n    <Show>\n      <Image src=\"test.png\" />\n      <img src=\"test.png\" />\n      <img src=\"https://picsum.photos/200/300\"></img>\n    </Show>\n  )\n}\n"
  },
  {
    "path": "tests/image/test.md",
    "content": "# test\n\n## Inline Base64 Image\n\n![Hello World](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAAUCAAAAAAVAxSkAAABrUlEQVQ4y+3TPUvDQBgH8OdDOGa+oUMgk2MpdHIIgpSUiqC0OKirgxYX8QVFRQRpBRF8KShqLbgIYkUEteCgFVuqUEVxEIkvJFhae3m8S2KbSkcFBw9yHP88+eXucgH8kQZ/jSm4VDaIy9RKCpKac9NKgU4uEJNwhHhK3qvPBVO8rxRWmFXPF+NSM1KVMbwriAMwhDgVcrxeMZm85GR0PhvGJAAmyozJsbsxgNEir4iEjIK0SYqGd8sOR3rJAGN2BCEkOxhxMhpd8Mk0CXtZacxi1hr20mI/rzgnxayoidevcGuHXTC/q6QuYSMt1jC+gBIiMg12v2vb5NlklChiWnhmFZpwvxDGzuUzV8kOg+N8UUvNBp64vy9q3UN7gDXhwWLY2nMC3zRDibfsY7wjEkY79CdMZhrxSqqzxf4ZRPXwzWJirMicDa5KwiPeARygHXKNMQHEy3rMopDR20XNZGbJzUtrwDC/KshlLDWyqdmhxZzCsdYmf2fWZPoxCEDyfIvdtNQH0PRkH6Q51g8rFO3Qzxh2LbItcDCOpmuOsV7ntNaERe3v/lP/zO8yn4N+yNPrekmPAAAAAElFTkSuQmCC)\n\n## Wikilinks\n\n!![[test.png]]\n\n!![[test.png|options]]\n\n## Injected HTML\n\n<img src=\"test.png\" alt=\"png\" width=\"200\" height=\"30\" />\n\n<a href=\"https://github.com/folke/lazy.nvim/releases/latest\">\n  <img alt=\"Latest release\" src=\"https://img.shields.io/github/v/release/folke/lazy.nvim?style=for-the-badge&logo=starship&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41&include_prerelease&sort=semver\" />\n</a>\n\n## Markdown Links\n\n![small](https://picsum.photos/200/30)\n\n![relative png](./test.png)\n\n![png](test.png)\n\n![jpg](test.jpg)\n"
  },
  {
    "path": "tests/image/test.mmd",
    "content": "sequenceDiagram\n    participant dotcom\n    participant iframe\n    participant viewscreen\n    dotcom->>iframe: loads html w/ iframe url\n    iframe->>viewscreen: request template\n    viewscreen->>iframe: html & javascript\n    iframe->>dotcom: iframe ready\n    dotcom->>iframe: set mermaid data on iframe\n    iframe->>iframe: render mermaid\n"
  },
  {
    "path": "tests/image/test.norg",
    "content": "# Test\n\n- foo\n\nThis sentence $E = mc^2$ uses delimiters to show math inline: $`\\sqrt{3x-1}+(1+x)^2`$\n\n.image ./test.png\n\n.image test.png\nThis sentence uses `$` delimiters to show math inline: $\\sqrt{3x-1}+(1+x)^2$\n\nThis sentence $E = mc^2$ uses delimiters to show math inline: $`\\sqrt{3x-1}+(1+x)^2`$\n\n**The Cauchy-Schwarz Inequality**\n$$\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)$$\n\n**The Cauchy-Schwarz Inequality**\n\n```math\n\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)\n```\n\n![image](test copy.png)\n\n$\\int_0^1 x^2 dx$\n\n<!-- snacks: header start\n\\def\\x{5}\nsnacks: header end -->\n\n$ \\x \\leq 17 $\n"
  },
  {
    "path": "tests/image/test.org",
    "content": "[[test.png]]\n"
  },
  {
    "path": "tests/image/test.scss",
    "content": ".foo {\n  background: lightblue url(\"./test.png\") no-repeat fixed center;\n  background: lightblue url(\"./test.png\") no-repeat fixed center;\n  content: url(test.png);\n}\n"
  },
  {
    "path": "tests/image/test.svelte",
    "content": "<script>\n  const greeting = \"hello\";\n</script>\n\n<p class=\"greeting\">{greeting}</p>\n<img src=\"test.png\" alt=\"test\" />\n\n<style>\n  .greeting {\n    color: red;\n    font-weight: bold;\n    background: url(\"test.png\");\n  }\n</style>\n"
  },
  {
    "path": "tests/image/test.tex",
    "content": "\\documentclass{article}\n\\usepackage{graphicx, amsmath, amssymb, multirow}\n\\usepackage{geometry} \n\\geometry{\n  a4paper,\n  total={170mm,257mm},\n  left=20mm,\n  top=20mm,\n}\n\n\\begin{document}\n\n\\section{Image Tests}\n\nInline image: \\includegraphics[width=0.15\\textwidth]{test.png}\n\n\\begin{figure}[h]\n    \\centering\n    \\includegraphics[width=0.7\\textwidth]{test.png}\n    \\caption{Test image centered in a figure environment.}\n    \\label{fig:test_image}\n\\end{figure}\n\n\\newpage\n\n\\section{Some beautiful mathematical equations}\n\nRamanujan's formula:\n\n$$\\frac{1}{\\pi}=\\frac{2\\sqrt{2}}{9801}\\sum_{k=0}^\\infty\\frac{(4k)! (1103+26390k)}{(k!)^4 396^{4k}}$$\n\nEuler's formula: $e^{i\\pi}+1=0$\n\nArea of triangle with sides a,b,c is: \n\n$$A=\\frac{1}{2} \\sqrt{s(s-a)(s-b)(s-c)},\\quad s=\\frac{a+b+c}{2}$$\n\nThe most important formula in calculus:\n\n\\[\n  f'(x)=\\lim_{h\\to 0}\\frac{f(x+h)-f(x)}{h}\n\\]\n\nEinstain's field equations:\n\n\\[\nR_{\\mu\\nu}-\\frac{1}{2}g_{\\mu\\nu}R+\\Lambda g_{\\mu\\nu}=\\frac{8\\pi G}{c^4}T_{\\mu\\nu}\n\\]\n\nGamma function:\n\n\\[\n  \\Gamma(z) = \\int_0^\\infty t^{z-1} e^{-t} dt,\\quad\\Gamma(z+1) = z \\Gamma(z)\n\\]\n\nPythagora's theorem:\n\n$$a^2+b^2=c^2$$\n\nLogarithms: \n\n$$\\log ab=\\log a+\\log b$$\n\nNavier-Stokes equation:\n\n$$\\rho\\left(\\frac{\\partial \\textbf{v}}{\\partial t}+\\textbf{v}\\cdot\\nabla\\textbf{v}\\right)+\\nabla p-\\nabla\\cdot\\textbf{T}=\\textbf{f}$$\n\nLaw of gravity:\n\n$$F=G\\frac{m_1m_2}{r^2}$$\n\nFourier transform:\n\n\\[\n  F(\\omega) = \\int_{-\\infty}^\\infty f(t) e^{-2\\pi i t \\omega} dt\n\\]\n\nMaxwell's equations: \n\n\\begin{equation}\n\\begin{aligned}\n\\nabla \\times \\textbf{E}&=\\frac{\\rho}{\\epsilon_0} \\\\\n\\nabla \\cdot \\textbf{H}&=0 \\\\\n\\nabla \\times \\textbf{E}&=-\\frac 1c\\frac{\\partial \\textbf{H}}{\\partial t} \\\\\n\\nabla \\times \\textbf{H}&=\\frac 1c\\frac{\\partial \\textbf{E}}{\\partial t}\n\\end{aligned}\n\\end{equation}\n\nSchroedinger equation:\n\n\\[\ni \\hbar \\frac{\\partial \\psi}{\\partial t} = H\\Psi\n\\]\n\nChaos theory:\n\n$$x_{t+1}=kx_t(1-x_t)$$\n\nInformation theory:\n\n\\[\n  H=-\\sum p(x)\\log p(x)\n\\]\n\nBlack-Scholes equation:\n\n$$\\frac12\\sigma^2S^2\\frac{\\partial^2V}{\\partial S^2}+rS\\frac{\\partial V}{\\partial S}+\\frac{\\partial V}{\\partial t}-rV=0$$\n\nSecond law or thermodynamics:\n\n$$dS\\ge 0$$\n\nMass-energy equivalence:\n\n$$E=mc^2$$\n\nBasel problem:\n\\[\n  \\frac{\\pi^2}{6}=\\sum_{n=1}^\\infty \\frac{1}{n^2}\n\\]\n\nEuler-Masceroni constant:\n\n\\[\n\\gamma = \\lim_{n\\to\\infty}(\\sum_{n=1}^\\infty \\frac{1}{n}-\\log n)\\approx 0.5772156649\\ldots\n\\]\n\nBinomial expansion:\n\n\\[\n(a+b)^n = \\sum_{k=0}^n \\binom{n}{k} a^k b^{n-k}  \n\\]\n\nGauss:\n\n$$\\int_{-\\infty}^\\infty e^{-x^2} dx = \\sqrt{\\pi}$$\n\nThe Callan-Symanzik equation:\n\n\\[\n\\left[M\\frac{\\partial}{\\partial M}+\\beta(g)\\frac{\\partial}{\\partial g}+n\\gamma\\right]G^n(x_1,x_2,...,x_n;M,g)=0\n\\]\n\nMinimal surface equation:\n\n$$\\mathcal{A}(u)=\\int_\\Omega(1+|\\nabla u|^2)^{1/2} dx_1 dx_2 ... dx_n$$ \n\nMultiline equations:\n\n\\begin{eqnarray*}\n\\cos{2\\theta} & = & \\cos^2\\theta - \\sin^2\\theta \\\\\n              & = & 2\\cos^2\\theta - 1 \\\\\n              & = & 1 - 2\\sin^2\\theta\n\\end{eqnarray*}\n\nAnd finally:\n\n$$1=0.999999999999999999\\ldots$$\n\nJust for fun: $6 + 9 + 6 \\cdot 9 = 69$\n\nQuadratic equation:\n\n\\[\n  ax^2+bx+c=0 \\implies x_{1,2}=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}  \n\\]\n\nFour more ways to calculate pi:\n\n\\begin{equation*}\n  \\pi=\\sum_{k=0}^\\infty\\left[\\frac{1}{16^k}\\left(\\frac{4}{8k+1}-\\frac{2}{8k+4}-\\frac{1}{8k+5}-\\frac{1}{8k+6} \\right) \\right]\n\\end{equation*}\n\n\\begin{equation*}\n  \\frac{2}{\\pi}=\\frac{\\sqrt{2}}{2}\\cdot\\frac{\\sqrt{2+\\sqrt{2}}}{2}\\cdot\\frac{\\sqrt{2+\\sqrt{2+\\sqrt{2}}}}{2}\\cdot\\ldots\n\\end{equation*}\n\n\\begin{equation*}\n\\pi = 3+\\textstyle \\cfrac{1}{7+\\textstyle \\cfrac{1}{15+\\textstyle \\cfrac{1}{1+\\textstyle \\cfrac{1}{292+\\textstyle \\cfrac{1}{1+\\textstyle \\cfrac{1}{1+\\textstyle \\cfrac{1}{1+\\ddots}}}}}}}\n\\end{equation*}\n\nChudnovsky Formula:\n\n\\[\n\\frac{1}{\\pi} = \\frac{\\sqrt{10005}}{4270934400} \\sum_{k=0}^\\infty \\frac{(6k)! (13591409 + 545140134k)}{(3k)!\\,k!^3 (-640320)^{3k}}\n\\]\n\nCauchy's integral formula:\n\n$$f(a)=\\frac{1}{2\\pi i}\\int_{C} \\frac{f(z)}{z-a} dz $$\n\nStirling's factorial approximation:\n$$n! = \\sqrt{2 \\pi n} \\left( \\frac{n}{e} \\right)^n \\left( 1 + O \\left( \\frac{1}{n} \\right) \\right)$$\n\n\\end{document}\n\n\n\n\n"
  },
  {
    "path": "tests/image/test.tsx",
    "content": "export const Modal = (props) => {\n\n  return (\n    <Show>\n      <img src=\"test.png\" />\n      <img src=\"https://picsum.photos/200/300\"></img>\n    </Show>\n  )\n}\n"
  },
  {
    "path": "tests/image/test.typ",
    "content": "\n#figure(\n  image(\"test.png\", width: 80%),\n  caption: [\n    A step in the molecular testing\n    pipeline of our lab.\n  ],\n)\n\n#set page(width: auto, height: auto, margin: (x: 2pt, y: 2pt))\n#set text(size: 12pt, fill: rgb(\"#FF0000\"))\n$ 5 + 5 = 10 $\n\n$ E = g c^2 $\n\n$ A = pi r^2 $\n\n$ \"area\" = pi dot \"radius\"^2 $\n\n$ cal(A) :=\n\n    { x in RR | x \"is natural\" } $\n#let x = 5\n\n$ x < 17 $\n\n$ (3x + y) / 7 &= 9 && \"given\" \\\n  3x + y &= 63 & \"multiply by 7\" \\\n  3x &= 63 - y && \"subtract y\" \\\n  x &= 21 - y/3 & \"divide by 3\" $\n// snacks: header start\n#let x = 5\n// snacks: header end\n$ #x <= 17 $\n"
  },
  {
    "path": "tests/image/test.vue",
    "content": "<script setup>\nimport { ref } from \"vue\";\nconst greeting = ref(\"Hello World!\");\n</script>\n\n<template>\n  <p class=\"greeting\">{{ greeting }}</p>\n  <img src=\"test.png\" alt=\"test\" />\n</template>\n\n<style>\n.greeting {\n  color: red;\n  font-weight: bold;\n  background: url(\"test.png\");\n}\n</style>\n"
  },
  {
    "path": "tests/image/test2.md",
    "content": "# test\n\n## Wikilinks\n\n!![[test.jpg]]\n"
  },
  {
    "path": "tests/minit.lua",
    "content": "#!/usr/bin/env -S nvim -l\n\nvim.env.LAZY_STDPATH = \".tests\"\nvim.env.LAZY_PATH = vim.fs.normalize(\"~/projects/lazy.nvim\")\n\nif vim.fn.isdirectory(vim.env.LAZY_PATH) == 1 then\n  loadfile(vim.env.LAZY_PATH .. \"/bootstrap.lua\")()\nelse\n  load(vim.fn.system(\"curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua\"), \"bootstrap.lua\")()\nend\n\n-- Setup lazy.nvim\nrequire(\"lazy.minit\").setup({\n  spec = {\n    { dir = vim.uv.cwd() },\n  },\n})\n"
  },
  {
    "path": "tests/picker/diff_spec.lua",
    "content": "describe(\"picker.diff\", function()\n  local diff = require(\"snacks.picker.source.diff\")\n\n  describe(\"parse\", function()\n    it(\"parses git diff format\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"index abc123..def456 100644\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1,3 +1,3 @@ context\",\n        \" unchanged\",\n        \"-old line\",\n        \"+new line\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"file.txt\", blocks[1].file)\n      assert.equals(4, #blocks[1].header)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(1, blocks[1].hunks[1].line)\n      assert.equals(4, #blocks[1].hunks[1].diff)\n    end)\n\n    it(\"doesn't parse a filename from deleted lua comment\", function()\n      local lines = {\n        \"diff --git a/lua/todo-comments/config.lua b/lua/todo-comments/config.lua\",\n        \"index 0e2d34e..a8e1077 100644\",\n        \"--- a/lua/todo-comments/config.lua\",\n        \"+++ b/lua/todo-comments/config.lua\",\n        \"@@ -11,7 +11,6 @@ M.loaded = false\",\n        ' M.ns = vim.api.nvim_create_namespace(\"todo-comments\")',\n        \"\",\n        \" --- @class TodoOptions\",\n        \"--- TODO: add support for markdown todos\",\n        \" local defaults = {\",\n        \"   signs = true, -- show icons in the signs column\",\n        \"   sign_priority = 8, -- sign priority\",\n        \"      }\",\n        \"    end)\",\n        \"\",\n      }\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"lua/todo-comments/config.lua\", blocks[1].file)\n    end)\n\n    it(\"parses plain diff format (no git header)\", function()\n      local lines = {\n        \"--- file1.txt\\t2024-01-01 12:00:00\",\n        \"+++ file2.txt\\t2024-01-02 12:00:00\",\n        \"@@ -1,3 +1,3 @@\",\n        \" unchanged\",\n        \"-old line\",\n        \"+new line\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"file2.txt\", blocks[1].file)\n      assert.equals(2, #blocks[1].header)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(1, blocks[1].hunks[1].line)\n    end)\n\n    it(\"parses plain diff format (recursive)\", function()\n      local lines = {\n        \"diff -Naur old/file1.txt new/file1.txt\",\n        \"--- old/file1.txt\t2025-01-01 13:00:00.000000000 +0100\",\n        \"+++ new/file1.txt\t1970-01-01 01:00:00.000000000 +0100\",\n        \"@@ -1,3 +0,0 @@\",\n        \"-context1\",\n        \"-old content\",\n        \"-context3\",\n        \"diff -Naur old/file2.txt new/file2.txt\",\n        \"--- old/file2.txt\t1970-01-01 01:00:00.000000000 +0100\",\n        \"+++ new/file2.txt\t2025-01-01 13:00:00.000000000 +0100\",\n        \"@@ -0,0 +1,3 @@\",\n        \"+context1\",\n        \"+new line\",\n        \"+context3\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(2, #blocks)\n      assert.equals(3, #blocks[1].header)\n      assert.equals(\"file1.txt\", blocks[1].file)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(0, blocks[1].hunks[1].line)\n      assert.equals(3, #blocks[2].header)\n      assert.equals(\"file2.txt\", blocks[2].file)\n      assert.equals(1, #blocks[2].hunks)\n      assert.equals(1, blocks[2].hunks[1].line)\n    end)\n\n    it(\"parses combined diff format (merge commits)\", function()\n      local lines = {\n        \"diff --cc file.txt\",\n        \"index abc,def..123\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@@ -10,5 -12,3 +10,6 @@@ context\",\n        \"  unchanged in all\",\n        \"--removed from parent 1\",\n        \" -removed from parent 2\",\n        \"++added in merge\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"file.txt\", blocks[1].file)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(10, blocks[1].hunks[1].line) -- third position (+10)\n    end)\n\n    it(\"parses multiple files\", function()\n      local lines = {\n        \"diff --git a/file1.txt b/file1.txt\",\n        \"--- a/file1.txt\",\n        \"+++ b/file1.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old1\",\n        \"+new1\",\n        \"diff --git a/file2.txt b/file2.txt\",\n        \"--- a/file2.txt\",\n        \"+++ b/file2.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old2\",\n        \"+new2\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(2, #blocks)\n      assert.equals(\"file1.txt\", blocks[1].file)\n      assert.equals(\"file2.txt\", blocks[2].file)\n    end)\n\n    it(\"parses multiple hunks per file\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old1\",\n        \"+new1\",\n        \"@@ -10,1 +10,1 @@\",\n        \"-old2\",\n        \"+new2\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(2, #blocks[1].hunks)\n      assert.equals(1, blocks[1].hunks[1].line)\n      assert.equals(10, blocks[1].hunks[2].line)\n    end)\n\n    it(\"sorts hunks by line number\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -50,1 +50,1 @@\",\n        \"-old2\",\n        \"@@ -10,1 +10,1 @@\",\n        \"-old1\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(2, #blocks[1].hunks)\n      assert.equals(10, blocks[1].hunks[1].line) -- sorted\n      assert.equals(50, blocks[1].hunks[2].line)\n    end)\n\n    it(\"handles binary files\", function()\n      local lines = {\n        \"diff --git a/image.png b/image.png\",\n        \"index abc123..def456 100644\",\n        \"Binary files a/image.png and b/image.png differ\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"image.png\", blocks[1].file)\n      assert.equals(3, #blocks[1].header) -- diff line + binary notice\n      assert.equals(0, #blocks[1].hunks) -- no hunks for binary\n    end)\n\n    it(\"handles binary files with prefixes in the path\", function()\n      local lines = {\n        \"diff --git a/ b/image.png b/ b/image.png\",\n        \"index abc123..def456 100644\",\n        \"Binary files a/image.png and b/image.png differ\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\" b/image.png\", blocks[1].file)\n      assert.equals(3, #blocks[1].header) -- diff line + binary notice\n      assert.equals(0, #blocks[1].hunks) -- no hunks for binary\n    end)\n\n    it(\"handles pure renames\", function()\n      local lines = {\n        \"diff --git a/old.txt b/new.txt\",\n        \"similarity index 100%\",\n        \"rename from old.txt\",\n        \"rename to new.txt\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"new.txt\", blocks[1].file)\n      assert.equals(4, #blocks[1].header)\n      assert.equals(0, #blocks[1].hunks)\n    end)\n\n    it(\"handles renames with a diff\", function()\n      local lines = {\n        \"diff --git a/old.txt b/new.txt\",\n        \"similarity index 66%\",\n        \"rename from old.txt\",\n        \"rename to new.txt\",\n        \"--- a/old.text\",\n        \"+++ b/new.txt\",\n        \"@@ -1,3 +1,3 @@\",\n        \"-line0\",\n        \" line1\",\n        \" line2\",\n        \"+line3\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"new.txt\", blocks[1].file)\n      assert.equals(6, #blocks[1].header)\n      assert.equals(1, #blocks[1].hunks)\n    end)\n\n    it(\"handles mode changes\", function()\n      local lines = {\n        \"diff --git a/script.sh b/script.sh\",\n        \"old mode 100644\",\n        \"new mode 100755\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"script.sh\", blocks[1].file)\n      assert.equals(3, #blocks[1].header)\n      assert.equals(0, #blocks[1].hunks)\n    end)\n\n    it(\"handles deleted files\", function()\n      local lines = {\n        \"diff --git a/deleted.txt b/deleted.txt\",\n        \"deleted file mode 100644\",\n        \"index abc123..0000000\",\n        \"--- a/deleted.txt\",\n        \"+++ /dev/null\",\n        \"@@ -1,3 +0,0 @@\",\n        \"-line1\",\n        \"-line2\",\n        \"-line3\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"deleted.txt\", blocks[1].file)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(0, blocks[1].hunks[1].line) -- deleted at line 0\n    end)\n\n    it(\"handles new files\", function()\n      local lines = {\n        \"diff --git a/new.txt b/new.txt\",\n        \"new file mode 100644\",\n        \"index 0000000..abc123\",\n        \"--- /dev/null\",\n        \"+++ b/new.txt\",\n        \"@@ -0,0 +1,3 @@\",\n        \"+line1\",\n        \"+line2\",\n        \"+line3\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"new.txt\", blocks[1].file)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(1, blocks[1].hunks[1].line)\n    end)\n\n    it(\"ignores empty lines before diff\", function()\n      local lines = {\n        \"\",\n        \"  \",\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"file.txt\", blocks[1].file)\n    end)\n\n    it(\"handles files with spaces in name\", function()\n      local lines = {\n        \"diff --git a/dir c/my file.txt b/dir c/my file.txt\",\n        \"--- a/dir c/my file.txt\",\n        \"+++ b/dir c/my file.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"dir c/my file.txt\", blocks[1].file)\n    end)\n\n    it(\"handles quoted filenames\", function()\n      local lines = {\n        'diff --git \"a/my file.txt\" \"b/my file.txt\"',\n        '--- \"a/my file.txt\"',\n        '+++ \"b/my file.txt\"',\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"my file.txt\", blocks[1].file)\n    end)\n\n    it(\"handles files in subdirectories\", function()\n      local lines = {\n        \"diff --git a/path/to/file.txt b/path/to/file.txt\",\n        \"--- a/path/to/file.txt\",\n        \"+++ b/path/to/file.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"path/to/file.txt\", blocks[1].file)\n    end)\n\n    it(\"handles single-line changes in hunk header\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -5 +5 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(5, blocks[1].hunks[1].line)\n    end)\n\n    it(\"preserves diff content including - and + prefixes\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1,3 +1,3 @@\",\n        \" context\",\n        \"-removed\",\n        \"+added\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      local hunk_diff = blocks[1].hunks[1].diff\n      assert.equals(\"@@ -1,3 +1,3 @@\", hunk_diff[1])\n      assert.equals(\" context\", hunk_diff[2])\n      assert.equals(\"-removed\", hunk_diff[3])\n      assert.equals(\"+added\", hunk_diff[4])\n    end)\n\n    it(\"handles empty hunks (just @@ header)\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1,0 +1,0 @@\",\n        \"@@ -10,1 +10,1 @@\",\n        \"-old\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(2, #blocks[1].hunks)\n      assert.equals(1, #blocks[1].hunks[1].diff) -- just the @@ line\n      assert.equals(2, #blocks[1].hunks[2].diff) -- @@ + one line\n    end)\n\n    it(\"handles context-only hunks (no changes, just context)\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1,3 +1,3 @@\",\n        \" context line 1\",\n        \" context line 2\",\n        \" context line 3\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(1, #blocks[1].hunks)\n      assert.equals(4, #blocks[1].hunks[1].diff)\n    end)\n\n    it(\"handles hunk at line 0 (insertion at start)\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -0,0 +1,2 @@\",\n        \"+line1\",\n        \"+line2\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, blocks[1].hunks[1].line)\n    end)\n\n    it(\"handles very long filenames\", function()\n      local long_path = \"very/long/path/with/many/segments/\" .. string.rep(\"a\", 200) .. \".txt\"\n      local lines = {\n        \"diff --git a/\" .. long_path .. \" b/\" .. long_path,\n        \"--- a/\" .. long_path,\n        \"+++ b/\" .. long_path,\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(long_path, blocks[1].file)\n    end)\n\n    it(\"handles unicode in filenames\", function()\n      local lines = {\n        \"diff --git a/文件.txt b/文件.txt\",\n        \"--- a/文件.txt\",\n        \"+++ b/文件.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"文件.txt\", blocks[1].file)\n    end)\n\n    it(\"handles truncated/incomplete diff gracefully\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1,5 +1,5 @@\",\n        \" context\",\n        \"-old\",\n        -- Missing rest of hunk\n      }\n\n      -- Should not crash\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(1, #blocks[1].hunks)\n    end)\n\n    it(\"handles multiple git diffs\", function()\n      local lines = {\n        \"diff --git a/git1.txt b/git1.txt\",\n        \"--- a/git1.txt\",\n        \"+++ b/git1.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n        \"diff --git a/git2.txt b/git2.txt\",\n        \"--- a/git2.txt\",\n        \"+++ b/git2.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n        \"diff --git a/git3.txt b/git3.txt\",\n        \"--- a/git3.txt\",\n        \"+++ b/git3.txt\",\n        \"@@ -1,1 +1,1 @@\",\n        \"-old\",\n        \"+new\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(3, #blocks)\n      assert.equals(\"git1.txt\", blocks[1].file)\n      assert.equals(\"git2.txt\", blocks[2].file)\n      assert.equals(\"git3.txt\", blocks[3].file)\n    end)\n\n    it(\"handles symlink changes\", function()\n      local lines = {\n        \"diff --git a/link.txt b/link.txt\",\n        \"deleted file mode 120000\",\n        \"--- a/link.txt\",\n        \"+++ /dev/null\",\n        \"@@ -1 +0,0 @@\",\n        \"-target.txt\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(\"link.txt\", blocks[1].file)\n    end)\n\n    it(\"handles files with only newline changes\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n        \"@@ -1 +1 @@\",\n        \"-line\",\n        \"\\\\ No newline at end of file\",\n        \"+line\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(1, #blocks[1].hunks)\n      -- Should include the \"No newline\" marker\n      assert.truthy(vim.tbl_contains(blocks[1].hunks[1].diff, \"\\\\ No newline at end of file\"))\n    end)\n\n    it(\"handles diff with no file changes (same content)\", function()\n      local lines = {\n        \"diff --git a/file.txt b/file.txt\",\n        \"index abc123..abc123 100644\",\n        \"--- a/file.txt\",\n        \"+++ b/file.txt\",\n      }\n\n      local blocks = diff.parse(lines).blocks\n      assert.equals(1, #blocks)\n      assert.equals(0, #blocks[1].hunks) -- no hunks = no changes\n    end)\n  end)\nend)\n"
  },
  {
    "path": "tests/picker/git_status_spec.lua",
    "content": "---@module 'luassert'\n\nlocal Git = require(\"snacks.picker.source.git\")\n\ndescribe(\"git status\", function()\n  -- git status codes are always 2 characters\n  local tests = {\n    -- Unmerged cases\n    [\"AA\"] = { xy = \"AA\", status = \"added\", unmerged = true },\n    [\"UU\"] = { xy = \"UU\", status = \"modified\", unmerged = true },\n    [\"AU\"] = { xy = \"AU\", status = \"added\", unmerged = true },\n    [\"DD\"] = { xy = \"DD\", status = \"deleted\", unmerged = true },\n    [\"UD\"] = { xy = \"UD\", status = \"deleted\", unmerged = true },\n    [\"DU\"] = { xy = \"DU\", status = \"deleted\", unmerged = true },\n    [\"UA\"] = { xy = \"UA\", status = \"added\", unmerged = true },\n\n    -- Regular cases\n    [\" M\"] = { xy = \" M\", status = \"modified\" },\n    [\" D\"] = { xy = \" D\", status = \"deleted\" },\n    [\" R\"] = { xy = \" R\", status = \"renamed\" },\n    [\"??\"] = { xy = \"??\", status = \"untracked\" },\n    [\"!!\"] = { xy = \"!!\", status = \"ignored\" },\n\n    -- Staged cases\n    [\"M \"] = { xy = \"M \", status = \"modified\", staged = true },\n    [\"T \"] = { xy = \"T \", status = \"modified\", staged = true },\n    [\"D \"] = { xy = \"D \", status = \"deleted\", staged = true },\n    [\"A \"] = { xy = \"A \", status = \"added\", staged = true },\n    [\"AD\"] = { xy = \"AD\", status = \"added\", staged = true },\n    [\"C \"] = { xy = \"C \", status = \"copied\", staged = true },\n  }\n  for _, test in pairs(tests) do\n    it(\"should parse `\" .. test.xy .. \"`\", function()\n      local status = Git.git_status(test.xy)\n      status.priority = nil\n      assert.are.same(test, status)\n    end)\n  end\nend)\n"
  },
  {
    "path": "tests/picker/matcher_spec.lua",
    "content": "---@module 'luassert'\n\nlocal M = {}\nM.files = {\n  \"lua/snacks/animate/\",\n  \"lua/snacks/animate/easing.lua\",\n  \"lua/snacks/animate/init.lua\",\n  \"lua/snacks/bigfile.lua\",\n  \"lua/snacks/bufdelete.lua\",\n  \"lua/snacks/dashboard.lua\",\n  \"lua/snacks/debug.lua\",\n  \"lua/snacks/dim.lua\",\n  \"lua/snacks/git.lua\",\n  \"lua/snacks/gitbrowse.lua\",\n  \"lua/snacks/health.lua\",\n  \"lua/snacks/indent.lua\",\n  \"lua/snacks/init.lua\",\n  \"lua/snacks/input.lua\",\n  \"lua/snacks/lazygit.lua\",\n  \"lua/snacks/meta/\",\n  \"lua/snacks/meta/docs.lua\",\n  \"lua/snacks/meta/init.lua\",\n  \"lua/snacks/meta/types.lua\",\n  \"lua/snacks/notifier.lua\",\n  \"lua/snacks/notify.lua\",\n  \"lua/snacks/picker/\",\n  \"lua/snacks/picker/async.lua\",\n  \"lua/snacks/picker/init.lua\",\n  \"lua/snacks/picker/list.lua\",\n  \"lua/snacks/picker/matcher.lua\",\n  \"lua/snacks/picker/preview.lua\",\n  \"lua/snacks/picker/queue.lua\",\n  \"lua/snacks/picker/sorter.lua\",\n  \"lua/snacks/picker/topk.lua\",\n  \"lua/snacks/profiler/\",\n  \"lua/snacks/profiler/core.lua\",\n  \"lua/snacks/profiler/init.lua\",\n  \"lua/snacks/profiler/loc.lua\",\n  \"lua/snacks/profiler/picker.lua\",\n  \"lua/snacks/profiler/tracer.lua\",\n  \"lua/snacks/profiler/ui.lua\",\n  \"lua/snacks/quickfile.lua\",\n  \"lua/snacks/rename.lua\",\n  \"lua/snacks/scope.lua\",\n  \"lua/snacks/scratch.lua\",\n  \"lua/snacks/scroll.lua\",\n  \"lua/snacks/statuscolumn.lua\",\n  \"lua/snacks/terminal.lua\",\n  \"lua/snacks/toggle.lua\",\n  \"lua/snacks/util.lua\",\n  \"lua/snacks/win.lua\",\n  \"lua/snacks/words.lua\",\n  \"lua/snacks/zen.lua\",\n}\n\nlocal function fuzzy(pattern)\n  local chars = vim.split(pattern, \"\")\n  local pat = table.concat(chars, \".*\")\n  return vim.tbl_filter(function(v)\n    return v:find(pat)\n  end, M.files)\nend\n\ndescribe(\"fuzzy matching\", function()\n  local matcher = require(\"snacks.picker.core.matcher\").new()\n\n  local tests = {\n    { \"mod.md\", \"md\", { 5, 6 } },\n  }\n\n  for t, test in ipairs(tests) do\n    it(\"should find optimal match for \" .. t, function()\n      matcher:init(test[2])\n      local item = { text = test[1], idx = 1, score = 0 }\n      local score = matcher:match(item)\n      assert(score and score > 0, \"no match found\")\n      local positions = matcher:positions(item).text\n      assert.are.same(test[3], positions)\n    end)\n  end\n\n  local patterns = { \"snacks\", \"lua\", \"sgbs\", \"mark\", \"dcs\", \"xxx\", \"lsw\" }\n  local algos = { \"fuzzy\", \"fuzzy_find\" }\n  for _, pattern in ipairs(patterns) do\n    local chars = vim.split(pattern, \"\")\n    local expect = fuzzy(pattern)\n    for _, algo in ipairs(algos) do\n      it((\"should find fuzzy matches for %q with %s\"):format(pattern, algo), function()\n        local matches = {} ---@type string[]\n        for _, file in ipairs(M.files) do\n          if matcher[algo](matcher, file, file, chars) then\n            table.insert(matches, file)\n          end\n        end\n        assert.are.same(expect, matches)\n      end)\n    end\n  end\nend)\n"
  },
  {
    "path": "tests/picker/minheap_spec.lua",
    "content": "---@module 'luassert'\n\nlocal MinHeap = require(\"snacks.picker.util.minheap\")\n\ndescribe(\"MinHeap\", function()\n  local values = {} ---@type number[]\n  for i = 1, 2000 do\n    values[i] = i\n  end\n  ---@param tbl number[]\n  local function shuffle(tbl)\n    for i = #tbl, 2, -1 do\n      local j = math.random(i)\n      tbl[i], tbl[j] = tbl[j], tbl[i]\n    end\n    return tbl\n  end\n\n  for _ = 1, 100 do\n    it(\"should push and pop values correctly\", function()\n      local topk = MinHeap.new({ capacity = 10 })\n      for _, v in ipairs(shuffle(values)) do\n        topk:add(v)\n      end\n\n      table.sort(values, topk.cmp)\n      local topn = vim.list_slice(values, 1, 10)\n      assert.same(topn, topk:get())\n    end)\n  end\nend)\n"
  },
  {
    "path": "tests/picker/util_spec.lua",
    "content": "---@module 'luassert'\n\ndescribe(\"globs\", function()\n  local tests = {\n    [\"*.lua\"] = \"%.lua$\",\n    [\"*/*.lua\"] = \"/[^/]*%.lua$\",\n    [\"**/*.lua\"] = \"/[^/]*%.lua$\",\n    [\"foo/**/bar/*.lua\"] = \"foo/.*/bar/[^/]*%.lua$\",\n    [\"foo/*\"] = \"foo/\",\n    [\"foo/**\"] = \"foo/\",\n    [\"*.?sx\"] = \"%.[^/]sx$\",\n  }\n  for glob, pattern in pairs(tests) do\n    it(\"should convert glob to pattern: \" .. glob, function()\n      local result = Snacks.picker.util.glob2pattern(glob)\n      assert.are.same(pattern, result)\n    end)\n  end\nend)\n"
  },
  {
    "path": "tests/scope_spec.lua",
    "content": "---@module 'luassert'\n\nlocal M = {}\n\n---@param lines string|string[]\n---@param opts? {ft?: string, ts?: boolean}\nfunction M.set_lines(lines, opts)\n  opts = opts or {}\n  lines = type(lines) == \"string\" and vim.split(lines, \"\\n\") or lines\n  vim.api.nvim_buf_set_lines(0, 0, -1, false, lines --[[ @as string[] ]])\n  vim.bo.filetype = opts.ft or \"\"\n  vim.treesitter.stop()\n  assert(not vim.b.ts_highlight, \"treesitter highlight is still enabled\")\n  vim.b.snacks_ts = nil\n  if opts.ts then\n    vim.treesitter.start()\n    assert(vim.b.ts_highlight, \"treesitter highlight is not enabled\")\n  end\nend\n\nfunction M.inspect(v)\n  return vim.inspect(v):gsub(\"%s+\", \" \")\nend\n\nlocal test = [[\nfunction foo()\n  while true do\n    if x == 1 then\n      break\n    end\n    local y = 2\n  end\nend\n]]\n\ndescribe(\"scope\", function()\n  local tests = {\n    [1] = { 1, 8 },\n    [2] = { 2, 7 },\n    [3] = { 3, 5 },\n    [4] = { 3, 5 },\n    [5] = { 3, 5 },\n    [6] = { 2, 7 },\n    [7] = { 2, 7 },\n    [8] = { 1, 8 },\n  }\n  Snacks.config.scope = {\n    cursor = false,\n    min_size = 2,\n    treesitter = {\n      blocks = false,\n    },\n  }\n\n  for _, ws in ipairs({ false, true }) do\n    local lines = vim.split(vim.trim(test), \"\\n\")\n\n    local t = vim.deepcopy(tests)\n\n    if ws == true then\n      local c = #lines\n      -- insert empty lines\n      for i = 1, c do\n        table.insert(lines, i * 2, \"\")\n      end\n      local test2 = {}\n      -- transform tests\n      for line, s in pairs(t) do\n        test2[line * 2 - 1] = { s[1] * 2 - 1, s[2] * 2 - 1 }\n      end\n      t = test2\n    end\n\n    for _, ts in ipairs({ true, false }) do\n      for line, s in pairs(t) do\n        it(\"should get scope \" .. M.inspect({ line = line, scope = s, ts = ts, ws = ws }), function()\n          M.set_lines(lines, {\n            ft = \"lua\",\n            ts = ts,\n          })\n          Snacks.scope.get(function(scope)\n            assert(scope)\n            assert((scope.node == nil) == not ts)\n            assert.same(scope.from, s[1])\n            assert.same(scope.to, s[2])\n          end, {\n            pos = { line, 0 },\n            treesitter = { enabled = ts },\n          })\n        end)\n      end\n    end\n  end\nend)\n\nlocal function foo()\n  while true do\n    -- doo\n\n    if x == 1 and false then\n      break\n    end\n\n    local y = 2\n    local y = 2\n  end\nend\n"
  },
  {
    "path": "tests/terminal_spec.lua",
    "content": "---@module \"luassert\"\n\nlocal terminal = require(\"snacks.terminal\")\n\nlocal tests = {\n  { \"bash\", { \"bash\" } },\n  { '\"bash\"', { \"bash\" } },\n  {\n    '\"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\"     -c \"echo hello\"',\n    { \"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\", \"-c\", \"echo hello\" },\n  },\n  { \"pwsh -NoLogo\", { \"pwsh\", \"-NoLogo\" } },\n  { 'echo \"foo\\tbar\"', { \"echo\", \"foo\\tbar\" } },\n  { \"echo\\tfoo\", { \"echo\", \"foo\" } },\n  { 'this \"is \\\\\"a test\"', { \"this\", 'is \"a test' } },\n}\n\ndescribe(\"terminal.parse\", function()\n  for _, test in ipairs(tests) do\n    it(\"should parse \" .. test[1], function()\n      local result = terminal.parse(test[1])\n      assert.are.same(test[2], result)\n    end)\n  end\nend)\n\ndescribe(\"terminal.open\", function()\n  it(\"should set buffer when position is 'current'\", function()\n    -- Create a test buffer with content\n    vim.cmd(\"enew\")\n    local test_buf = vim.api.nvim_get_current_buf()\n    vim.api.nvim_buf_set_lines(test_buf, 0, -1, false, { \"test content\" })\n\n    -- Open terminal with position='current'\n    local term = terminal.open(nil, { win = { position = \"current\" } })\n\n    -- Check that the current window now has the terminal buffer\n    local current_win = vim.api.nvim_get_current_win()\n    local current_buf = vim.api.nvim_win_get_buf(current_win)\n\n    assert.are.equal(term.buf, current_buf, \"Terminal buffer should be set in current window\")\n    assert.are.equal(\"terminal\", vim.bo[current_buf].buftype, \"Buffer should be a terminal\")\n\n    -- Clean up\n    term:close()\n  end)\nend)\n"
  },
  {
    "path": "tests/util_spec.lua",
    "content": "---@module 'luassert'\n\nvim.g.mapleader = \" \"\n\ndescribe(\"util.normkey\", function()\n  local normkey = require(\"snacks.util\").normkey\n\n  local tests = {\n    [\"<c-a>\"] = \"<C-A>\",\n    [\"<C-A>\"] = \"<C-A>\",\n    [\"<a-a>\"] = \"<M-a>\",\n    [\"<a-A>\"] = \"<M-A>\",\n    [\"<m-a>\"] = \"<M-a>\",\n    [\"<m-A>\"] = \"<M-A>\",\n    [\"<s-a>\"] = \"A\",\n    [\"<c-j>\"] = \"<C-J>\",\n    [\"<c-]>\"] = \"<C-]>\",\n    [\"<c-\\\\>\"] = \"<C-\\\\>\",\n    [\"<c-/>\"] = \"<C-/>\",\n    [\"<Cr>\"] = \"<CR>\",\n    [\"<c-down>\"] = \"<C-Down>\",\n    [\"<scrollwheelUP>\"] = \"<ScrollWheelUp>\",\n    [\"<c-scrollwheelUP>\"] = \"<C-ScrollWheelUp>\",\n    [\"<Space>\"] = \"<Space>\",\n    [\"<space>\"] = \"<Space>\",\n    [\"<space><space>\"] = \"<Space><Space>\",\n    [\"<leader>\"] = \"<Space>\",\n    [\"<leader> \"] = \"<Space><Space>\",\n    [\"<leader><leader>\"] = \"<Space><Space>\",\n    [\"<p\"] = \"<p\",\n    [\"<lt>p\"] = \"<p\",\n  }\n\n  for input, expected in pairs(tests) do\n    it('should normalize \"' .. input .. '\"', function()\n      assert.are.equal(expected, normkey(input))\n    end)\n  end\nend)\n"
  },
  {
    "path": "vim.yml",
    "content": "base: lua51\nlua_versions:\n  - luajit\n\nglobals:\n  Snacks:\n    any: true\n  vim:\n    any: true\n  jit:\n    any: true\n  assert:\n    any: true\n  describe:\n    any: true\n  it:\n    any: true\n  before_each:\n    any: true\n"
  }
]