├── .editorconfig ├── queries ├── markdown │ └── clipping.scm ├── python │ └── clipping.scm ├── rust │ └── clipping.scm ├── lua │ └── clipping.scm └── toml │ └── clipping.scm ├── plugin └── nvim-treesitter-clipping.lua ├── README.md ├── LICENSE └── lua └── nvim-treesitter-clipping.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | charset = utf-8 10 | 11 | [{Makefile,**/Makefile,runtime/doc/*.txt}] 12 | indent_style = tab 13 | indent_size = 8 14 | -------------------------------------------------------------------------------- /queries/markdown/clipping.scm: -------------------------------------------------------------------------------- 1 | 2 | (fenced_code_block 3 | (info_string 4 | (language) @filetype 5 | ) 6 | (code_fence_content) @clip 7 | (#set! "exclude_bounds" "end") 8 | ) 9 | 10 | ( 11 | (code_fence_content) @clip 12 | (#set! "exclude_bounds" "end") 13 | ) 14 | -------------------------------------------------------------------------------- /plugin/nvim-treesitter-clipping.lua: -------------------------------------------------------------------------------- 1 | if vim.g.loaded_nvim_treesitter_clipping then 2 | return 3 | end 4 | vim.g.loaded_nvim_treesitter_clipping = true 5 | 6 | vim.keymap.set("n", "(ts-clipping-clip)", function() 7 | require("nvim-treesitter-clipping").clip() 8 | end) 9 | 10 | vim.keymap.set({ "n", "v", "o" }, "(ts-clipping-select)", function() 11 | require("nvim-treesitter-clipping").select() 12 | end) 13 | -------------------------------------------------------------------------------- /queries/python/clipping.scm: -------------------------------------------------------------------------------- 1 | (function_definition 2 | body: 3 | (block 4 | (expression_statement 5 | (string) @clip 6 | (#set! "exclude_bounds" "both") 7 | ) 8 | ) 9 | (#set! "filetype" "markdown") 10 | ) 11 | 12 | (class_definition 13 | body: 14 | (block 15 | (expression_statement 16 | (string) @clip 17 | (#set! "exclude_bounds" "both") 18 | ) 19 | ) 20 | (#set! "filetype" "markdown") 21 | ) 22 | 23 | ( 24 | (comment) @clip_group 25 | (#set! "prefix_pattern" "\\s*#\\s*") 26 | ) 27 | -------------------------------------------------------------------------------- /queries/rust/clipping.scm: -------------------------------------------------------------------------------- 1 | ( 2 | ( 3 | line_comment 4 | outer: (outer_doc_comment_marker) 5 | doc: (doc_comment) 6 | ) @clip_group 7 | 8 | (#set! "exclude_bounds" "end") 9 | (#set! "filetype" "markdown") 10 | (#set! "prefix_pattern" "\\s*///\\s*") 11 | ) 12 | 13 | ( 14 | ( 15 | line_comment 16 | inner: (inner_doc_comment_marker) 17 | doc: (doc_comment) 18 | ) @clip_group 19 | 20 | (#set! "exclude_bounds" "end") 21 | (#set! "filetype" "markdown") 22 | (#set! "prefix_pattern" "\\s*//!\\s*") 23 | ) 24 | 25 | ( 26 | (line_comment) @clip_group 27 | (#set! "filetype" "markdown") 28 | (#set! "prefix_pattern" "\\s*//\\s*") 29 | ) 30 | -------------------------------------------------------------------------------- /queries/lua/clipping.scm: -------------------------------------------------------------------------------- 1 | ;; A query for lua files 2 | 3 | ; vim.cmd [[ 4 | ; %% HERE %% 5 | ; ]] 6 | (function_call 7 | name: (_) @funcname 8 | arguments: (arguments (string) @clip) 9 | (#eq? @funcname "vim.cmd") 10 | (#match? @clip "^\\[") ; enable only [[ .. ]] string 11 | (#set! "filetype" "vim") 12 | (#set! "exclude_bounds" "both") 13 | ) 14 | 15 | ; some_func [[ 16 | ; %% HERE %% 17 | ; ]] 18 | (function_call 19 | name: (_) 20 | arguments: (arguments (string) @clip) 21 | (#match? @clip "^\\[") 22 | (#set! "exclude_bounds" "both") 23 | ; (#offset! @clip 1 0 -1 0) 24 | ) 25 | 26 | ; comment lines 27 | ( 28 | (comment) @clip_group 29 | (#set! "prefix_pattern" "\\s*---\\?\\s*") 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-treesitter-clipping 2 | 3 | This plugin uses [vim-partedit](https://github.com/thinca/vim-partedit). 4 | 5 | ## Requirements 6 | 7 | * Neovim **>= 0.11.0** 8 | * [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) 9 | * [vim-partedit](https://github.com/thinca/vim-partedit) 10 | 11 | ## Usage 12 | 13 | For example: 14 | 15 | ```lua 16 | vim.keymap.set("n", "c", "(ts-clipping-clip)") 17 | vim.keymap.set({"x", "o"}, "c", "(ts-clipping-select)") 18 | ``` 19 | 20 | Press `c` on 21 | 22 | * Lua comments, 23 | * Markdown code blocks, 24 | * Python docstrings, 25 | * Rust comments, 26 | 27 | and see what happens. 28 | 29 | ## Supported Languages 30 | 31 | - [x] Lua 32 | - [x] Markdown 33 | - [x] Python 34 | - [x] Rust 35 | - [x] Toml 36 | -------------------------------------------------------------------------------- /queries/toml/clipping.scm: -------------------------------------------------------------------------------- 1 | ( 2 | (pair 3 | (bare_key) @_key 4 | (string) @clip) 5 | (#vim-match? @_key "^hook_\w*") 6 | (#vim-match? @clip "^('''|\"\"\")") 7 | (#set! "exclude_bounds" "both") 8 | (#set! "filetype" "vim") 9 | ) 10 | 11 | ( 12 | (pair 13 | (bare_key) @_key 14 | (string) @clip) 15 | (#vim-match? @_key "^lua_\w*") 16 | (#vim-match? @clip "^('''|\"\"\")") 17 | (#set! "exclude_bounds" "both") 18 | (#set! "filetype" "lua") 19 | ) 20 | 21 | ( 22 | (table 23 | (dotted_key) @_key 24 | (pair 25 | (string) @clip)) 26 | (#vim-match? @_key "^%(plugins\.)?ftplugin$") 27 | (#vim-match? @clip "^('''|\"\"\")") 28 | (#set! "exclude_bounds" "both") 29 | (#set! "filetype" "vim") 30 | ) 31 | 32 | ( 33 | (string) @clip 34 | (#vim-match? @clip "^('''|\"\"\")") 35 | (#set! "exclude_bounds" "both") 36 | ) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mogami Shinichi 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 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-clipping.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@alias parteditargs {from: integer, to: integer, pattern_id: integer, filetype?: string, prefix?: string, auto_prefix?: string, prefix_pattern?: string} 4 | 5 | ---range の列を受け取り、隣接している range をいい感じにマージする。 6 | ---@param clip_group {from: integer, to: integer, pattern_id: integer}[] 7 | ---@return {from: integer, to: integer, pattern_id: integer}[] 8 | local function merge_clip_group(clip_group) 9 | -- パターン ID が異なるものはマージできないため 10 | -- パターン ID の若い順・開始が早い順にソート 11 | table.sort(clip_group, function(clip1, clip2) 12 | if clip1.pattern_id == clip2.pattern_id then 13 | return clip1.from < clip2.from 14 | else 15 | return clip1.pattern_id < clip2.pattern_id 16 | end 17 | end) 18 | 19 | local lst = {} 20 | local tmp 21 | for _, clip in ipairs(clip_group) do 22 | if tmp == nil then 23 | tmp = clip 24 | else 25 | if clip.pattern_id == tmp.pattern_id and clip.from - 1 <= tmp.to then 26 | -- merge する。 27 | tmp.to = clip.to 28 | else 29 | lst[#lst + 1] = tmp 30 | tmp = clip 31 | end 32 | end 33 | end 34 | if tmp ~= nil then 35 | lst[#lst + 1] = tmp 36 | end 37 | 38 | return lst 39 | end 40 | 41 | ---partedit 可能な箇所のリストを返す。 42 | ---partedit 可能な箇所は query にマッチする箇所として判定する。具体的には 43 | ---`@clip`: partedit 可能な node をキャプチャする 44 | ---`@clip_group`: partedit 可能な node の一部をキャプチャする(連続したら連ねる) 45 | ---@param bufnr integer 46 | ---@return parteditargs[] 47 | function M.get_code_ranges(bufnr) 48 | if bufnr == nil then 49 | bufnr = 0 50 | end 51 | 52 | local parser = vim.treesitter.get_parser(bufnr) 53 | local lang = parser:lang() 54 | local tree = parser:parse()[1] 55 | 56 | local query = vim.treesitter.query.get(lang, "clipping") 57 | if query == nil then 58 | return {} 59 | end 60 | 61 | local clip_captures = {} 62 | local clip_group = {} 63 | 64 | for pattern_id, match, metadata in query:iter_matches(tree:root(), bufnr) do 65 | ---@type "clip" | "clip_group" | nil 66 | local kind 67 | local range 68 | local filetype 69 | 70 | if metadata.filetype ~= nil then 71 | filetype = metadata.filetype 72 | end 73 | local prefix = metadata.prefix 74 | local auto_prefix = metadata.auto_prefix 75 | local prefix_pattern = metadata.prefix_pattern 76 | local exclude_bounds = metadata.exclude_bounds 77 | 78 | for id, nodes in pairs(match) do 79 | -- TODO: 一旦マッチした最初のノードのみ取る。本当は node ごとに iterate すべき 80 | local node = nodes[1] 81 | local name = query.captures[id] 82 | -- `node` was captured by the `name` capture in the match 83 | if name == "clip" or name == "clip_group" then 84 | kind = name 85 | local metadata_match = metadata[id] 86 | if metadata_match ~= nil and metadata_match.range ~= nil then 87 | range = metadata_match.range 88 | else 89 | range = { node:range() } 90 | end 91 | elseif name == "filetype" then 92 | filetype = vim.treesitter.get_node_text(node, bufnr or 0, {}) 93 | end 94 | end 95 | 96 | local offset_start = 0 97 | local offset_end = 0 98 | if exclude_bounds == "start" or exclude_bounds == "both" then 99 | offset_start = 1 100 | end 101 | if exclude_bounds == "end" or exclude_bounds == "both" then 102 | offset_end = 1 103 | end 104 | 105 | local capture_info = { 106 | from = range[1] + 1 + offset_start, 107 | to = range[3] + 1 - offset_end, 108 | pattern_id = pattern_id, 109 | filetype = filetype, 110 | prefix = prefix, 111 | auto_prefix = auto_prefix, 112 | prefix_pattern = prefix_pattern, 113 | } 114 | 115 | if kind == "clip" then 116 | clip_captures[#clip_captures + 1] = capture_info 117 | elseif kind == "clip_group" then 118 | clip_group[#clip_group + 1] = capture_info 119 | end 120 | end 121 | 122 | local merged_clip_group = merge_clip_group(clip_group) 123 | vim.list_extend(clip_captures, merged_clip_group) 124 | 125 | -- 開始が早い順・パターン ID の若い順にソート 126 | table.sort(clip_captures, function(clip1, clip2) 127 | if clip1.from == clip2.from then 128 | return clip1.pattern_id < clip2.pattern_id 129 | else 130 | return clip1.from < clip2.from 131 | end 132 | end) 133 | 134 | return clip_captures 135 | end 136 | 137 | ---現在のカーソル位置にあり、切り出せそうなところを新しいバッファに切り取る。 138 | ---切り出せそうなところは clipping.scm の @clip でキャプチャされたところ。 139 | ---@param bufnr? number 140 | function M.clip(bufnr) 141 | -- LanguageTree object 142 | 143 | if bufnr == nil then 144 | bufnr = vim.fn.bufnr() 145 | end 146 | 147 | local cursor = vim.fn.getcurpos() 148 | local row_cursor = cursor[2] 149 | 150 | local code_ranges = M.get_code_ranges(bufnr) 151 | 152 | for _, d in ipairs(code_ranges) do 153 | if d.from <= row_cursor and row_cursor <= d.to then 154 | vim.fn["partedit#start"](d.from, d.to, { 155 | filetype = d.filetype or "", 156 | prefix = d.prefix, 157 | auto_prefix = d.auto_prefix, 158 | prefix_pattern = d.prefix_pattern, 159 | }) 160 | return 161 | end 162 | end 163 | end 164 | 165 | local function select_in_visual_mode(from, to) 166 | if vim.fn.mode() ~= "V" then 167 | vim.cmd.normal("V") 168 | end 169 | vim.fn.cursor { from, 1 } 170 | vim.cmd.normal("o") 171 | vim.fn.cursor { to, 1 } 172 | end 173 | 174 | ---現在のカーソル位置にあり、切り出せそうなところを選択する。 175 | ---切り出せそうなところは clipping.scm の @clip でキャプチャされたところ。 176 | ---@param bufnr? number 177 | function M.select(bufnr) 178 | -- LanguageTree object 179 | 180 | if bufnr == nil then 181 | bufnr = vim.fn.bufnr() 182 | end 183 | 184 | local cursor = vim.fn.getcurpos() 185 | local row_cursor = cursor[2] 186 | 187 | local code_ranges = M.get_code_ranges(bufnr) 188 | 189 | for _, d in ipairs(code_ranges) do 190 | if d.from <= row_cursor and row_cursor <= d.to then 191 | select_in_visual_mode(d.from, d.to) 192 | return 193 | end 194 | end 195 | end 196 | 197 | return M 198 | --------------------------------------------------------------------------------