├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── support_draft.md └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo.gif └── lua └── comment-hide ├── commands.lua ├── init.lua └── utils.lua /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug Report' 3 | about: Report a reproducible bug. 4 | title: 'Bug: ' 5 | labels: 'Status: Unconfirmed' 6 | --- 7 | 8 | ## Expected Behavior 9 | 10 | [//]: # '[Describe what you expected to happen.]' 11 | 12 | ## Actual Behavior 13 | 14 | [//]: # '[Describe what actually happened.]' 15 | 16 | ## Environment 17 | 18 | [//]: # '- Neovim version:' 19 | 20 | ## Possible Solution 21 | 22 | [//]: # '[If you have any ideas on how to fix or address the issue, please share them here.]' 23 | 24 | - [x] : **Do you fix this error?(YOU ARE SO GOOD!)** 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support_draft.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Support draft' 3 | about: Support your like lang. 4 | title: 'Support: ' 5 | labels: 'enhancement' 6 | --- 7 | 8 | ## Support lang 9 | 10 | [//]: # '[ex: Javascript]' 11 | 12 | ## Are you willing to finish it? 13 | 14 | - [x] : **YES! (YOU ARE SO GOOD!)** 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: Release 6 | jobs: 7 | release-please: 8 | name: Release Please 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: GoogleCloudPlatform/release-please-action@v3 13 | id: release 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | release-type: node 17 | package-name: standard-version 18 | changelog-types: '[{"type": "types", "section":"Types", "hidden": false},{"type": "revert", "section":"Reverts", "hidden": false},{"type": "feat", "section": "Features", "hidden": false},{"type": "fix", "section": "Bug Fixes", "hidden": false},{"type": "docs", "section":"Docs", "hidden": false},{"type": "style", "section":"Styling", "hidden": false},{"type": "refactor", "section":"Code Refactoring", "hidden": false},{"type": "perf", "section":"Performance Improvements", "hidden": false},{"type": "test", "section":"Tests", "hidden": false},{"type": "build", "section":"Build System", "hidden": false},{"type": "ci", "section":"CI", "hidden":false}]' 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .annotations 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.2.0](https://github.com/jiangxue-analysis/nvim.comment-hide/compare/v1.1.1...v1.2.0) (2025-05-21) 4 | 5 | 6 | ### Features 7 | 8 | * basic support for erlang and elixir [#8](https://github.com/jiangxue-analysis/nvim.comment-hide/issues/8) ([1897835](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/18978355f644765a76fed2710ccdb5e6863b325c)) 9 | 10 | 11 | ### CI 12 | 13 | * add support lang template ([7c317f7](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/7c317f78b2386fe0d74eef5f3a78f12856eb9b47)) 14 | 15 | ## [1.1.1](https://github.com/jiangxue-analysis/nvim.comment-hide/compare/v1.1.0...v1.1.1) (2025-05-16) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * bash and shell file support and fix [#5](https://github.com/jiangxue-analysis/nvim.comment-hide/issues/5) ([01f7640](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/01f7640ecb933ae68f2d76164aecc4ae2592ea59)) 21 | 22 | ## [1.1.0](https://github.com/jiangxue-analysis/nvim.comment-hide/compare/v1.0.0...v1.1.0) (2025-05-10) 23 | 24 | 25 | ### Features 26 | 27 | * add gitignore config auto .annotations/ to .gitignore [#3](https://github.com/jiangxue-analysis/nvim.comment-hide/issues/3) ([1a15ad8](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/1a15ad8c68b1fb49bddb2b2bfa5aceda0a0b6eef)) 28 | 29 | 30 | ### Docs 31 | 32 | * add gitignore config desc ([54fa7c4](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/54fa7c4f5f1eb5a3eb1727da2a8f2d3c8e0601b7)) 33 | * typo ([1b84503](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/1b845037e9bfd31ff8f61daa2d0d85f127aeb20b)) 34 | 35 | ## 1.0.0 (2025-05-07) 36 | 37 | 38 | ### Features 39 | 40 | * add docs and workflow ([e142077](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/e14207738542bfcd1e75d696e0f4cfbce088abe0)) 41 | * add support scala lang ([dfefd91](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/dfefd9189ffa1560d4dbf6cbcbf9aac9b6907ebf)) 42 | * project init ([360c164](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/360c16460aeb3880933493d9a1dbef121a9ba67f)) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * `` style code error hide ([464647b](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/464647b5cfbea3f76c5fb94142dd43d67e2e6d8a)) 48 | * python ' and " comment no hide error ([a6c840f](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/a6c840fe6b4e5030dd6f5de99d2eb82cb316a742)) 49 | * ruby and outher lang error ([f6efab9](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/f6efab9842fa70411199a542626af6e8101fbdb7)) 50 | 51 | 52 | ### Docs 53 | 54 | * add scala ([8f4b738](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/8f4b73867f4d2aa47257e3d5e17730a0798d4a0c)) 55 | * add test lang comment info ([2f53894](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/2f538940dbc74adbb624a7cf8d82394c0fd04914)) 56 | * add video ([3341e88](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/3341e88c6204ff332a06aa420eac76bace574c22)) 57 | * del logo ([51c45da](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/51c45dab65a7f88789d90a55dae2d89e34e294a2)) 58 | * del video use gif ([cf5e76e](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/cf5e76e9a654a027ca209d949d997e3b9b213f45)) 59 | * fix error markdown ([f9a7c18](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/f9a7c1888ec70593d3034c2c8f8a15238dc8cbfa)) 60 | * fix key error command ([a095c7a](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/a095c7ac5e2cd072c6e7c352ce345b753ff09d3f)) 61 | * upgrate install style ([94a7cc2](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/94a7cc2fbb46e3b317c671aaec397bde2c3d794b)) 62 | * upgrate README.md ([7a772b0](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/7a772b085e7fc2417d0df016b2011f0579ab15ae)) 63 | * use mp4 ([d9675af](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/d9675afcf412ce7616d7d4f241a984e29080cde9)) 64 | 65 | 66 | ### Code Refactoring 67 | 68 | * very good code layout ([9a2ba38](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/9a2ba38f1677fb9ffd759679cfd9a98c463bc4e7)) 69 | 70 | 71 | ### CI 72 | 73 | * add issue template ([6947b08](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/6947b08480bc7df41023f56f9a9a989f6c40c9c2)) 74 | * fix bug_report template typo ([261fd48](https://github.com/jiangxue-analysis/nvim.comment-hide/commit/261fd488ee6af6606e7ea584c8cc4d7d97b7204a)) 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 jiangxue-analysis 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 | ### nvim.command-hide 2 | 3 | The plugin allows you to hide and show comments, and saves them to a specified folder. 4 | 5 | ![](demo.gif) 6 | 7 | #### Why install? 8 | 9 | > [!NOTE] 10 | > This is test version, if error and bug, click [issues](https://github.com/jiangxue-analysis/nvim.comment-hide/issues). 11 | 12 | You are use [lazy.nvim](https://github.com/folke/lazy.nvim): 13 | 14 | ```lua 15 | 16 | return { 17 | "jiangxue-analysis/nvim.comment-hide", 18 | name = "comment-hide", 19 | lazy = false, 20 | config = function() 21 | require("comment-hide").setup({ 22 | gitignore = true, -- Automatically add .annotations/ to .gitignore. 23 | }) 24 | vim.keymap.set("n", "vs", "CommentHideSave", { desc = "Comment: Save (strip comments)" }) 25 | vim.keymap.set("n", "vr", "CommentHideRestore", { desc = "Comment: Restore from backup" }) 26 | end, 27 | } 28 | ``` 29 | 30 | If you not user [lazy.nvim](https://github.com/folke/lazy.nvim)? God be with you~ 31 | 32 | #### Why use? 33 | 34 | 1. **:CommentHideSave**: Create `.annotations/` storage code comments and **Delete the current file comment** move comments to `.annotations`. 35 | 2. **:CommentHideRestore**: Restore comments from `.annotations/` to the current file. 36 | 37 | If you add the `.annotations/` directory to the `.gitignore` file, anyone without this directory will **be unable to restore your comments**. 38 | 39 | #### Public comments 40 | 41 | > 42 | > 43 | > After executing `:CommentHideSave`, **please do not make any changes**, as this will disrupt the line numbers and prevent `:CommentHideRestore` from restoring the comments. 👊🐱🔥 44 | 45 | ```js 46 | 0 /* >>> 47 | 1 This will not be hidden and will be 2 visible to everyone 48 | 2 */ 49 | 3 50 | 4 const x = 42; // This is a comment 51 | 5 /* This is a multi-line 52 | 6 comment */ 53 | 7 // Another comment 54 | ``` 55 | 56 | run `:CommentHideRestore`: 57 | 58 | ```js 59 | 1 /* >>> 60 | 2 This will not be hidden and will be 3 visible to everyone 61 | 3 */ 62 | 4 63 | 5 const x = 42; 64 | ``` 65 | 66 | The `/* */` block remains because comment-hide allows preserving comments using `>>>`. Only block-style `/* */` comments support this feature. 67 | 68 | These comments are stored in the `.annotations/` folder at the root directory. You can locate the JSON file by following the current file name. 69 | 70 | ```json 71 | {"comments":[{"text":"\/\/ This is a comment"},{"text":"\/\/ Another comment"},{"multi":true,"text":"\/* This is a multi-line\n\/* This is a multi-line\n comment *\/"}],"originalContent":"\/* >>>\n This will not be hidden and will be visible to everyone\n*\/\n\nconst x = 42; \/\/ This is a comment\n\/* This is a multi-line\n comment *\/\n\/\/ Another comment","filePath":"Code\/project\/iusx\/test\/hhha.js"} 72 | ``` 73 | 74 | To restore comments, run `:CommentHideRestore`, and the plugin will reinsert comments based on line numbers and positions: 75 | 76 | ```js 77 | 0 /* >>> 78 | 1 This will not be hidden and will be 2 visible to everyone 79 | 2 */ 80 | 3 81 | 4 const x = 42; // This is a comment 82 | 5 /* This is a multi-line 83 | 6 comment */ 84 | 7 // Another comment 85 | ``` 86 | 87 | #### Next? 88 | 89 | - [ ] : Restore all comments 90 | - [ ] : Hide all file comments to the `.annotations/` directory 91 | - [x] : Fix space placeholders after `:CommentHideSave`. 92 | - [x] : Fix the absolute positioning issue. 93 | - [x] : Customize hiding and showing, for example, comment blocks containing `>>>` will not be hidden 94 | 95 | #### Support language / framework 96 | 97 | ```js 98 | // Support: Java, Lua, R, C++, Go, Python, Ruby, Rust, 99 | JavaScript, HTML, SCSS, CSS, TypeScript, 100 | TSX, JSX, Vue, Scala, Bash 101 | --- 102 | maybe more? 103 | ``` 104 | 105 | For comment support, please refer to [comment_patterns](https://github.com/jiangxue-analysis/nvim.comment-hide/blob/main/lua/comment-hide/utils.lua), as each language has many different comment styles, so not all of them may be supported. 106 | 107 | ``` 108 | local comment_patterns = { 109 | ["c"] = { 110 | { single = "//" }, 111 | { multi_start = "/*", multi_end = "*/" }, 112 | }, 113 | ["cpp"] = { 114 | { single = "//" }, 115 | { multi_start = "/*", multi_end = "*/" }, 116 | }, 117 | ["cs"] = { 118 | { single = "//" }, 119 | { multi_start = "/*", multi_end = "*/" }, 120 | }, 121 | ["css"] = { 122 | { single = "//" }, 123 | { multi_start = "/*", multi_end = "*/" }, 124 | }, 125 | …… 126 | } 127 | ``` 128 | 129 | #### Example 130 | 131 | ```js 132 | [RUST] 133 | 1 // COMMENT | 1 fn main() { 134 | 2 fn main() { | 2 135 | 3 /* | 3 println!("// Hello, World!"); 136 | 4 * COMMENT | 4 } 137 | 5 */ | 5 138 | 6 println!("// Hello, World!"); | 6 fn main() { 139 | 7 } | 7 140 | 8 // COMMENT | 8 println!("Hello, World! /* test */"); 141 | 9 | 9 } 142 | 10 fn main() { 143 | 11 /* 144 | 12 * COMMENT 145 | 13 */ 146 | 14 println!("Hello, World! /* test */"); // TEST 147 | 15 } 148 | 149 | 150 | 151 | [SCSS] 152 | 1 /* Set default margin and font for the body */ 153 | 2 body { | 1 body { 154 | 3 margin: 0; | 2 margin: 0; 155 | 4 font-family: Arial, sans-serif; | 3 font-family: Arial, sans-serif; 156 | 5 background-color: #f5f5f5; | 4 background-color: #f5f5f5; 157 | 6 h1 { | 5 h1 { 158 | 7 color: #333; // TEST | 6 color: #333; 159 | 8 text-align: center; | 7 text-align: center; 160 | 9 margin-top: 40px; | 8 margin-top: 40px; 161 | 10 } | 9 } 162 | 11 } | 10 } 163 | 164 | 165 | 166 | [TS] 167 | 1 // This is a single-line comment 168 | 2 const commentRegex = /\/\/.*|\/\*[\s\S]*?\*\/||#.*$/gm; 169 | 3 170 | 4 /* Multi-line | 1 const commentRegex = /\/\/.*|\/\*[\s\S]*?\*\/||#.*$/gm; 171 | 5 comment */ | 2 172 | 6 | 3 const regex = /\/\*[\s\S]*?\*\/|\/\/.*$/gm; 173 | 7 174 | 8 // String with // insideconst str = "This is a // string"; 175 | 9 const regex = /\/\*[\s\S]*?\*\/|\/\/.*$/gm; // Regex with comment-like content 176 | 177 | [TSX] 178 | 1 { | 1 { 179 | 2 // image | 2 180 | 3 } | 3 } 181 | 4 { 182 | 5 /* {isValidImageIcon 183 | 6 ? answer icon 184 | 7 : (icon && icon !== '') ? : 185 | 8 } */ 186 | 9 } 187 | ``` 188 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiangxue-analysis/nvim.comment-hide/ed947a23956524b43566833ad8cf0641bb36321a/demo.gif -------------------------------------------------------------------------------- /lua/comment-hide/commands.lua: -------------------------------------------------------------------------------- 1 | local utils = require("comment-hide.utils") 2 | 3 | local M = {} 4 | 5 | local function get_project_root() 6 | local current_file = vim.api.nvim_buf_get_name(0) 7 | local project_root = vim.fn.finddir(".git", vim.fn.fnamemodify(current_file, ":p:h") .. ";") 8 | return project_root ~= "" and vim.fn.fnamemodify(project_root, ":h") or vim.fn.getcwd() 9 | end 10 | 11 | local function get_comment_store_path() 12 | return get_project_root() .. "/.annotations" 13 | end 14 | 15 | local function ensure_in_gitignore() 16 | if not M.config or not M.config.gitignore then 17 | return 18 | end 19 | 20 | local gitignore_path = get_project_root() .. "/.gitignore" 21 | local target_entry = ".annotations" 22 | 23 | if vim.fn.filereadable(gitignore_path) == 1 then 24 | for _, line in ipairs(vim.fn.readfile(gitignore_path)) do 25 | if line:match("^%s*" .. vim.pesc(target_entry) .. "%s*$") then 26 | return 27 | end 28 | end 29 | end 30 | 31 | local file = io.open(gitignore_path, "a") 32 | if file then 33 | file:write((vim.fn.getfsize(gitignore_path) > 0 and "\n" or "") .. target_entry .. "\n") 34 | file:close() 35 | vim.notify("Added '" .. target_entry .. "' to .gitignore", vim.log.levels.INFO) 36 | else 37 | vim.notify("Failed to update .gitignore file", vim.log.levels.WARN) 38 | end 39 | end 40 | 41 | local function save_comments() 42 | local buf = vim.api.nvim_get_current_buf() 43 | local filepath = vim.api.nvim_buf_get_name(buf) 44 | local project_root = get_project_root() 45 | 46 | local comment_store_path = get_comment_store_path() 47 | local relative_path = filepath:gsub(project_root .. "/", "") 48 | 49 | if vim.fn.isdirectory(comment_store_path) == 0 then 50 | vim.fn.mkdir(comment_store_path, "p") 51 | end 52 | 53 | local content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n") 54 | local filetype = vim.api.nvim_buf_get_option(buf, "filetype") 55 | 56 | local comments, uncommented_code = utils.extract_comments(content, filetype) 57 | 58 | local comment_file_path = comment_store_path .. "/" .. relative_path .. ".json" 59 | vim.fn.mkdir(vim.fn.fnamemodify(comment_file_path, ":h"), "p") 60 | 61 | local comment_data = { 62 | originalContent = content, 63 | comments = comments, 64 | filePath = relative_path, 65 | timestamp = os.date("%Y-%m-%d %H:%M:%S"), 66 | } 67 | 68 | vim.fn.writefile({ vim.json.encode(comment_data) }, comment_file_path) 69 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(uncommented_code, "\n")) 70 | 71 | vim.notify("Comments removed. Backup at: " .. comment_file_path, vim.log.levels.INFO) 72 | end 73 | 74 | local function restore_comments() 75 | local buf = vim.api.nvim_get_current_buf() 76 | local filepath = vim.api.nvim_buf_get_name(buf) 77 | local project_root = get_project_root() 78 | 79 | local comment_store_path = get_comment_store_path() 80 | local relative_path = filepath:gsub(project_root .. "/", "") 81 | local comment_file_path = comment_store_path .. "/" .. relative_path .. ".json" 82 | 83 | if vim.fn.filereadable(comment_file_path) == 0 then 84 | vim.notify("No saved comments found at: " .. comment_file_path, vim.log.levels.ERROR) 85 | return 86 | end 87 | 88 | local ok, comment_data = pcall(vim.json.decode, table.concat(vim.fn.readfile(comment_file_path), "\n")) 89 | if not ok then 90 | vim.notify("Failed to parse comment file", vim.log.levels.ERROR) 91 | return 92 | end 93 | 94 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(comment_data.originalContent, "\n")) 95 | vim.notify("Restored comments from: " .. comment_file_path, vim.log.levels.INFO) 96 | end 97 | 98 | function M.setup(opts) 99 | if M.config then return end 100 | 101 | M.config = vim.tbl_deep_extend("force", { 102 | gitignore = false, 103 | }, opts or {}) 104 | 105 | vim.api.nvim_create_user_command("CommentHideSave", function() 106 | if M.config.gitignore then 107 | ensure_in_gitignore() 108 | end 109 | save_comments() 110 | end, { desc = "Save and strip comments" }) 111 | 112 | vim.api.nvim_create_user_command("CommentHideRestore", restore_comments, { 113 | desc = "Restore original content with comments", 114 | }) 115 | end 116 | 117 | return M 118 | -------------------------------------------------------------------------------- /lua/comment-hide/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local default_config = { 4 | gitignore = false 5 | } 6 | 7 | M.config = nil 8 | 9 | local function setup(opts) 10 | M.config = vim.tbl_deep_extend("force", {}, default_config, opts or {}) 11 | require("comment-hide.commands").setup(M.config) 12 | end 13 | 14 | M.setup = setup 15 | 16 | return M 17 | -------------------------------------------------------------------------------- /lua/comment-hide/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local comment_patterns = { 4 | ["c"] = { 5 | { single = "//" }, 6 | { multi_start = "/*", multi_end = "*/" }, 7 | }, 8 | ["cpp"] = { 9 | { single = "//" }, 10 | { multi_start = "/*", multi_end = "*/" }, 11 | }, 12 | ["cs"] = { 13 | { single = "//" }, 14 | { multi_start = "/*", multi_end = "*/" }, 15 | }, 16 | ["css"] = { 17 | { single = "//" }, 18 | { multi_start = "/*", multi_end = "*/" }, 19 | }, 20 | ["go"] = { 21 | { single = "//" }, 22 | { multi_start = "/*", multi_end = "*/" }, 23 | }, 24 | ["java"] = { 25 | { single = "//" }, 26 | { multi_start = "/*", multi_end = "*/" }, 27 | }, 28 | ["javascript"] = { 29 | { single = "//" }, 30 | { multi_start = "/*", multi_end = "*/" }, 31 | }, 32 | ["javascriptreact"] = { 33 | { single = "//" }, 34 | { multi_start = "/*", multi_end = "*/" }, 35 | }, 36 | ["typescript"] = { 37 | { single = "//" }, 38 | { multi_start = "/*", multi_end = "*/" }, 39 | }, 40 | ["typescriptreact"] = { 41 | { single = "//" }, 42 | { multi_start = "/*", multi_end = "*/" }, 43 | }, 44 | ["scala"] = { 45 | { single = "//" }, 46 | { multi_start = "/*", multi_end = "*/" }, 47 | { multi_start = "/**", multi_end = "*/" }, 48 | }, 49 | ["lua"] = { 50 | { single = "--" }, 51 | { multi_start = "--[[", multi_end = "]]" }, 52 | }, 53 | ["python"] = { 54 | { single = "#" }, 55 | { multi_start = '"""', multi_end = '"""' }, 56 | { multi_start = "'''", multi_end = "'''" }, 57 | { single = "//" }, 58 | { multi_start = "/*", multi_end = "*/" }, 59 | }, 60 | ["ruby"] = { 61 | { single = "#" }, 62 | { multi_start = "=begin", multi_end = "=end" }, 63 | }, 64 | ["r"] = { 65 | { single = "#" }, 66 | }, 67 | ["rust"] = { 68 | { single = "//" }, 69 | { multi_start = "/*", multi_end = "*/" }, 70 | }, 71 | ["sh"] = { 72 | { single = "#" }, 73 | }, 74 | ["html"] = { 75 | { multi_start = "" }, 76 | { single = "//" }, 77 | { multi_start = "/*", multi_end = "*/" }, 78 | }, 79 | ["markdown"] = { 80 | { multi_start = "" }, 81 | }, 82 | ["php"] = { 83 | { single = "//" }, 84 | { single = "#" }, 85 | { multi_start = "/*", multi_end = "*/" }, 86 | }, 87 | ["scss"] = { 88 | { single = "//" }, 89 | { multi_start = "/*", multi_end = "*/" }, 90 | }, 91 | ["vue"] = { 92 | { multi_start = "" }, 93 | { single = "//" }, 94 | { multi_start = "/*", multi_end = "*/" }, 95 | }, 96 | ["svelte"] = { 97 | { multi_start = "" }, 98 | { single = "//" }, 99 | { multi_start = "/*", multi_end = "*/" }, 100 | }, 101 | ["elixir"] = { 102 | { single = "#" }, 103 | }, 104 | ["erlang"] = { 105 | { single = "%" }, 106 | }, 107 | } 108 | 109 | local function extract_heredocs(content, filetype) 110 | if filetype ~= "ruby" then 111 | return {}, content 112 | end 113 | 114 | local heredocs = {} 115 | local processed = content 116 | local i = 1 117 | 118 | processed = processed:gsub( 119 | "(<<[-~]?%s*['\"]?([%w_]+)['\"]?[\r\n])(.-)(\n%s*%2)", 120 | function(start, delim, content, ending) 121 | heredocs[i] = { 122 | delim = delim, 123 | content = start .. content .. ending, 124 | placeholder = "HEREDOC_" .. i .. "_", 125 | } 126 | i = i + 1 127 | return heredocs[i - 1].placeholder 128 | end 129 | ) 130 | 131 | return heredocs, processed 132 | end 133 | 134 | local function restore_heredocs(content, heredocs) 135 | for _, h in ipairs(heredocs) do 136 | content = content:gsub(h.placeholder, h.content) 137 | end 138 | return content 139 | end 140 | 141 | local function is_in_string_or_special(line, pos, filetype, heredocs) 142 | if filetype == "bash" or filetype == "sh" then 143 | if pos == 1 and line:sub(1, 2) == "#!" then 144 | return true 145 | end 146 | local before = line:sub(1, pos - 1) 147 | local after = line:sub(pos) 148 | 149 | if before:match("%${[^}]*$") and after:match("^[#%%]") then 150 | return true 151 | end 152 | 153 | if before:match("%${$") and after:match("^#[^}]*}") then 154 | return true 155 | end 156 | end 157 | 158 | if filetype == "elixir" then 159 | local hash_pos = line:find("#", 1, true) 160 | if hash_pos and line:sub(hash_pos + 1, hash_pos + 1) == "{" then 161 | return true 162 | end 163 | 164 | local first_nonspace = line:match("^%s*(.)") 165 | if first_nonspace == '"' then 166 | return true 167 | end 168 | end 169 | 170 | local in_string_single = false 171 | local in_string_double = false 172 | local in_backtick = false 173 | local in_regex = false 174 | local in_percent_string = false 175 | local percent_char = nil 176 | 177 | for i = 1, pos do 178 | local char = line:sub(i, i) 179 | local prev_char = i > 1 and line:sub(i - 1, i - 1) or "" 180 | 181 | if not in_percent_string then 182 | if char == "'" and prev_char ~= "\\" and not in_string_double and not in_backtick then 183 | in_string_single = not in_string_single 184 | elseif char == '"' and prev_char ~= "\\" and not in_string_single and not in_backtick then 185 | in_string_double = not in_string_double 186 | elseif char == "`" and prev_char ~= "\\" and not in_string_single and not in_string_double then -- NEW 187 | in_backtick = not in_backtick 188 | elseif 189 | filetype == "ruby" 190 | and char == "/" 191 | and not in_string_single 192 | and not in_string_double 193 | and not in_backtick 194 | and prev_char ~= "\\" 195 | then 196 | in_regex = not in_regex 197 | end 198 | end 199 | 200 | if not in_percent_string and char == "%" then 201 | local next_char = line:sub(i + 1, i + 1) 202 | if next_char == "q" or next_char == "Q" then 203 | local delim = line:sub(i + 2, i + 2) 204 | if delim == "{" then 205 | in_percent_string = true 206 | percent_char = "}" 207 | elseif delim == "(" then 208 | in_percent_string = true 209 | percent_char = ")" 210 | elseif delim == "[" then 211 | in_percent_string = true 212 | percent_char = "]" 213 | elseif delim == "<" then 214 | in_percent_string = true 215 | percent_char = ">" 216 | end 217 | end 218 | elseif in_percent_string and char == percent_char and prev_char ~= "\\" then 219 | in_percent_string = false 220 | end 221 | end 222 | 223 | for _, h in ipairs(heredocs) do 224 | if line:find(h.delim, 1, true) then 225 | return true 226 | end 227 | end 228 | 229 | return in_string_single or in_string_double or in_backtick or in_regex or in_percent_string 230 | end 231 | 232 | function M.extract_comments(content, filetype) 233 | local comments = {} 234 | local uncommented = content 235 | 236 | local heredocs, processed_content = extract_heredocs(content, filetype) 237 | uncommented = processed_content 238 | 239 | local protected = {} 240 | uncommented = uncommented:gsub("/%*%s*>>>.-[^%*/]-*/", function(match) 241 | table.insert(protected, match) 242 | return "PROTECTED_" .. #protected .. "_" 243 | end) 244 | 245 | local patterns = comment_patterns[filetype] or {} 246 | for _, pattern in ipairs(patterns) do 247 | if pattern.multi_start and pattern.multi_end then 248 | if filetype == "python" and (pattern.multi_start == '"""' or pattern.multi_start == "'''") then 249 | local content_lines = vim.split(uncommented, "\n") 250 | local new_lines = {} 251 | local in_comment = false 252 | local comment_start_line = 1 253 | local comment_content = {} 254 | 255 | for i, line in ipairs(content_lines) do 256 | if not in_comment then 257 | local trimmed = line:match("^%s*(.-)%s*$") 258 | if trimmed == pattern.multi_start then 259 | in_comment = true 260 | comment_start_line = i 261 | table.insert(comment_content, line) 262 | else 263 | table.insert(new_lines, line) 264 | end 265 | else 266 | table.insert(comment_content, line) 267 | local trimmed = line:match("^%s*(.-)%s*$") 268 | if trimmed == pattern.multi_end then 269 | in_comment = false 270 | table.insert(comments, { 271 | text = table.concat(comment_content, "\n"), 272 | multi = true, 273 | }) 274 | comment_content = {} 275 | end 276 | end 277 | end 278 | 279 | if in_comment then 280 | for _, l in ipairs(comment_content) do 281 | table.insert(new_lines, l) 282 | end 283 | end 284 | 285 | uncommented = table.concat(new_lines, "\n") 286 | else 287 | local lines = vim.split(uncommented, "\n") 288 | local new_lines = {} 289 | local inside_multi = false 290 | local comment_buf = {} 291 | local start_pat = pattern.multi_start 292 | local end_pat = pattern.multi_end 293 | local current_line_idx = 1 294 | 295 | while current_line_idx <= #lines do 296 | local line = lines[current_line_idx] 297 | local i = 1 298 | local output_line = "" 299 | 300 | while i <= #line do 301 | if not inside_multi then 302 | local s, e = line:find(vim.pesc(start_pat), i) 303 | if s then 304 | if is_in_string_or_special(line, s, filetype, {}) then 305 | output_line = output_line .. line:sub(i, e) 306 | i = e + 1 307 | else 308 | inside_multi = true 309 | comment_buf = { line:sub(s) } 310 | output_line = output_line .. line:sub(i, s - 1) 311 | i = e + 1 312 | end 313 | else 314 | output_line = output_line .. line:sub(i) 315 | break 316 | end 317 | else 318 | table.insert(comment_buf, line) 319 | local s, e = line:find(vim.pesc(end_pat), i) 320 | if s then 321 | inside_multi = false 322 | local comment = table.concat(comment_buf, "\n") 323 | table.insert(comments, { text = comment, multi = true }) 324 | i = e + 1 325 | comment_buf = {} 326 | else 327 | break 328 | end 329 | end 330 | end 331 | 332 | if not inside_multi then 333 | table.insert(new_lines, output_line) 334 | end 335 | 336 | current_line_idx = current_line_idx + 1 337 | end 338 | 339 | uncommented = table.concat(new_lines, "\n") 340 | end 341 | end 342 | 343 | if pattern.single then 344 | local lines = vim.split(uncommented, "\n") 345 | local new_lines = {} 346 | 347 | for line_num, line in ipairs(lines) do 348 | local new_line = "" 349 | local comment_start = nil 350 | 351 | for i = 1, #line do 352 | if line:sub(i, i + #pattern.single - 1) == pattern.single then 353 | if not is_in_string_or_special(line, i, filetype, heredocs) then 354 | comment_start = i 355 | break 356 | end 357 | end 358 | end 359 | 360 | if comment_start then 361 | table.insert(comments, { text = line:sub(comment_start) }) 362 | new_line = line:sub(1, comment_start - 1) 363 | else 364 | new_line = line 365 | end 366 | 367 | table.insert(new_lines, new_line) 368 | end 369 | 370 | uncommented = table.concat(new_lines, "\n") 371 | end 372 | end 373 | 374 | for i, match in ipairs(protected) do 375 | uncommented = uncommented:gsub("PROTECTED_" .. i .. "_", match) 376 | end 377 | 378 | uncommented = restore_heredocs(uncommented, heredocs) 379 | 380 | local lines = vim.split(uncommented, "\n") 381 | local cleaned_lines = {} 382 | local last_was_empty = false 383 | 384 | for _, line in ipairs(lines) do 385 | local trimmed = line:match("^%s*(.-)%s*$") 386 | if trimmed ~= "" then 387 | table.insert(cleaned_lines, line) 388 | last_was_empty = false 389 | elseif not last_was_empty then 390 | table.insert(cleaned_lines, "") 391 | last_was_empty = true 392 | end 393 | end 394 | 395 | while #cleaned_lines > 0 and cleaned_lines[1]:match("^%s*$") do 396 | table.remove(cleaned_lines, 1) 397 | end 398 | while #cleaned_lines > 0 and cleaned_lines[#cleaned_lines]:match("^%s*$") do 399 | table.remove(cleaned_lines) 400 | end 401 | 402 | return comments, table.concat(cleaned_lines, "\n") 403 | end 404 | 405 | return M 406 | --------------------------------------------------------------------------------