├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── markdownlint.yml │ ├── nvim-type-check.yml │ ├── panvimdoc.yml │ ├── pr-title.yml │ ├── stale-bot.yml │ └── stylua.yml ├── .gitignore ├── .luarc.json ├── .markdownlint.yaml ├── .stylua.toml ├── Justfile ├── LICENSE ├── README.md ├── doc └── nvim-origami.txt └── lua └── origami ├── config.lua ├── features ├── autofold-comments-imports.lua ├── fold-keymaps.lua ├── foldtext.lua ├── inspect-folds.lua ├── lsp-and-treesitter-foldexpr.lua ├── pause-folds-on-search.lua └── remember-folds.lua ├── init.lua ├── reference.md └── utils.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | # vim: filetype=editorconfig 2 | root = true 3 | 4 | [*] 5 | max_line_length = 100 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | indent_style = tab 10 | indent_size = 3 11 | tab_width = 3 12 | trim_trailing_whitespace = true 13 | 14 | [*.{yml,yaml,scm,cff}] 15 | indent_style = space 16 | indent_size = 2 17 | tab_width = 2 18 | 19 | [*.py] 20 | indent_style = space 21 | indent_size = 4 22 | tab_width = 4 23 | 24 | [*.md] 25 | indent_size = 4 26 | tab_width = 4 27 | trim_trailing_whitespace = false 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/displaying-a-sponsor-button-in-your-repository 2 | 3 | custom: https://www.paypal.me/ChrisGrieser 4 | ko_fi: pseudometa 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: textarea 7 | id: bug-description 8 | attributes: 9 | label: Bug Description 10 | description: A clear and concise description of the bug. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain your problem. 18 | - type: textarea 19 | id: reproduction-steps 20 | attributes: 21 | label: To Reproduce 22 | description: Steps to reproduce the problem 23 | placeholder: | 24 | For example: 25 | 1. Go to '...' 26 | 2. Click on '...' 27 | 3. Scroll down to '...' 28 | - type: textarea 29 | id: version-info 30 | attributes: 31 | label: neovim version 32 | render: Text 33 | validations: 34 | required: true 35 | - type: checkboxes 36 | id: checklist 37 | attributes: 38 | label: Make sure you have done the following 39 | options: 40 | - label: I have updated to the latest version of the plugin. 41 | required: true 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea 3 | title: "Feature Request: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: feature-requested 8 | attributes: 9 | label: Feature Requested 10 | description: A clear and concise description of the feature. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain the request. 18 | - type: checkboxes 19 | id: checklist 20 | attributes: 21 | label: Checklist 22 | options: 23 | - label: The feature would be useful to more users than just me. 24 | required: true 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What problem does this PR solve? 2 | 3 | ## How does the PR solve it? 4 | 5 | ## Checklist 6 | - [ ] Used only `camelCase` variable names. 7 | - [ ] If functionality is added or modified, also made respective changes to the 8 | `README.md` (the `.txt` file is auto-generated and does not need to be 9 | modified). 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "chore(dependabot): " 9 | -------------------------------------------------------------------------------- /.github/workflows/markdownlint.yml: -------------------------------------------------------------------------------- 1 | name: Markdownlint check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "**.md" 8 | - ".github/workflows/markdownlint.yml" 9 | - ".markdownlint.*" # markdownlint config files 10 | pull_request: 11 | paths: 12 | - "**.md" 13 | 14 | jobs: 15 | markdownlint: 16 | name: Markdownlint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: DavidAnson/markdownlint-cli2-action@v20 21 | with: 22 | globs: "**/*.md" 23 | -------------------------------------------------------------------------------- /.github/workflows/nvim-type-check.yml: -------------------------------------------------------------------------------- 1 | name: nvim type check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | build: 12 | name: nvim type check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: stevearc/nvim-typecheck-action@v2 17 | -------------------------------------------------------------------------------- /.github/workflows/panvimdoc.yml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - README.md 8 | - .github/workflows/panvimdoc.yml 9 | workflow_dispatch: {} # allows manual execution 10 | 11 | permissions: 12 | contents: write 13 | 14 | #─────────────────────────────────────────────────────────────────────────────── 15 | 16 | jobs: 17 | docs: 18 | runs-on: ubuntu-latest 19 | name: README.md to vimdoc 20 | steps: 21 | - uses: actions/checkout@v4 22 | - run: git pull # fix failure when multiple commits are pushed in succession 23 | - run: mkdir -p doc 24 | 25 | - name: panvimdoc 26 | uses: kdheepak/panvimdoc@main 27 | with: 28 | vimdoc: ${{ github.event.repository.name }} 29 | version: "Neovim" 30 | demojify: true 31 | treesitter: true 32 | 33 | - run: git pull 34 | - name: push changes 35 | uses: stefanzweifel/git-auto-commit-action@v5 36 | with: 37 | commit_message: "chore: auto-generate vimdocs" 38 | branch: ${{ github.head_ref }} 39 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | - ready_for_review 11 | 12 | permissions: 13 | pull-requests: read 14 | 15 | jobs: 16 | semantic-pull-request: 17 | name: Check PR title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | requireScope: false 25 | subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital 26 | types: | # add `improv` to the list of allowed types 27 | improv 28 | fix 29 | feat 30 | refactor 31 | build 32 | ci 33 | style 34 | test 35 | chore 36 | perf 37 | docs 38 | break 39 | revert 40 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: Stale bot 2 | on: 3 | schedule: 4 | - cron: "18 04 * * 3" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Close stale issues 15 | uses: actions/stale@v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | # DOCS https://github.com/actions/stale#all-options 20 | days-before-stale: 180 21 | days-before-close: 7 22 | stale-issue-label: "Stale" 23 | stale-issue-message: | 24 | This issue has been automatically marked as stale. 25 | **If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open. 26 | close-issue-message: | 27 | This issue has been closed due to inactivity, and will not be monitored. 28 | -------------------------------------------------------------------------------- /.github/workflows/stylua.yml: -------------------------------------------------------------------------------- 1 | name: Stylua check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | stylua: 12 | name: Stylua 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: JohnnyMorganz/stylua-action@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | version: latest 20 | args: --check . 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # help-tags auto-generated by lazy.nvim 2 | doc/tags 3 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime.version": "LuaJIT", 3 | "diagnostics.globals": [ 4 | "vim" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Defaults https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 2 | # DOCS https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md 3 | #─────────────────────────────────────────────────────────────────────────────── 4 | 5 | # MODIFIED SETTINGS 6 | blanks-around-headings: 7 | lines_below: 0 # space waster 8 | ul-style: { style: sublist } 9 | 10 | # not autofixable 11 | ol-prefix: { style: ordered } 12 | line-length: 13 | tables: false 14 | code_blocks: false 15 | no-inline-html: 16 | allowed_elements: [img, details, summary, kbd, a, br] 17 | 18 | #───────────────────────────────────────────────────────────────────────────── 19 | # DISABLED 20 | ul-indent: false # not compatible with using tabs 21 | no-hard-tabs: false # taken care of by editorconfig 22 | blanks-around-lists: false # space waster 23 | first-line-heading: false # e.g., ignore-comments 24 | no-emphasis-as-heading: false # sometimes useful 25 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/JohnnyMorganz/StyLua#options 2 | #─────────────────────────────────────────────────────────────────────────────── 3 | column_width = 100 4 | line_endings = "Unix" 5 | indent_type = "Tabs" 6 | indent_width = 3 7 | quote_style = "AutoPreferDouble" 8 | call_parentheses = "NoSingleTable" 9 | collapse_simple_statement = "Always" 10 | 11 | [sort_requires] 12 | enabled = true 13 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set quiet := true 2 | 3 | masonPath := "$HOME/.local/share/nvim/mason/bin/" 4 | 5 | #─────────────────────────────────────────────────────────────────────────────── 6 | 7 | stylua: 8 | #!/usr/bin/env zsh 9 | {{ masonPath }}/stylua --check --output-format=summary . && return 0 10 | {{ masonPath }}/stylua . 11 | echo "\nFiles formatted." 12 | 13 | lua_ls_check: 14 | {{ masonPath }}/lua-language-server --check . 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christopher Grieser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # nvim-origami 🐦📄 3 | 4 | 5 | badge 6 | 7 | *Fold with relentless elegance.* A collection of Quality-of-life features 8 | related to folding. 9 | 10 | Showcase 11 | 12 | ## Table of Content 13 | 14 | 15 | 16 | - [Features](#features) 17 | - [Installation](#installation) 18 | - [Configuration](#configuration) 19 | - [FAQ](#faq) 20 | * [Folds are still opened](#folds-are-still-opened) 21 | * [Debug folding issues](#debug-folding-issues) 22 | - [Credits](#credits) 23 | - [About the developer](#about-the-developer) 24 | 25 | 26 | 27 | ## Features 28 | 29 | 30 | | opts | description | requirements | 31 | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | 32 | | `.useLspFoldsWithTreesitterFallback` | Use the LSP to provide folds, with Treesitter as fallback if the LSP does not provide folding info. | 1. *not* using `nvim-ufo` (feature is redundant with `nvim-ufo`)
2. Nvim 0.11 | 33 | | `.foldKeymaps` | Overload the `h` key which will fold a line when used one the first non-blank character of (or before). And overload the `l` key, which will unfold a line when used on a folded line.[^1] This allows you to ditch `zc`, `zo`, and `za`, `h` and `l` are all you need. | — | 34 | | `.autoFold` | Automatically fold comments and/or imports when opening a file. | 1. *not* using `nvim-ufo` (feature is redundant with `nvim-ufo`)
2. nvim 0.11
3. LSP that provides fold information | 35 | | `.foldtextWithLineCount` | Add line count to the `foldtext`, preserving the syntax highlighting of the line. | 1. *not* using `nvim-ufo` (feature is redundant with `nvim-ufo`)
2. Treesitter parser for the language. | 36 | | `.pauseFoldsOnSearch` | Pause folds while searching, restore folds when done with searching. (Normally, folds are opened when you search for text inside them, and *stay* open afterward.) | — | 37 | | `.keepFoldsAcrossSessions` | Remember folds across sessions. | `nvim-ufo` | 38 | 39 | 40 | 41 | The requirements may look detailed, but the plugin works mostly out-of-the-box. 42 | If you are on nvim 0.11+ and do not have `nvim-ufo` installed, all features 43 | except `autoFold` and `keepFoldsAcrossSessions` are enabled by default and work 44 | without any need for additional configuration. 45 | 46 | With nvim 0.11+, `nvim-origami` is able to replace most of `nvim-ufo`'s feature 47 | set in a much more lightweight way. 48 | 49 | ## Installation 50 | 51 | ```lua 52 | -- lazy.nvim 53 | { 54 | "chrisgrieser/nvim-origami", 55 | event = "VeryLazy", 56 | opts = {}, -- needed even when using default config 57 | 58 | -- recommended: disable vim's auto-folding 59 | init = function() 60 | vim.opt.foldlevel = 99 61 | vim.opt.foldlevelstart = 99 62 | end, 63 | }, 64 | ``` 65 | 66 | ## Configuration 67 | 68 | ```lua 69 | -- default settings 70 | require("origami").setup { 71 | -- features incompatible with `nvim-ufo` 72 | useLspFoldsWithTreesitterFallback = not package.loaded["ufo"], 73 | autoFold = { 74 | enabled = false, 75 | kinds = { "comment", "imports" }, ---@type lsp.FoldingRangeKind[] 76 | }, 77 | foldtextWithLineCount = { 78 | enabled = not package.loaded["ufo"], 79 | template = " %s lines", -- `%s` gets the number of folded lines 80 | hlgroupForCount = "Comment", 81 | }, 82 | 83 | -- can be used with or without `nvim-ufo` 84 | pauseFoldsOnSearch = true, 85 | foldKeymaps = { 86 | setup = true, -- modifies `h` and `l` 87 | hOnlyOpensOnFirstColumn = false, 88 | }, 89 | 90 | -- features requiring `nvim-ufo` 91 | keepFoldsAcrossSessions = package.loaded["ufo"], 92 | } 93 | ``` 94 | 95 | If you use other keys than `h` and `l` for vertical movement, set 96 | `opts.foldKeymaps.setup = false` and map the keys yourself: 97 | 98 | ```lua 99 | vim.keymap.set("n", "", function() require("origami").h() end) 100 | vim.keymap.set("n", "", function() require("origami").l() end) 101 | ``` 102 | 103 | ## FAQ 104 | 105 | ### Folds are still opened 106 | [Many formatting plugins open all your 107 | folds](https://www.reddit.com/r/neovim/comments/164gg5v/preserve_folds_when_formatting/) 108 | and unfortunately, there is nothing this plugin can do about it. The only two 109 | tools I am aware of that are able to preserve folds are the 110 | [efm-language-server](https://github.com/mattn/efm-langserver) and 111 | [conform.nvim](https://github.com/stevearc/conform.nvim). 112 | 113 | ### Debug folding issues 114 | 115 | ```lua 116 | -- Folds provided by the LSP 117 | require("origami").inspectLspFolds("special") -- comment & import only 118 | require("origami").inspectLspFolds("all") 119 | ``` 120 | 121 | ## Credits 122 | - [@magnusriga](https://github.com/neovim/neovim/pull/27217#issuecomment-2631614344) 123 | for the more performant implementation of fold text with highlighting. 124 | 125 | ## About the developer 126 | In my day job, I am a sociologist studying the social mechanisms underlying the 127 | digital economy. For my PhD project, I investigate the governance of the app 128 | economy and how software ecosystems manage the tension between innovation and 129 | compatibility. If you are interested in this subject, feel free to get in touch. 130 | 131 | - [Website](https://chris-grieser.de/) 132 | - [Mastodon](https://pkm.social/@pseudometa) 133 | - [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser) 134 | - [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/) 135 | 136 | Buy Me a Coffee at ko-fi.com 139 | 140 | [^1]: Technically, unfolding with `l` is already a built-in vim feature when 141 | `vim.opt.foldopen` includes `hor`. However, this plugin still sets up a `l` 142 | key replicating that behavior, since the built-in version still moves you to 143 | one character to the side, which can be considered a bit counterintuitive. 144 | -------------------------------------------------------------------------------- /doc/nvim-origami.txt: -------------------------------------------------------------------------------- 1 | *nvim-origami.txt* For Neovim Last change: 2025 May 19 2 | 3 | ============================================================================== 4 | Table of Contents *nvim-origami-table-of-contents* 5 | 6 | 1. nvim-origami |nvim-origami-nvim-origami-| 7 | - Table of Content |nvim-origami-nvim-origami--table-of-content| 8 | - Features |nvim-origami-nvim-origami--features| 9 | - Installation |nvim-origami-nvim-origami--installation| 10 | - Configuration |nvim-origami-nvim-origami--configuration| 11 | - FAQ |nvim-origami-nvim-origami--faq| 12 | - Credits |nvim-origami-nvim-origami--credits| 13 | - About the developer |nvim-origami-nvim-origami--about-the-developer| 14 | 2. Links |nvim-origami-links| 15 | 16 | ============================================================================== 17 | 1. nvim-origami *nvim-origami-nvim-origami-* 18 | 19 | 20 | 21 | _Foldwith relentless elegance._ A collection of Quality-of-life features 22 | related to folding. 23 | 24 | 25 | 26 | 27 | TABLE OF CONTENT *nvim-origami-nvim-origami--table-of-content* 28 | 29 | - |nvim-origami-features| 30 | - |nvim-origami-installation| 31 | - |nvim-origami-configuration| 32 | - |nvim-origami-faq| 33 | - |nvim-origami-folds-are-still-opened| 34 | - |nvim-origami-debug-folding-issues| 35 | - |nvim-origami-credits| 36 | - |nvim-origami-about-the-developer| 37 | 38 | 39 | FEATURES *nvim-origami-nvim-origami--features* 40 | 41 | ------------------------------------------------------------------------------------------------------ 42 | opts description requirements 43 | ------------------------------------ -------------------------------------------- -------------------- 44 | .useLspFoldsWithTreesitterFallback Use the LSP to provide folds, with 1. not using 45 | Treesitter as fallback if the LSP does not nvim-ufo (feature is 46 | provide folding info. redundant with 47 | nvim-ufo)2. Nvim 48 | 0.11 49 | 50 | .foldKeymaps Overload the h key which will fold a line — 51 | when used one the first non-blank character 52 | of (or before). And overload the l key, 53 | which will unfold a line when used on a 54 | folded line.[1] This allows you to ditch zc, 55 | zo, and za, h and l are all you need. 56 | 57 | .autoFold Automatically fold comments and/or imports 1. not using 58 | when opening a file. nvim-ufo (feature is 59 | redundant with 60 | nvim-ufo)2. nvim 61 | 0.113. LSP that 62 | provides fold 63 | information 64 | 65 | .foldtextWithLineCount Add line count to the foldtext, preserving 1. not using 66 | the syntax highlighting of the line. nvim-ufo (feature is 67 | redundant with 68 | nvim-ufo)2. 69 | Treesitter parser 70 | for the language. 71 | 72 | .pauseFoldsOnSearch Pause folds while searching, restore folds — 73 | when done with searching. (Normally, folds 74 | are opened when you search for text inside 75 | them, and stay open afterward.) 76 | 77 | .keepFoldsAcrossSessions Remember folds across sessions. nvim-ufo 78 | ------------------------------------------------------------------------------------------------------ 79 | 80 | [1] Technically, unfolding with l is already a built-in vim feature when 81 | vim.opt.foldopen includes hor. However, this plugin still sets up a l 82 | key replicating that behavior, since the built-in version still moves 83 | you to one character to the side, which can be considered a bit 84 | counterintuitive. 85 | The requirements may look detailed, but the plugin works mostly out-of-the-box. 86 | If you are on nvim 0.11+ and do not have `nvim-ufo` installed, all features 87 | except `autoFold` and `keepFoldsAcrossSessions` are enabled by default and work 88 | without any need for additional configuration. 89 | 90 | With nvim 0.11+, `nvim-origami` is able to replace most of `nvim-ufo`’s 91 | feature set in a much more lightweight way. 92 | 93 | 94 | INSTALLATION *nvim-origami-nvim-origami--installation* 95 | 96 | >lua 97 | -- lazy.nvim 98 | { 99 | "chrisgrieser/nvim-origami", 100 | event = "VeryLazy", 101 | opts = {}, -- needed even when using default config 102 | 103 | -- recommended: disable vim's auto-folding 104 | init = function() 105 | vim.opt.foldlevel = 99 106 | vim.opt.foldlevelstart = 99 107 | end, 108 | }, 109 | < 110 | 111 | 112 | CONFIGURATION *nvim-origami-nvim-origami--configuration* 113 | 114 | >lua 115 | -- default settings 116 | require("origami").setup { 117 | -- features incompatible with `nvim-ufo` 118 | useLspFoldsWithTreesitterFallback = not package.loaded["ufo"], 119 | autoFold = { 120 | enabled = false, 121 | kinds = { "comment", "imports" }, ---@type lsp.FoldingRangeKind[] 122 | }, 123 | foldtextWithLineCount = { 124 | enabled = not package.loaded["ufo"], 125 | template = " %s lines", -- `%s` gets the number of folded lines 126 | hlgroupForCount = "Comment", 127 | }, 128 | 129 | -- can be used with or without `nvim-ufo` 130 | pauseFoldsOnSearch = true, 131 | foldKeymaps = { 132 | setup = true, -- modifies `h` and `l` 133 | hOnlyOpensOnFirstColumn = false, 134 | }, 135 | 136 | -- features requiring `nvim-ufo` 137 | keepFoldsAcrossSessions = package.loaded["ufo"], 138 | } 139 | < 140 | 141 | If you use other keys than `h` and `l` for vertical movement, set 142 | `opts.foldKeymaps.setup = false` and map the keys yourself: 143 | 144 | >lua 145 | vim.keymap.set("n", "", function() require("origami").h() end) 146 | vim.keymap.set("n", "", function() require("origami").l() end) 147 | < 148 | 149 | 150 | FAQ *nvim-origami-nvim-origami--faq* 151 | 152 | 153 | FOLDS ARE STILL OPENED ~ 154 | 155 | Many formatting plugins open all your folds 156 | 157 | and unfortunately, there is nothing this plugin can do about it. The only two 158 | tools I am aware of that are able to preserve folds are the efm-language-server 159 | and conform.nvim 160 | . 161 | 162 | 163 | DEBUG FOLDING ISSUES ~ 164 | 165 | >lua 166 | -- Folds provided by the LSP 167 | require("origami").inspectLspFolds("special") -- comment & import only 168 | require("origami").inspectLspFolds("all") 169 | < 170 | 171 | 172 | CREDITS *nvim-origami-nvim-origami--credits* 173 | 174 | - @magnusriga 175 | for the more performant implementation of fold text with highlighting. 176 | 177 | 178 | ABOUT THE DEVELOPER *nvim-origami-nvim-origami--about-the-developer* 179 | 180 | In my day job, I am a sociologist studying the social mechanisms underlying the 181 | digital economy. For my PhD project, I investigate the governance of the app 182 | economy and how software ecosystems manage the tension between innovation and 183 | compatibility. If you are interested in this subject, feel free to get in 184 | touch. 185 | 186 | - Website 187 | - Mastodon 188 | - ResearchGate 189 | - LinkedIn 190 | 191 | 192 | 193 | ============================================================================== 194 | 2. Links *nvim-origami-links* 195 | 196 | 1. *@magnusriga*: 197 | 198 | Generated by panvimdoc 199 | 200 | vim:tw=78:ts=8:noet:ft=help:norl: 201 | -------------------------------------------------------------------------------- /lua/origami/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | ---@class Origami.config 5 | local defaultConfig = { 6 | -- features incompatible with `nvim-ufo` 7 | useLspFoldsWithTreesitterFallback = not package.loaded["ufo"], 8 | autoFold = { 9 | enabled = false, 10 | kinds = { "comment", "imports" }, ---@type lsp.FoldingRangeKind[] 11 | }, 12 | foldtextWithLineCount = { 13 | enabled = not package.loaded["ufo"], 14 | template = " %s lines", -- `%s` gets the number of folded lines 15 | hlgroupForCount = "Comment", 16 | }, 17 | 18 | -- can be used with or without `nvim-ufo` 19 | pauseFoldsOnSearch = true, 20 | foldKeymaps = { 21 | setup = true, -- modifies `h` and `l` 22 | hOnlyOpensOnFirstColumn = false, 23 | }, 24 | 25 | -- features requiring `nvim-ufo` 26 | keepFoldsAcrossSessions = package.loaded["ufo"], 27 | } 28 | M.config = defaultConfig 29 | 30 | -------------------------------------------------------------------------------- 31 | 32 | ---@param userConfig? Origami.config 33 | function M.setup(userConfig) 34 | M.config = vim.tbl_deep_extend("force", defaultConfig, userConfig or {}) 35 | 36 | if M.config.keepFoldsAcrossSessions then require("origami.features.remember-folds") end 37 | if M.config.pauseFoldsOnSearch then require("origami.features.pause-folds-on-search") end 38 | if M.config.foldtextWithLineCount.enabled then require("origami.features.foldtext") end 39 | if M.config.autoFold.enabled then require("origami.features.autofold-comments-imports") end 40 | if M.config.useLspFoldsWithTreesitterFallback then 41 | require("origami.features.lsp-and-treesitter-foldexpr") 42 | end 43 | 44 | -- DEPRECATION (2025-03-30) 45 | ---@diagnostic disable: undefined-field 46 | local u = require("origami.utils") 47 | if M.config.setupFoldKeymaps then 48 | u.warn("nvim-origami config `setupFoldKeymaps` was moved to `foldKeymaps.setup`.") 49 | M.config.foldKeymaps.setup = M.config.setupFoldKeymaps 50 | end 51 | if M.config.hOnlyOpensOnFirstColumn then 52 | u.warn( 53 | "nvim-origami config `hOnlyOpensOnFirstColumn` was moved to `foldKeymaps.hOnlyOpensOnFirstColumn`." 54 | ) 55 | M.config.foldKeymaps.hOnlyOpensOnFirstColumn = M.config.hOnlyOpensOnFirstColumn 56 | end 57 | ---@diagnostic enable: undefined-field 58 | 59 | if M.config.foldKeymaps.setup then 60 | vim.keymap.set( 61 | "n", 62 | "h", 63 | function() require("origami.features.fold-keymaps").h() end, 64 | { desc = "Origami h" } 65 | ) 66 | vim.keymap.set( 67 | "n", 68 | "l", 69 | function() require("origami.features.fold-keymaps").l() end, 70 | { desc = "Origami l" } 71 | ) 72 | end 73 | end 74 | 75 | -------------------------------------------------------------------------------- 76 | return M 77 | -------------------------------------------------------------------------------- /lua/origami/features/autofold-comments-imports.lua: -------------------------------------------------------------------------------- 1 | if package.loaded["ufo"] then 2 | require("origami.utils").warn( 3 | "nvim-origami's `foldtextWithLineCount` cannot be used at the same time as `nvim-ufo`." 4 | ) 5 | return 6 | end 7 | if not vim.lsp.foldclose then return end -- only added in nvim 0.11 8 | -------------------------------------------------------------------------------- 9 | 10 | vim.api.nvim_create_autocmd("LspNotify", { 11 | desc = "Origami: Close imports and comments on load", 12 | group = vim.api.nvim_create_augroup("origami-autofolds", { clear = true }), 13 | callback = function(ctx) 14 | if ctx.data.method ~= "textDocument/didOpen" then return end 15 | if vim.bo[ctx.buf].buftype ~= "" or not vim.api.nvim_buf_is_valid(ctx.buf) then return end 16 | 17 | -- not using `lsp.get_clients_by_id` to additionally check for the correct 18 | -- buffer (can change in quick events) 19 | local client = vim.lsp.get_clients({ bufnr = ctx.buf, id = ctx.data.client_id })[1] 20 | if not client then return end 21 | if not client:supports_method("textDocument/foldingRange") then return end 22 | 23 | local kinds = require("origami.config").config.autoFold.kinds 24 | local winid = vim.fn.bufwinid(ctx.buf) 25 | if not winid or not vim.api.nvim_win_is_valid(winid) then return end 26 | for _, kind in ipairs(kinds) do 27 | pcall(vim.lsp.foldclose, kind, winid) 28 | end 29 | 30 | -- unfold under cursor 31 | vim.schedule(function() vim.cmd.normal { "zv", bang = true } end) 32 | end, 33 | }) 34 | -------------------------------------------------------------------------------- /lua/origami/features/fold-keymaps.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | local function normal(cmdStr) vim.cmd.normal { cmdStr, bang = true } end 5 | 6 | -- `h` closes folds when at the beginning of a line. 7 | function M.h() 8 | local config = require("origami.config").config 9 | local count = vim.v.count1 -- saved as `normal` affects it 10 | for _ = 1, count, 1 do 11 | local col = vim.api.nvim_win_get_cursor(0)[2] 12 | local textBeforeCursor = vim.api.nvim_get_current_line():sub(1, col) 13 | local onIndentOrFirstNonBlank = textBeforeCursor:match("^%s*$") 14 | and not config.foldKeymaps.hOnlyOpensOnFirstColumn 15 | local firstChar = col == 0 and config.foldKeymaps.hOnlyOpensOnFirstColumn 16 | if onIndentOrFirstNonBlank or firstChar then 17 | local wasFolded = pcall(normal, "zc") 18 | if not wasFolded then normal("h") end 19 | else 20 | normal("h") 21 | end 22 | end 23 | end 24 | 25 | -- `l` on a folded line opens the fold. 26 | function M.l() 27 | local count = vim.v.count1 -- count needs to be saved due to `normal` affecting it 28 | for _ = 1, count, 1 do 29 | local isOnFold = vim.fn.foldclosed(".") > -1 ---@diagnostic disable-line: param-type-mismatch 30 | local action = isOnFold and "zo" or "l" 31 | pcall(normal, action) 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- 36 | return M 37 | -------------------------------------------------------------------------------- /lua/origami/features/foldtext.lua: -------------------------------------------------------------------------------- 1 | if package.loaded["ufo"] then 2 | require("origami.utils").warn( 3 | "nvim-origami's `foldtextWithLineCount` cannot be used at the same time as `nvim-ufo`." 4 | ) 5 | return 6 | end 7 | -------------------------------------------------------------------------------- 8 | 9 | -- Credits for this function go to @magnusriga ([1], similar: [3]). As opposed 10 | -- to other implementations that iterate every character of a folded line(e.g., 11 | -- [2]), this approach only iterates captures, making it more performant. (The 12 | -- performance difference is already noticeable as soon as there are many closed 13 | -- folds in a file.) 14 | -- [1]: https://github.com/neovim/neovim/pull/27217#issuecomment-2631614344 15 | -- [2]: https://www.reddit.com/r/neovim/comments/1fzn1zt/custom_fold_text_function_with_treesitter_syntax/ 16 | -- [3]: https://github.com/Wansmer/nvim-config/blob/6967fe34695972441d63173d5458a4be74a4ba42/lua/modules/foldtext.lua 17 | ---@param foldStart number 18 | ---@return { text: string, hlgroup: string }[]|string 19 | local function foldtextWithTSHighlights(foldStart) 20 | local foldLine = vim.api.nvim_buf_get_lines(0, foldStart - 1, foldStart, false)[1] 21 | 22 | local lang = vim.treesitter.language.get_lang(vim.bo.filetype) 23 | local ok, parser = pcall(vim.treesitter.get_parser, 0, lang) 24 | if not ok or not parser then return vim.fn.foldtext() end -- fallback 25 | 26 | -- Get `highlights` query for current buffer parser, as table from file, 27 | -- which gives information on highlights of tree nodes produced by parser. 28 | local query = vim.treesitter.query.get(parser:lang(), "highlights") 29 | if not query then return vim.fn.foldtext() end 30 | 31 | -- Partial TSTree for buffer, including root TSNode, and TSNodes of folded line. 32 | -- PERF Only parsing needed range, as parsing whole file would be slower. 33 | local tree = parser:parse({ foldStart - 1, foldStart })[1] 34 | 35 | local result = {} 36 | local linePos = 0 37 | local prevRange = { 0, 0 } 38 | 39 | -- Loop through matched "captures", i.e. node-to-capture-group pairs, for 40 | -- each TSNode in given range. Each TSNode could occur several times in list, 41 | -- i.e., map to several capture groups, and each capture group could be used 42 | -- by several TSNodes. 43 | for id, node, _ in query:iter_captures(tree:root(), 0, foldStart - 1, foldStart) do 44 | local captureName = query.captures[id] 45 | local text = vim.treesitter.get_node_text(node, 0) 46 | text = text:gsub("[\n\r].*", "") -- account for multiline captures 47 | local _, startCol, _, endCol = node:range() 48 | 49 | -- include whitespace (part between captured TSNodes) with arbitrary hlgroup 50 | if startCol > linePos then 51 | table.insert(result, { foldLine:sub(linePos + 1, startCol), "Folded" }) 52 | end 53 | -- Move `linePos` to end column of current node, so next loop iteration 54 | -- includes whitespace between TSNodes. 55 | linePos = endCol 56 | 57 | if not endCol or not startCol then break end 58 | 59 | -- Save code range current TSNode spans, so current TSNode can be ignored 60 | -- if next capture is for TSNode covering same section of source code. 61 | local range = { startCol, endCol } 62 | 63 | -- Use language specific highlight, if it exists. 64 | local highlight = "@" .. captureName 65 | local highlightLang = highlight .. "." .. lang 66 | if vim.fn.hlexists(highlightLang) then highlight = highlightLang end 67 | 68 | -- Accumulate text + hlgroup 69 | if range[1] == prevRange[1] and range[2] == prevRange[2] then 70 | -- Overwrite previous capture, as it was for same range from source code. 71 | result[#result] = { text, highlight } 72 | else 73 | -- Insert capture for TSNode covering new range of source code. 74 | table.insert(result, { text, highlight }) 75 | prevRange = range 76 | end 77 | end 78 | 79 | return result 80 | end 81 | 82 | local function foldtextWithLineCount() 83 | local foldtextChunks = foldtextWithTSHighlights(vim.v.foldstart) 84 | -- GUARD `vim.fn.foldtext()` fallback already has count 85 | if type(foldtextChunks) == "string" then return foldtextChunks end 86 | 87 | local config = require("origami.config").config.foldtextWithLineCount 88 | local lineCountText = config.template:format(vim.v.foldend - vim.v.foldstart) 89 | 90 | table.insert(foldtextChunks, { lineCountText, config.hlgroupForCount }) 91 | return foldtextChunks 92 | end 93 | 94 | vim.opt.foldtext = "v:lua.require('origami.features.foldtext').get()" 95 | vim.opt.fillchars:append { fold = " " } -- text after end of foldtext 96 | 97 | -------------------------------------------------------------------------------- 98 | return { get = foldtextWithLineCount } 99 | -------------------------------------------------------------------------------- /lua/origami/features/inspect-folds.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local u = require("origami.utils") 3 | -------------------------------------------------------------------------------- 4 | 5 | ---@param type "special"|"all"? 6 | function M.inspectLspFolds(type) 7 | if not type then type = "all" end 8 | local bufnr = vim.api.nvim_get_current_buf() 9 | 10 | local foldingLsp = 11 | vim.lsp.get_clients({ bufnr = bufnr, method = "textDocument/foldingRange" })[1] 12 | if not foldingLsp then 13 | u.warn("No LSPs with folding support attached.") 14 | return 15 | end 16 | 17 | local params = { textDocument = { uri = vim.uri_from_bufnr(bufnr) } } 18 | foldingLsp:request("textDocument/foldingRange", params, function(err, result, _) 19 | if err or not result then 20 | local msg = ("[%s] Failed to get folding ranges. "):format(foldingLsp.name) 21 | if err then msg = msg .. err.message end 22 | u.warn(msg) 23 | return 24 | end 25 | local specialFolds = vim.iter(result) 26 | :filter(function(fold) 27 | if type == "all" then return true end 28 | return fold.kind ~= nil and fold.kind ~= "region" 29 | end) 30 | :map(function(fold) 31 | local range = fold.startLine + 1 32 | if fold.endLine > fold.startLine then range = range .. "-" .. (fold.endLine + 1) end 33 | return ("- %s %s"):format(range, fold.kind or "") 34 | end) 35 | :join("\n") 36 | 37 | if specialFolds == "" then 38 | u.info(("[%s] No special folds found."):format(foldingLsp.name)) 39 | else 40 | local header = ("[%s: special folds]"):format(foldingLsp.name) 41 | u.info(header .. "\n" .. specialFolds) 42 | end 43 | end) 44 | end 45 | 46 | -------------------------------------------------------------------------------- 47 | return M 48 | -------------------------------------------------------------------------------- /lua/origami/features/lsp-and-treesitter-foldexpr.lua: -------------------------------------------------------------------------------- 1 | if package.loaded["ufo"] then 2 | require("origami.utils").warn( 3 | "nvim-origami's `useLspFoldsWithTreesitterFallback` cannot be used at the same time as `nvim-ufo`." 4 | ) 5 | return 6 | end 7 | if not vim.lsp.foldexpr then return end -- only added in nvim 0.11 8 | -------------------------------------------------------------------------------- 9 | 10 | vim.opt.foldmethod = "expr" 11 | vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()" -- fallback 12 | 13 | vim.api.nvim_create_autocmd("LspAttach", { 14 | desc = "Origami: Set LSP folding if client supports it", 15 | callback = function(ctx) 16 | local client = assert(vim.lsp.get_client_by_id(ctx.data.client_id)) 17 | if client:supports_method("textDocument/foldingRange") then 18 | local win = vim.api.nvim_get_current_win() 19 | vim.wo[win][0].foldexpr = "v:lua.vim.lsp.foldexpr()" 20 | end 21 | end, 22 | }) 23 | -------------------------------------------------------------------------------- /lua/origami/features/pause-folds-on-search.lua: -------------------------------------------------------------------------------- 1 | -- Disabling search in foldopen has the disadvantage of making search nearly 2 | -- unusable. Enabling search in foldopen has the disadvantage of constantly 3 | -- opening all your folds as soon as you search. This snippet fixes this by 4 | -- pausing folds while searching, but restoring them when you are done 5 | -- searching. 6 | -------------------------------------------------------------------------------- 7 | 8 | -- disable auto-open when searching, since we take care of that in a better way 9 | vim.opt.foldopen:remove { "search" } 10 | 11 | local ns = vim.api.nvim_create_namespace("auto_pause_folds") 12 | 13 | vim.on_key(function(char) 14 | if vim.g.scrollview_refreshing then return end -- FIX https://github.com/dstein64/nvim-scrollview/issues/88#issuecomment-1570400161 15 | local key = vim.fn.keytrans(char) 16 | local isCmdlineSearch = vim.fn.getcmdtype():find("[/?]") ~= nil 17 | local isNormalMode = vim.api.nvim_get_mode().mode == "n" 18 | 19 | local searchStarted = (key == "/" or key == "?") and isNormalMode 20 | local searchConfirmed = (key == "" and isCmdlineSearch) 21 | local searchCancelled = (key == "" and isCmdlineSearch) 22 | if not (searchStarted or searchConfirmed or searchCancelled or isNormalMode) then return end 23 | local foldsArePaused = not (vim.opt.foldenable:get()) 24 | -- works for RHS, therefore no need to consider remaps 25 | local searchMovement = vim.tbl_contains({ "n", "N", "*", "#" }, key) 26 | 27 | local pauseFold = (searchConfirmed or searchStarted or searchMovement) and not foldsArePaused 28 | local unpauseFold = foldsArePaused and (searchCancelled or not searchMovement) 29 | if pauseFold then 30 | vim.opt_local.foldenable = false 31 | elseif unpauseFold then 32 | vim.opt_local.foldenable = true 33 | pcall(vim.cmd.foldopen, { bang = true }) -- after closing folds, keep the *current* fold open 34 | end 35 | end, ns) 36 | -------------------------------------------------------------------------------- /lua/origami/features/remember-folds.lua: -------------------------------------------------------------------------------- 1 | if not package.loaded["ufo"] then 2 | require("origami.utils").warn("nvim-origami's `keepFoldsAcrossSessions` requires `nvim-ufo`.") 3 | return 4 | end 5 | --[[ INFO 6 | `nvim-ufo` uses some hack to save the fold information in form of manual folds, 7 | thus storing fold information in viewfiles saves by `mkview`. Using the LSP 8 | foldexpression without ufo results in those information not being stored, so 9 | that folds are only available after the LSP has finished attaching to the 10 | buffer, which is too late for `loadview`. (`loadview` would only work 2s after 11 | opening a file, resulting in too glitchy behavior.) 12 | 13 | Attempts to manually insert fold information retrieved from an LSP into the 14 | viewfile proved not to work reliably. Thus, this implementation of fold-saving 15 | pretty much only works with `nvim-ufo`. 16 | --]] 17 | -------------------------------------------------------------------------------- 18 | 19 | local VIEW_SLOT = 1 20 | 21 | local function remember(mode) 22 | if vim.bo.buftype ~= "" or not vim.bo.modifiable then return end 23 | 24 | if mode == "save" then 25 | -- only save folds and cursor, do not save options or the cwd, as that 26 | -- leads to unpredictable behavior 27 | local viewOptsBefore = vim.opt.viewoptions:get() 28 | vim.opt.viewoptions = { "cursor", "folds" } 29 | pcall(vim.cmd.mkview, VIEW_SLOT) -- pcall for edge cases like #11 30 | vim.opt.viewoptions = viewOptsBefore 31 | else 32 | pcall(vim.cmd.loadview, VIEW_SLOT) -- pcall, since new files have no viewfile 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- 37 | 38 | local group = vim.api.nvim_create_augroup("origami-keep-folds", { clear = true }) 39 | 40 | vim.api.nvim_create_autocmd("BufWinLeave", { 41 | pattern = "?*", 42 | desc = "Origami: save folds", 43 | callback = function() remember("save") end, 44 | group = group, 45 | }) 46 | 47 | vim.api.nvim_create_autocmd("BufWinEnter", { 48 | pattern = "?*", 49 | desc = "Origami: load folds", 50 | callback = function() remember("load") end, 51 | group = group, 52 | }) 53 | remember("load") -- initialize in current buffer in case of lazy loading 54 | -------------------------------------------------------------------------------- /lua/origami/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | ---@param userConfig? Origami.config 5 | function M.setup(userConfig) require("origami.config").setup(userConfig) end 6 | 7 | -- make keymaps accessible from `require("origami")` for easier remapping 8 | function M.l() require("origami.features.fold-keymaps").l() end 9 | function M.h() require("origami.features.fold-keymaps").h() end 10 | 11 | ---@param type "special"|"all"? 12 | function M.inspectLspFolds(type) require("origami.features.inspect-folds").inspectLspFolds(type) end 13 | 14 | -------------------------------------------------------------------------------- 15 | return M 16 | -------------------------------------------------------------------------------- /lua/origami/reference.md: -------------------------------------------------------------------------------- 1 | ## How to get fold info from an LSP 2 | 3 | ```lua 4 | local bufnr = 0 5 | 6 | local foldingLsp = vim.lsp.get_clients({ bufnr = bufnr, method = "textDocument/foldingRange" })[1] 7 | if not foldingLsp then return end 8 | 9 | local params = { textDocument = { uri = vim.uri_from_bufnr(bufnr) } } 10 | foldingLsp:request("textDocument/foldingRange", params, function(err, result, _) 11 | if err then 12 | local msg = ("Failed to get folding ranges from %s: %s"):format(foldingLsp.name, err) 13 | vim.notify(msg, vim.log.levels.ERROR) 14 | return 15 | end 16 | vim.notify(vim.inspect(result)) 17 | end) 18 | ``` 19 | -------------------------------------------------------------------------------- /lua/origami/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | ---@param msg string 5 | function M.warn(msg) vim.notify(msg, vim.log.levels.WARN, { title = "origami", icon = "" }) end 6 | 7 | function M.info(msg) vim.notify(msg, nil, { title = "origami", icon = "" }) end 8 | 9 | -------------------------------------------------------------------------------- 10 | return M 11 | --------------------------------------------------------------------------------