├── .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 |
6 |
7 | *Fold with relentless elegance.* A collection of Quality-of-life features
8 | related to folding.
9 |
10 |
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 |
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 |
--------------------------------------------------------------------------------