├── .editorconfig ├── .github └── workflows │ └── lint.yml ├── .luacheckrc ├── .stylua.toml ├── LICENSE ├── README.md ├── after └── ftplugin │ └── query.vim ├── lua ├── nvim-treesitter-playground.lua ├── nvim-treesitter-playground │ ├── hl-info.lua │ ├── internal.lua │ ├── printer.lua │ ├── promise.lua │ ├── query.lua │ ├── query_linter.lua │ └── utils.lua └── nvim_treesitter_playground_query_omnifunc.lua ├── plugin └── nvim-treesitter-playground.lua ├── queries └── query │ ├── captures.scm │ └── query-linter-queries.scm └── syntax └── tsplayground.vim /.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 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting and style checking 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | luacheck: 7 | name: Luacheck 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Prepare 13 | run: | 14 | sudo apt-get update 15 | sudo add-apt-repository universe 16 | sudo apt install luarocks -y 17 | sudo luarocks install luacheck 18 | 19 | - name: Run Luacheck 20 | run: luacheck . 21 | 22 | stylua: 23 | name: StyLua 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Lint with stylua 28 | uses: JohnnyMorganz/stylua-action@v1 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | args: --check . 32 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- Rerun tests only if their modification time changed. 2 | cache = true 3 | codes = true 4 | 5 | -- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html 6 | ignore = { 7 | "212", -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off. 8 | "411", -- Redefining a local variable. 9 | "412", -- Redefining an argument. 10 | "422", -- Shadowing an argument 11 | "122" -- Indirectly setting a readonly global 12 | } 13 | 14 | -- Global objects defined by the C code 15 | read_globals = { 16 | "vim", 17 | } 18 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | no_call_parentheses = true 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neovim Treesitter Playground 2 | 3 | View treesitter information directly in Neovim! 4 | 5 | ![demo](https://user-images.githubusercontent.com/2361214/202389106-244ac890-9442-4759-9b2c-4fe3c247dfbc.gif) 6 | 7 | ## Deprecation notice 8 | 9 | This plugin is **deprecated** since the functionality is included in Neovim: Use 10 | 11 | - `:Inspect` to show the highlight groups under the cursor 12 | - `:InspectTree` to show the parsed syntax tree ("TSPlayground") 13 | - `:EditQuery` to open the Live Query Editor (Nvim 0.10+) 14 | 15 | ## Requirements 16 | - Neovim [nightly](https://github.com/neovim/neovim#install-from-source) 17 | - [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) plugin (with the `query` grammar installed) 18 | 19 | ## Setup 20 | 21 | Install the plugin (vim-plug shown): 22 | 23 | ```vim 24 | Plug 'nvim-treesitter/nvim-treesitter' 25 | Plug 'nvim-treesitter/playground' 26 | ``` 27 | 28 | It's also recommended that you install the `query` parser for query editor highlighting. Run this after installing the above plugins. 29 | 30 | ```vim 31 | :TSInstall query 32 | ``` 33 | 34 | The configuration is like any other nvim-treesitter module. 35 | 36 | ```lua 37 | require "nvim-treesitter.configs".setup { 38 | playground = { 39 | enable = true, 40 | disable = {}, 41 | updatetime = 25, -- Debounced time for highlighting nodes in the playground from source code 42 | persist_queries = false, -- Whether the query persists across vim sessions 43 | keybindings = { 44 | toggle_query_editor = 'o', 45 | toggle_hl_groups = 'i', 46 | toggle_injected_languages = 't', 47 | toggle_anonymous_nodes = 'a', 48 | toggle_language_display = 'I', 49 | focus_language = 'f', 50 | unfocus_language = 'F', 51 | update = 'R', 52 | goto_node = '', 53 | show_help = '?', 54 | }, 55 | } 56 | } 57 | ``` 58 | 59 | ## Usage 60 | 61 | The tree can be toggled using the command `:TSPlaygroundToggle`. 62 | 63 | ### Keybindings 64 | 65 | - `R`: Refreshes the playground view when focused or reloads the query when the query editor is focused. 66 | - `o`: Toggles the query editor when the playground is focused. 67 | - `a`: Toggles visibility of anonymous nodes. 68 | - `i`: Toggles visibility of highlight groups. 69 | - `I`: Toggles visibility of the language the node belongs to. 70 | - `t`: Toggles visibility of injected languages. 71 | - `f`: Focuses the language tree under the cursor in the playground. The query editor will now be using the focused language. 72 | - `F`: Unfocuses the currently focused language. 73 | - ``: Go to current node in code buffer 74 | 75 | ## Query Editor 76 | 77 | Press `o` to show the query editor. 78 | Write your query like `(node) @capture`, 79 | put the cursor under the capture to highlight the matches. 80 | 81 | ## Completions 82 | 83 | When you are on a `query` buffer, you can get a list of suggestions with 84 | Ctrl-X Ctrl-O. See `:h 'omnifunc'`. 85 | 86 | ## Query Linter 87 | 88 | The playground can lint query files for you. For that, you need to activate the `query_linter` module: 89 | 90 | ```lua 91 | require "nvim-treesitter.configs".setup { 92 | query_linter = { 93 | enable = true, 94 | use_virtual_text = true, 95 | lint_events = {"BufWrite", "CursorHold"}, 96 | }, 97 | } 98 | ``` 99 | 100 | *Note: Query linter assumes certain directory structure to identify which language queries belong to. It expect query files to be under `./queries/`* 101 | 102 | ![image](https://user-images.githubusercontent.com/7189118/101246661-06089a00-3715-11eb-9c57-6d6439defbf8.png) 103 | 104 | ## Show treesitter and syntax highlight groups under the cursor 105 | 106 | The playground comes with `:TSHighlightCapturesUnderCursor` that shows any treesitter or syntax highlight groups under the cursor. 107 | 108 | 109 | 110 | 111 | 112 | ## Show treesitter node under the cursor 113 | 114 | If you only wish to view information about the node your cursor is currently on (without having to open up the full tree), you can use `:TSNodeUnderCursor` instead. 115 | A floating window containing information about the parser, node name and row/col ranges will be shown. 116 | 117 | 118 | -------------------------------------------------------------------------------- /after/ftplugin/query.vim: -------------------------------------------------------------------------------- 1 | setlocal omnifunc=v:lua.require'nvim_treesitter_playground_query_omnifunc'.omnifunc 2 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground.lua: -------------------------------------------------------------------------------- 1 | local parsers = require "nvim-treesitter.parsers" 2 | local M = {} 3 | 4 | function M.init() 5 | require("nvim-treesitter").define_modules { 6 | playground = { 7 | module_path = "nvim-treesitter-playground.internal", 8 | updatetime = 25, 9 | persist_queries = false, 10 | keybindings = { 11 | toggle_query_editor = "o", 12 | toggle_hl_groups = "i", 13 | toggle_injected_languages = "t", 14 | toggle_anonymous_nodes = "a", 15 | toggle_language_display = "I", 16 | focus_language = "f", 17 | unfocus_language = "F", 18 | update = "R", 19 | goto_node = "", 20 | show_help = "?", 21 | }, 22 | }, 23 | query_linter = { 24 | module_path = "nvim-treesitter-playground.query_linter", 25 | use_diagnostics = true, 26 | lint_events = { "BufWrite", "CursorHold" }, 27 | is_supported = function(lang) 28 | return lang == "query" and parsers.has_parser "query" 29 | end, 30 | }, 31 | } 32 | end 33 | 34 | return M 35 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground/hl-info.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-treesitter-playground.utils" 2 | local highlighter = require "vim.treesitter.highlighter" 3 | local ts_utils = require "nvim-treesitter.ts_utils" 4 | local parsers = require "nvim-treesitter.parsers" 5 | 6 | local M = {} 7 | 8 | function M.get_treesitter_hl() 9 | local bufnr = vim.api.nvim_get_current_buf() 10 | local row, col = unpack(vim.api.nvim_win_get_cursor(0)) 11 | row = row - 1 12 | 13 | local results = utils.get_hl_groups_at_position(bufnr, row, col) 14 | local highlights = {} 15 | for _, hl in pairs(results) do 16 | local line = "* **@" .. hl.capture .. "**" 17 | if hl.priority then 18 | line = line .. "(" .. hl.priority .. ")" 19 | end 20 | table.insert(highlights, line) 21 | end 22 | return highlights 23 | end 24 | 25 | function M.get_syntax_hl() 26 | local line = vim.fn.line "." 27 | local col = vim.fn.col "." 28 | local matches = {} 29 | 30 | for _, i1 in ipairs(vim.fn.synstack(line, col)) do 31 | local i2 = vim.fn.synIDtrans(i1) 32 | local n1 = vim.fn.synIDattr(i1, "name") 33 | local n2 = vim.fn.synIDattr(i2, "name") 34 | table.insert(matches, "* " .. n1 .. " -> **" .. n2 .. "**") 35 | end 36 | 37 | return matches 38 | end 39 | 40 | function M.show_hl_captures() 41 | local buf = vim.api.nvim_get_current_buf() 42 | local result = {} 43 | 44 | local function add_to_result(matches, source) 45 | if #matches == 0 then 46 | return 47 | end 48 | 49 | table.insert(result, "# " .. source) 50 | 51 | for _, match in ipairs(matches) do 52 | table.insert(result, match) 53 | end 54 | end 55 | 56 | if highlighter.active[buf] then 57 | local matches = M.get_treesitter_hl() 58 | add_to_result(matches, "Treesitter") 59 | end 60 | 61 | if vim.b.current_syntax ~= nil or #result == 0 then 62 | local matches = M.get_syntax_hl() 63 | add_to_result(matches, "Syntax") 64 | end 65 | 66 | if #result == 0 then 67 | table.insert(result, "* No highlight groups found") 68 | end 69 | 70 | vim.lsp.util.open_floating_preview(result, "markdown", { border = "single", pad_left = 4, pad_right = 4 }) 71 | end 72 | 73 | -- Show Node at Cursor 74 | ---@param opts? table with optional fields 75 | --- - full_path: (boolean, default false) show full path to current node 76 | --- - show_range: (boolean, default true) show range of current node 77 | --- - include_anonymous: (boolean, default false) include anonymous node 78 | --- - highlight_node: (boolean, default true) highlight the current node 79 | --- - hl_group: (string, default "TSPlaygroundFocus") name of group 80 | --- @return number|nil bufnr number 81 | function M.show_ts_node(opts) 82 | opts = vim.tbl_deep_extend("keep", opts or {}, { 83 | full_path = false, 84 | show_range = true, 85 | include_anonymous = false, 86 | highlight_node = true, 87 | hl_group = "TSPlaygroundFocus", 88 | }) 89 | 90 | if not parsers.has_parser() then 91 | return 92 | end 93 | 94 | -- Get Full Path to node 95 | -- @param node 96 | -- @param array? 97 | -- @return string 98 | local function get_full_path(node, array) 99 | local parent = node:parent() 100 | if parent == nil then 101 | if array == nil then 102 | return node:type() 103 | end 104 | local reverse = vim.tbl_map(function(index) 105 | return array[#array + 1 - index]:type() 106 | end, vim.tbl_keys(array)) 107 | return table.concat(reverse, " -> ") 108 | end 109 | return get_full_path(parent, vim.list_extend(array or {}, { node })) 110 | end 111 | 112 | local cursor = vim.api.nvim_win_get_cursor(0) 113 | local line = cursor[1] - 1 114 | local col = cursor[2] 115 | 116 | local bufnr = 0 117 | local root_lang_tree = parsers.get_parser(bufnr) 118 | local lang_tree = root_lang_tree:language_for_range { line, col, line, col } 119 | 120 | local lines = { "# Treesitter" } 121 | local node_under_cursor 122 | 123 | for _, tree in ipairs(lang_tree:trees()) do 124 | local root = tree:root() 125 | if root and vim.treesitter.is_in_node_range(root, line, col) then 126 | local node = root:named_descendant_for_range(line, col, line, col) 127 | local path = opts.full_path and get_full_path(node) or node:type() 128 | 129 | node_under_cursor = node 130 | 131 | vim.list_extend(lines, { 132 | "* Parser: " .. lang_tree:lang(), 133 | string.format("* %s: ", opts.full_path and "Node path" or "Node") .. path, 134 | }) 135 | 136 | if opts.include_anonymous then 137 | local anonymous_node = root:descendant_for_range(line, col, line, col) 138 | vim.list_extend(lines, { 139 | " - Anonymous: " .. anonymous_node:type(), 140 | }) 141 | end 142 | 143 | if opts.show_range then 144 | local srow, scol, erow, ecol = ts_utils.get_vim_range({ node:range() }, bufnr) 145 | 146 | vim.list_extend(lines, { 147 | "* Range: ", 148 | " - Start row: " .. srow, 149 | " - End row: " .. erow, 150 | " - Start col: " .. scol, 151 | " - End col: " .. ecol, 152 | }) 153 | end 154 | end 155 | end 156 | 157 | if not node_under_cursor then 158 | lines[#lines + 1] = "* Node not found" 159 | end 160 | 161 | if opts.highlight_node and node_under_cursor then 162 | local ns = vim.api.nvim_create_namespace "nvim-treesitter-current-node" 163 | 164 | ts_utils.highlight_node(node_under_cursor, bufnr, ns, opts.hl_group) 165 | vim.api.nvim_create_autocmd("CursorMoved", { 166 | group = vim.api.nvim_create_augroup("TSNodeUnderCursor", {}), 167 | buffer = bufnr, 168 | callback = function() 169 | require("nvim-treesitter-playground.internal").clear_highlights(bufnr, ns) 170 | end, 171 | desc = "TSPlayground: clear highlights", 172 | }) 173 | end 174 | 175 | return vim.lsp.util.open_floating_preview(lines, "markdown", { border = "single", pad_left = 4, pad_right = 4 }) 176 | end 177 | 178 | return M 179 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground/internal.lua: -------------------------------------------------------------------------------- 1 | local parsers = require "nvim-treesitter.parsers" 2 | local configs = require "nvim-treesitter.configs" 3 | local ts_utils = require "nvim-treesitter.ts_utils" 4 | local printer = require "nvim-treesitter-playground.printer" 5 | local utils = require "nvim-treesitter-playground.utils" 6 | local ts_query = require "nvim-treesitter.query" 7 | local pl_query = require "nvim-treesitter-playground.query" 8 | local Promise = require "nvim-treesitter-playground.promise" 9 | local api = vim.api 10 | local luv = vim.loop 11 | 12 | local ts_compat = require "nvim-treesitter.compat" 13 | 14 | local M = {} 15 | 16 | local fs_mkdir = Promise.promisify(luv.fs_mkdir) 17 | local fs_open = Promise.promisify(luv.fs_open) 18 | local fs_write = Promise.promisify(luv.fs_write) 19 | local fs_close = Promise.promisify(luv.fs_close) 20 | local fs_stat = Promise.promisify(luv.fs_stat) 21 | local fs_fstat = Promise.promisify(luv.fs_fstat) 22 | local fs_read = Promise.promisify(luv.fs_read) 23 | 24 | M._entries = setmetatable({}, { 25 | __index = function(tbl, key) 26 | local entry = rawget(tbl, key) 27 | 28 | if not entry then 29 | entry = { 30 | include_anonymous_nodes = false, 31 | suppress_injected_languages = false, 32 | include_language = false, 33 | include_hl_groups = false, 34 | focused_language_tree = nil, 35 | } 36 | rawset(tbl, key, entry) 37 | end 38 | 39 | return entry 40 | end, 41 | }) 42 | 43 | local query_buf_var_name = "TSPlaygroundForBuf" 44 | local playground_ns = api.nvim_create_namespace "nvim-treesitter-playground" 45 | local query_hl_ns = api.nvim_create_namespace "nvim-treesitter-playground-query" 46 | 47 | local augroup = vim.api.nvim_create_augroup("TSPlayground", {}) 48 | 49 | local function get_node_at_cursor(options) 50 | options = options or {} 51 | 52 | local include_anonymous = options.include_anonymous 53 | local lnum, col = unpack(vim.api.nvim_win_get_cursor(0)) 54 | local root_lang_tree = parsers.get_parser() 55 | 56 | -- This can happen in some scenarios... best not assume. 57 | if not root_lang_tree then 58 | return 59 | end 60 | 61 | local owning_lang_tree = root_lang_tree:language_for_range { lnum - 1, col, lnum - 1, col } 62 | local result 63 | 64 | for _, tree in ipairs(owning_lang_tree:trees()) do 65 | local range = { lnum - 1, col, lnum - 1, col } 66 | 67 | if utils.node_contains(tree:root(), range) then 68 | if include_anonymous then 69 | result = tree:root():descendant_for_range(unpack(range)) 70 | else 71 | result = tree:root():named_descendant_for_range(unpack(range)) 72 | end 73 | 74 | if result then 75 | return result 76 | end 77 | end 78 | end 79 | end 80 | 81 | local function focus_buf(bufnr) 82 | if not bufnr then 83 | return 84 | end 85 | 86 | local windows = vim.fn.win_findbuf(bufnr) 87 | 88 | if windows[1] then 89 | api.nvim_set_current_win(windows[1]) 90 | end 91 | end 92 | 93 | local function close_buf_windows(bufnr) 94 | if not bufnr then 95 | return 96 | end 97 | 98 | utils.for_each_buf_window(bufnr, function(window) 99 | api.nvim_win_close(window, true) 100 | end) 101 | end 102 | 103 | local function close_buf(bufnr) 104 | if not bufnr then 105 | return 106 | end 107 | 108 | close_buf_windows(bufnr) 109 | 110 | if api.nvim_buf_is_loaded(bufnr) then 111 | vim.cmd(string.format("bw! %d", bufnr)) 112 | end 113 | end 114 | 115 | local function clear_entry(bufnr) 116 | local entry = M._entries[bufnr] 117 | 118 | close_buf(entry.display_bufnr) 119 | close_buf(entry.query_bufnr) 120 | M._entries[bufnr] = nil 121 | end 122 | 123 | local function is_buf_visible(bufnr) 124 | local windows = vim.fn.win_findbuf(bufnr) 125 | 126 | return #windows > 0 127 | end 128 | 129 | local function get_update_time() 130 | local config = configs.get_module "playground" 131 | 132 | return config and config.updatetime or 25 133 | end 134 | 135 | local function make_entry_toggle(property, options) 136 | options = options or {} 137 | 138 | local update_fn = options.update_fn or function(entry) 139 | entry[property] = not entry[property] 140 | end 141 | 142 | return function(bufnr) 143 | bufnr = bufnr or api.nvim_get_current_buf() 144 | update_fn(M._entries[bufnr]) 145 | 146 | local current_cursor = vim.api.nvim_win_get_cursor(0) 147 | local node_at_cursor = M.get_current_node(bufnr) 148 | 149 | if options.reprocess then 150 | M.update(bufnr) 151 | else 152 | M.render(bufnr) 153 | end 154 | 155 | -- Restore the cursor to the same node or at least the previous cursor position. 156 | local cursor_pos = current_cursor 157 | local node_entries = M._entries[bufnr].results 158 | 159 | if node_at_cursor then 160 | for lnum, node_entry in ipairs(node_entries) do 161 | if node_entry.node:id() == node_at_cursor:id() then 162 | cursor_pos = { lnum, cursor_pos[2] } 163 | end 164 | end 165 | end 166 | 167 | -- This could be out of bounds 168 | -- TODO(steelsojka): set to end if out of bounds 169 | pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos) 170 | end 171 | end 172 | 173 | local function setup_buf(for_buf) 174 | if M._entries[for_buf].display_bufnr then 175 | return M._entries[for_buf].display_bufnr 176 | end 177 | 178 | local buf = api.nvim_create_buf(false, false) 179 | 180 | api.nvim_buf_set_option(buf, "buftype", "nofile") 181 | api.nvim_buf_set_option(buf, "swapfile", false) 182 | api.nvim_buf_set_option(buf, "buflisted", false) 183 | api.nvim_buf_set_option(buf, "filetype", "tsplayground") 184 | api.nvim_buf_set_var(buf, query_buf_var_name, for_buf) 185 | 186 | vim.api.nvim_clear_autocmds { group = augroup, buffer = buf } 187 | vim.api.nvim_create_autocmd("CursorMoved", { 188 | group = augroup, 189 | buffer = buf, 190 | callback = function() 191 | require("nvim-treesitter-playground.internal").highlight_node(for_buf) 192 | end, 193 | desc = "TSPlayground: highlight node", 194 | }) 195 | vim.api.nvim_create_autocmd("BufLeave", { 196 | group = augroup, 197 | buffer = buf, 198 | callback = function() 199 | require("nvim-treesitter-playground.internal").clear_highlights(for_buf) 200 | end, 201 | desc = "TSPlayground: clear highlights", 202 | }) 203 | vim.api.nvim_create_autocmd("BufWinEnter", { 204 | group = augroup, 205 | buffer = buf, 206 | callback = function() 207 | require("nvim-treesitter-playground.internal").update(for_buf) 208 | end, 209 | desc = "TSPlayground: update", 210 | }) 211 | 212 | local config = configs.get_module "playground" 213 | 214 | for func, mapping in pairs(config.keybindings) do 215 | api.nvim_buf_set_keymap( 216 | buf, 217 | "n", 218 | mapping, 219 | string.format(':lua require "nvim-treesitter-playground.internal".%s(%d)', func, for_buf), 220 | { silent = true, noremap = true } 221 | ) 222 | end 223 | api.nvim_buf_attach(buf, false, { 224 | on_detach = function() 225 | clear_entry(for_buf) 226 | end, 227 | }) 228 | 229 | return buf 230 | end 231 | 232 | local function resolve_lang_tree(bufnr) 233 | local entry = M._entries[bufnr] 234 | 235 | if entry.focused_language_tree then 236 | local root_lang_tree = parsers.get_parser(bufnr) 237 | local found 238 | 239 | root_lang_tree:for_each_child(function(lang_tree) 240 | if not found and lang_tree == entry.focused_language_tree then 241 | found = lang_tree 242 | end 243 | end) 244 | 245 | if found then 246 | return found 247 | end 248 | end 249 | end 250 | 251 | local function setup_query_editor(bufnr) 252 | if M._entries[bufnr].query_bufnr then 253 | return M._entries[bufnr].query_bufnr 254 | end 255 | 256 | local buf = api.nvim_create_buf(false, false) 257 | 258 | api.nvim_buf_set_option(buf, "buftype", "nofile") 259 | api.nvim_buf_set_option(buf, "swapfile", false) 260 | api.nvim_buf_set_option(buf, "buflisted", false) 261 | api.nvim_buf_set_option(buf, "filetype", "query") 262 | api.nvim_buf_set_var(buf, query_buf_var_name, bufnr) 263 | 264 | vim.api.nvim_create_autocmd("CursorMoved", { 265 | group = augroup, 266 | buffer = buf, 267 | callback = function() 268 | require("nvim-treesitter-playground.internal").on_query_cursor_move(bufnr) 269 | end, 270 | desc = "TSPlayground: on query cursor move", 271 | }) 272 | 273 | vim.keymap.set("n", "R", function() 274 | require("nvim-treesitter-playground.internal").update_query(bufnr, buf) 275 | end, { 276 | silent = true, 277 | buffer = buf, 278 | desc = "TSPlayground: update query", 279 | }) 280 | 281 | api.nvim_buf_attach(buf, false, { 282 | on_lines = utils.debounce(function() 283 | M.update_query(bufnr, buf) 284 | end, 1000), 285 | }) 286 | 287 | local config = configs.get_module "playground" 288 | 289 | if config.persist_queries then 290 | M.read_saved_query(bufnr):then_(vim.schedule_wrap(function(lines) 291 | if #lines > 0 then 292 | api.nvim_buf_set_lines(buf, 0, -1, false, lines) 293 | end 294 | end)) 295 | else 296 | api.nvim_buf_set_lines(buf, 0, -1, false, { 297 | ";; Write your query here like `(node) @capture`,", 298 | ";; put the cursor under the capture to highlight the matches.", 299 | }) 300 | end 301 | 302 | return buf 303 | end 304 | 305 | local function get_cache_path() 306 | return vim.fn.stdpath "cache" .. "/nvim_treesitter_playground" 307 | end 308 | 309 | local function get_filename(bufnr) 310 | return vim.fn.fnamemodify(vim.fn.bufname(bufnr), ":t") 311 | end 312 | 313 | function M.save_query_file(bufnr, query) 314 | local cache_path = get_cache_path() 315 | local filename = get_filename(bufnr) 316 | 317 | fs_stat(cache_path) 318 | :catch(function() 319 | return fs_mkdir(cache_path, 493) 320 | end) 321 | :then_(function() 322 | return fs_open(cache_path .. "/" .. filename .. "~", "w", 493) 323 | end) 324 | :then_(function(fd) 325 | return fs_write(fd, query, -1):then_(function() 326 | return fd 327 | end) 328 | end) 329 | :then_(function(fd) 330 | return fs_close(fd) 331 | end) 332 | :catch(function(err) 333 | print(err) 334 | end) 335 | end 336 | 337 | function M.read_saved_query(bufnr) 338 | local cache_path = get_cache_path() 339 | local filename = get_filename(bufnr) 340 | local query_path = cache_path .. "/" .. filename .. "~" 341 | 342 | return fs_open(query_path, "r", 438) 343 | :then_(function(fd) 344 | return fs_fstat(fd) 345 | :then_(function(stat) 346 | return fs_read(fd, stat.size, 0) 347 | end) 348 | :then_(function(data) 349 | return fs_close(fd):then_(function() 350 | return vim.split(data, "\n") 351 | end) 352 | end) 353 | end) 354 | :catch(function() 355 | return {} 356 | end) 357 | end 358 | 359 | function M.focus_language(bufnr) 360 | local node_entry = M.get_current_entry(bufnr) 361 | 362 | if not node_entry then 363 | return 364 | end 365 | 366 | M.update(bufnr, node_entry.language_tree) 367 | end 368 | 369 | function M.unfocus_language(bufnr) 370 | M._entries[bufnr].focused_language_tree = nil 371 | M.update(bufnr) 372 | end 373 | 374 | function M.highlight_playground_nodes(bufnr, nodes) 375 | local entry = M._entries[bufnr] 376 | local results = entry.results 377 | local display_buf = entry.display_bufnr 378 | local lines = {} 379 | local count = 0 380 | local node_map = utils.to_lookup_table(nodes, function(node) 381 | return node:id() 382 | end) 383 | 384 | if not results or not display_buf then 385 | return 386 | end 387 | 388 | for line, result in ipairs(results) do 389 | if node_map[result.node:id()] then 390 | table.insert(lines, line) 391 | count = count + 1 392 | end 393 | 394 | if count >= #nodes then 395 | break 396 | end 397 | end 398 | 399 | for _, lnum in ipairs(lines) do 400 | local buf_lines = api.nvim_buf_get_lines(display_buf, lnum - 1, lnum, false) 401 | 402 | if buf_lines[1] then 403 | vim.api.nvim_buf_add_highlight(display_buf, playground_ns, "TSPlaygroundFocus", lnum - 1, 0, -1) 404 | end 405 | end 406 | 407 | return lines 408 | end 409 | 410 | function M.highlight_playground_node_from_buffer(bufnr) 411 | M.clear_playground_highlights(bufnr) 412 | 413 | local entry = M._entries[bufnr] 414 | local display_buf = entry.display_bufnr 415 | 416 | if not display_buf then 417 | return 418 | end 419 | 420 | local node_at_point = get_node_at_cursor { include_anonymous = entry.include_anonymous_nodes } 421 | 422 | if not node_at_point then 423 | return 424 | end 425 | 426 | local lnums = M.highlight_playground_nodes(bufnr, { node_at_point }) 427 | 428 | if lnums[1] then 429 | utils.for_each_buf_window(display_buf, function(window) 430 | api.nvim_win_set_cursor(window, { lnums[1], 0 }) 431 | end) 432 | end 433 | end 434 | 435 | M._highlight_playground_node_debounced = utils.debounce(M.highlight_playground_node_from_buffer, get_update_time) 436 | 437 | function M.get_current_entry(bufnr) 438 | local row, _ = unpack(api.nvim_win_get_cursor(0)) 439 | local results = M._entries[bufnr].results 440 | 441 | return results and results[row] 442 | end 443 | 444 | function M.get_current_node(bufnr) 445 | local entry = M.get_current_entry(bufnr) 446 | 447 | return entry and entry.node 448 | end 449 | 450 | function M.highlight_node(bufnr) 451 | M.clear_highlights(bufnr) 452 | 453 | local node = M.get_current_node(bufnr) 454 | 455 | if not node then 456 | return 457 | end 458 | 459 | local start_row, start_col, _ = node:start() 460 | local last_row, last_col = utils.get_end_pos(bufnr) 461 | -- Set the cursor to the last column 462 | -- if the node starts at the EOF mark. 463 | if start_row > last_row then 464 | start_row = last_row 465 | start_col = last_col 466 | end 467 | 468 | M.highlight_nodes(bufnr, { node }) 469 | 470 | utils.for_each_buf_window(bufnr, function(window) 471 | api.nvim_win_set_cursor(window, { start_row + 1, start_col }) 472 | end) 473 | end 474 | 475 | function M.highlight_nodes(bufnr, nodes) 476 | for _, node in ipairs(nodes) do 477 | ts_utils.highlight_node(node, bufnr, playground_ns, "TSPlaygroundFocus") 478 | end 479 | end 480 | 481 | function M.goto_node(bufnr) 482 | local bufwin = vim.fn.win_findbuf(bufnr)[1] 483 | if bufwin then 484 | api.nvim_set_current_win(bufwin) 485 | else 486 | local node = M.get_current_node(bufnr) 487 | 488 | local win = api.nvim_get_current_win() 489 | vim.cmd "vsplit" 490 | api.nvim_win_set_buf(win, bufnr) 491 | api.nvim_set_current_win(win) 492 | 493 | ts_utils.goto_node(node) 494 | M.clear_highlights(bufnr) 495 | end 496 | end 497 | 498 | function M.update_query(bufnr, query_bufnr) 499 | local query = table.concat(api.nvim_buf_get_lines(query_bufnr, 0, -1, false), "\n") 500 | local ok, matches = pcall(pl_query.parse, bufnr, query, M._entries[bufnr].focused_language_tree) 501 | if not ok then 502 | vim.notify("playground: " .. vim.inspect(matches), vim.log.levels.ERROR) 503 | return 504 | end 505 | local capture_by_color = {} 506 | local index = 1 507 | 508 | local config = configs.get_module "playground" 509 | 510 | if config.persist_queries then 511 | M.save_query_file(bufnr, query) 512 | end 513 | 514 | M._entries[bufnr].query_results = matches 515 | M._entries[bufnr].captures = {} 516 | M.clear_highlights(query_bufnr, query_hl_ns) 517 | M.clear_highlights(bufnr, query_hl_ns) 518 | 519 | for capture_match in ts_query.iter_group_results(query_bufnr, "captures") do 520 | table.insert(M._entries[bufnr].captures, capture_match.capture) 521 | 522 | local capture = ts_compat.get_node_text(capture_match.capture.name.node, query_bufnr) 523 | 524 | if not capture_by_color[capture] then 525 | capture_by_color[capture] = "TSPlaygroundCapture" .. index 526 | index = index + 1 527 | end 528 | 529 | ts_utils.highlight_node(capture_match.capture.def.node, query_bufnr, query_hl_ns, capture_by_color[capture]) 530 | end 531 | 532 | local node_highlights = {} 533 | 534 | for _, match in ipairs(matches) do 535 | local hl_group = capture_by_color[match.tag] 536 | 537 | if hl_group then 538 | table.insert(node_highlights, { match.node, hl_group }) 539 | end 540 | end 541 | 542 | for _, entry in ipairs(node_highlights) do 543 | ts_utils.highlight_node(entry[1], bufnr, query_hl_ns, entry[2]) 544 | end 545 | end 546 | 547 | function M.highlight_matched_query_nodes_from_capture(bufnr, capture) 548 | local query_results = M._entries[bufnr].query_results 549 | local display_buf = M._entries[bufnr].display_bufnr 550 | 551 | if not query_results then 552 | return 553 | end 554 | 555 | local nodes_to_highlight = {} 556 | 557 | for _, result in ipairs(query_results) do 558 | if result.tag == capture then 559 | table.insert(nodes_to_highlight, result.node) 560 | end 561 | end 562 | 563 | M.highlight_nodes(bufnr, nodes_to_highlight) 564 | 565 | if display_buf then 566 | M.highlight_playground_nodes(bufnr, nodes_to_highlight) 567 | end 568 | end 569 | 570 | function M.on_query_cursor_move(bufnr) 571 | local node_at_point = get_node_at_cursor { include_anonymous = false } 572 | local captures = M._entries[bufnr].captures 573 | 574 | M.clear_highlights(bufnr) 575 | M.clear_highlights(M._entries[bufnr].display_bufnr) 576 | 577 | if not node_at_point or not captures then 578 | return 579 | end 580 | 581 | for _, capture in ipairs(captures) do 582 | local _, _, capture_start = capture.def.node:start() 583 | local _, _, capture_end = capture.def.node:end_() 584 | local _, _, start = node_at_point:start() 585 | local _, _, _end = node_at_point:end_() 586 | local capture_name = ts_compat.get_node_text(capture.name.node, api.nvim_get_current_buf()) 587 | 588 | if start >= capture_start and _end <= capture_end and capture_name then 589 | M.highlight_matched_query_nodes_from_capture(bufnr, capture_name) 590 | break 591 | end 592 | end 593 | end 594 | 595 | function M.clear_highlights(bufnr, namespace) 596 | if not bufnr then 597 | return 598 | end 599 | 600 | namespace = namespace or playground_ns 601 | 602 | api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) 603 | end 604 | 605 | function M.clear_playground_highlights(bufnr) 606 | M.clear_highlights(M._entries[bufnr].display_bufnr) 607 | end 608 | 609 | function M.toggle_query_editor(bufnr) 610 | bufnr = bufnr or api.nvim_get_current_buf() 611 | 612 | local display_buf = M._entries[bufnr].display_bufnr 613 | local current_win = api.nvim_get_current_win() 614 | 615 | if not display_buf then 616 | display_buf = M.open(bufnr) 617 | end 618 | 619 | local query_buf = setup_query_editor(bufnr) 620 | 621 | if is_buf_visible(query_buf) then 622 | close_buf_windows(query_buf) 623 | else 624 | M._entries[bufnr].query_bufnr = query_buf 625 | 626 | focus_buf(display_buf) 627 | vim.cmd "split" 628 | vim.cmd(string.format("buffer %d", query_buf)) 629 | 630 | api.nvim_win_set_option(0, "spell", false) 631 | api.nvim_win_set_option(0, "number", true) 632 | 633 | api.nvim_set_current_win(current_win) 634 | end 635 | end 636 | 637 | function M.open(bufnr) 638 | bufnr = bufnr or api.nvim_get_current_buf() 639 | 640 | local display_buf = setup_buf(bufnr) 641 | local current_window = api.nvim_get_current_win() 642 | 643 | M._entries[bufnr].display_bufnr = display_buf 644 | vim.cmd "vsplit" 645 | vim.cmd(string.format("buffer %d", display_buf)) 646 | 647 | api.nvim_win_set_option(0, "spell", false) 648 | api.nvim_win_set_option(0, "number", false) 649 | api.nvim_win_set_option(0, "relativenumber", false) 650 | api.nvim_win_set_option(0, "cursorline", false) 651 | 652 | api.nvim_set_current_win(current_window) 653 | 654 | return display_buf 655 | end 656 | 657 | function M.toggle(bufnr) 658 | bufnr = bufnr or api.nvim_get_current_buf() 659 | 660 | local success, for_buf = pcall(api.nvim_buf_get_var, bufnr, query_buf_var_name) 661 | 662 | if success and for_buf then 663 | bufnr = for_buf 664 | end 665 | 666 | local display_buf = M._entries[bufnr].display_bufnr 667 | 668 | if display_buf and is_buf_visible(display_buf) then 669 | close_buf_windows(M._entries[bufnr].query_bufnr) 670 | close_buf_windows(display_buf) 671 | else 672 | M.open(bufnr) 673 | end 674 | end 675 | 676 | M.toggle_anonymous_nodes = make_entry_toggle("include_anonymous_nodes", { reprocess = true }) 677 | M.toggle_injected_languages = make_entry_toggle("suppress_injected_languages", { reprocess = true }) 678 | M.toggle_hl_groups = make_entry_toggle("include_hl_groups", { reprocess = true }) 679 | M.toggle_language_display = make_entry_toggle "include_language" 680 | 681 | function M.update(bufnr, lang_tree) 682 | bufnr = bufnr or api.nvim_get_current_buf() 683 | lang_tree = lang_tree or resolve_lang_tree(bufnr) 684 | 685 | local entry = M._entries[bufnr] 686 | local display_buf = entry.display_bufnr 687 | 688 | -- Don't bother updating if the playground isn't shown 689 | if not display_buf or not is_buf_visible(display_buf) then 690 | return 691 | end 692 | 693 | entry.focused_language_tree = lang_tree 694 | 695 | local results = printer.process(bufnr, lang_tree, { 696 | include_anonymous_nodes = entry.include_anonymous_nodes, 697 | suppress_injected_languages = entry.suppress_injected_languages, 698 | include_hl_groups = entry.include_hl_groups, 699 | }) 700 | 701 | M._entries[bufnr].results = results 702 | M.render(bufnr) 703 | end 704 | 705 | function M.render(bufnr) 706 | bufnr = bufnr or api.nvim_get_current_buf() 707 | 708 | local entry = M._entries[bufnr] 709 | local display_buf = entry.display_bufnr 710 | 711 | -- Don't bother updating if the playground isn't shown 712 | if not display_buf or not is_buf_visible(display_buf) then 713 | return 714 | end 715 | 716 | api.nvim_buf_set_lines(display_buf, 0, -1, false, printer.print_entries(entry.results)) 717 | 718 | if entry.query_bufnr then 719 | M.update_query(bufnr, entry.query_bufnr) 720 | end 721 | 722 | if entry.include_language then 723 | printer.print_language(display_buf, entry.results) 724 | else 725 | printer.remove_language(display_buf) 726 | end 727 | 728 | if entry.include_hl_groups then 729 | printer.print_hl_groups(display_buf, entry.results) 730 | else 731 | printer.remove_hl_groups(display_buf) 732 | end 733 | end 734 | 735 | function M.show_help() 736 | local function filter(item, path) 737 | if path[#path] == vim.inspect.METATABLE then 738 | return 739 | end 740 | return item 741 | end 742 | print "Current keybindings:" 743 | print(vim.inspect(configs.get_module("playground").keybindings, { process = filter })) 744 | end 745 | 746 | function M.get_entries() 747 | return M._entries 748 | end 749 | 750 | function M.attach(bufnr) 751 | api.nvim_buf_attach(bufnr, true, { 752 | on_lines = vim.schedule_wrap(utils.debounce(function() 753 | M.update(bufnr) 754 | end, get_update_time)), 755 | }) 756 | 757 | vim.api.nvim_clear_autocmds { group = augroup, buffer = bufnr } 758 | vim.api.nvim_create_autocmd("CursorMoved", { 759 | group = augroup, 760 | buffer = bufnr, 761 | callback = function() 762 | require("nvim-treesitter-playground.internal")._highlight_playground_node_debounced(bufnr) 763 | end, 764 | desc = "TSPlayground: highlight playground node debounce", 765 | }) 766 | vim.api.nvim_create_autocmd("BufLeave", { 767 | group = augroup, 768 | buffer = bufnr, 769 | callback = function() 770 | require("nvim-treesitter-playground.internal").clear_playground_highlights(bufnr) 771 | end, 772 | desc = "TSPlayground: clear playground highlights", 773 | }) 774 | end 775 | 776 | function M.detach(bufnr) 777 | clear_entry(bufnr) 778 | vim.api.nvim_clear_autocmds { group = augroup, buffer = bufnr, event = { "CursorMoved", "BufLeave" } } 779 | end 780 | 781 | return M 782 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground/printer.lua: -------------------------------------------------------------------------------- 1 | local parsers = require "nvim-treesitter.parsers" 2 | local utils = require "nvim-treesitter-playground.utils" 3 | local api = vim.api 4 | 5 | local M = {} 6 | local virt_text_id = api.nvim_create_namespace "TSPlaygroundHlGroups" 7 | local lang_virt_text_id = api.nvim_create_namespace "TSPlaygroundLangGroups" 8 | 9 | local function get_hl_group_for_node(bufnr, node) 10 | local start_row, start_col, _, _ = node:range() 11 | local hlgroups = utils.get_hl_groups_at_position(bufnr, start_row, start_col) 12 | local groups = {} 13 | 14 | if #hlgroups > 0 then 15 | for _, hl in pairs(hlgroups) do 16 | table.insert(groups, "@" .. hl.capture) 17 | end 18 | end 19 | 20 | return groups 21 | end 22 | 23 | local function flatten_node(root, results, level, language_tree, options) 24 | level = level or 0 25 | results = results or {} 26 | 27 | for node, field in root:iter_children() do 28 | if node:named() or options.include_anonymous_nodes then 29 | local node_entry = { 30 | level = level, 31 | node = node, 32 | field = field, 33 | language_tree = language_tree, 34 | hl_groups = options.include_hl_groups and options.bufnr and get_hl_group_for_node(options.bufnr, node) or {}, 35 | } 36 | 37 | table.insert(results, node_entry) 38 | 39 | flatten_node(node, results, level + 1, language_tree, options) 40 | end 41 | end 42 | 43 | return results 44 | end 45 | 46 | local function flatten_lang_tree(lang_tree, results, options) 47 | results = results or {} 48 | 49 | for _, tree in ipairs(lang_tree:trees()) do 50 | local root = tree:root() 51 | local head_entry = nil 52 | local head_entry_index = nil 53 | 54 | for i, node_entry in ipairs(results) do 55 | local is_contained = utils.node_contains(node_entry.node, { root:range() }) 56 | 57 | if is_contained then 58 | if not head_entry then 59 | head_entry = node_entry 60 | head_entry_index = i 61 | else 62 | if node_entry.level >= head_entry.level then 63 | head_entry = node_entry 64 | head_entry_index = i 65 | else 66 | -- If entry contains the root tree but is less specific, then we 67 | -- can exit the loop 68 | break 69 | end 70 | end 71 | end 72 | end 73 | 74 | local insert_index = head_entry_index and head_entry_index or #results 75 | local level = head_entry and head_entry.level + 1 or nil 76 | 77 | local flattened_root = flatten_node(root, nil, level, lang_tree, options) 78 | local i = insert_index + 1 79 | 80 | -- Insert new items into the table at the correct positions 81 | for _, entry in ipairs(flattened_root) do 82 | table.insert(results, i, entry) 83 | i = i + 1 84 | end 85 | end 86 | 87 | if not options.suppress_injected_languages then 88 | for _, child in pairs(lang_tree:children()) do 89 | flatten_lang_tree(child, results, options) 90 | end 91 | end 92 | 93 | return results 94 | end 95 | 96 | function M.process(bufnr, lang_tree, options) 97 | bufnr = bufnr or api.nvim_get_current_buf() 98 | options = options or {} 99 | lang_tree = lang_tree or parsers.get_parser(bufnr) 100 | options.bufnr = options.bufnr or bufnr 101 | 102 | if not lang_tree then 103 | return {} 104 | end 105 | 106 | return flatten_lang_tree(lang_tree, nil, options) 107 | end 108 | 109 | function M.print_entry(node_entry) 110 | local line 111 | local indent = string.rep(" ", node_entry.level) 112 | local node = node_entry.node 113 | local field = node_entry.field 114 | local node_name = node:type() 115 | 116 | if not node:named() then 117 | node_name = string.format([["%s"]], node_name) 118 | node_name = string.gsub(node_name, "\n", "\\n") 119 | end 120 | 121 | if field then 122 | line = string.format("%s%s: %s [%d, %d] - [%d, %d]", indent, field, node_name, node:range()) 123 | else 124 | line = string.format("%s%s [%d, %d] - [%d, %d]", indent, node_name, node:range()) 125 | end 126 | 127 | return line 128 | end 129 | 130 | function M.print_entries(node_entries) 131 | local results = {} 132 | 133 | for _, entry in ipairs(node_entries) do 134 | table.insert(results, M.print_entry(entry)) 135 | end 136 | 137 | return results 138 | end 139 | 140 | function M.print_hl_groups(bufnr, node_entries) 141 | for i, node_entry in ipairs(node_entries) do 142 | local groups = {} 143 | 144 | for j, hl_group in ipairs(node_entry.hl_groups) do 145 | local str = hl_group .. " / " 146 | 147 | if j == #hl_group then 148 | str = string.sub(str, 0, -3) 149 | end 150 | 151 | table.insert(groups, { str, hl_group }) 152 | end 153 | 154 | api.nvim_buf_set_virtual_text(bufnr, virt_text_id, i - 1, groups, {}) 155 | end 156 | end 157 | 158 | function M.print_language(bufnr, node_entries) 159 | for i, node_entry in ipairs(node_entries) do 160 | api.nvim_buf_set_virtual_text(bufnr, lang_virt_text_id, i - 1, { { node_entry.language_tree:lang() } }, {}) 161 | end 162 | end 163 | 164 | function M.remove_hl_groups(bufnr) 165 | api.nvim_buf_clear_namespace(bufnr, virt_text_id, 0, -1) 166 | end 167 | 168 | function M.remove_language(bufnr) 169 | api.nvim_buf_clear_namespace(bufnr, lang_virt_text_id, 0, -1) 170 | end 171 | 172 | return M 173 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground/promise.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local luv = vim.loop 3 | 4 | function M.promisify(fn) 5 | return function(...) 6 | local args = { ... } 7 | return M.new(function(resolve, reject) 8 | table.insert(args, function(err, v) 9 | if err then 10 | return reject(err) 11 | end 12 | 13 | resolve(v) 14 | end) 15 | 16 | fn(unpack(args)) 17 | end) 18 | end 19 | end 20 | 21 | local function set_timeout(timeout, fn) 22 | local timer = luv.new_timer() 23 | 24 | timer:start(timeout, 0, function() 25 | timer:stop() 26 | timer:close() 27 | fn() 28 | end) 29 | 30 | return timer 31 | end 32 | 33 | function M.new(sink) 34 | local p = setmetatable({ 35 | result = nil, 36 | is_resolved = false, 37 | is_errored = false, 38 | cbs = {}, 39 | err_cbs = {}, 40 | }, { 41 | __index = M, 42 | }) 43 | 44 | p._resolve = function(v) 45 | p:_set_result(v, false) 46 | end 47 | p._reject = function(err) 48 | p:_set_result(err, true) 49 | end 50 | 51 | local success, err = pcall(function() 52 | sink(p._resolve, p._reject) 53 | end) 54 | 55 | if not success then 56 | p._reject(err) 57 | end 58 | 59 | return p 60 | end 61 | 62 | function M:then_(on_success, on_error) 63 | local p = self 64 | 65 | return M.new(function(resolve, reject) 66 | table.insert(p.cbs, function(result) 67 | if not on_success then 68 | return resolve(result) 69 | end 70 | 71 | local success, res = pcall(function() 72 | resolve(on_success(result)) 73 | end) 74 | 75 | if not success then 76 | reject(res) 77 | end 78 | 79 | return res 80 | end) 81 | 82 | table.insert(p.err_cbs, function(result) 83 | if not on_error then 84 | return reject(result) 85 | end 86 | 87 | local success, res = pcall(function() 88 | resolve(on_error(result)) 89 | end) 90 | 91 | if not success then 92 | reject(res) 93 | end 94 | 95 | return res 96 | end) 97 | 98 | p:_exec_handlers() 99 | end) 100 | end 101 | 102 | function M:catch(on_error) 103 | return self:then_(nil, on_error) 104 | end 105 | 106 | function M:_exec_handlers() 107 | if self.is_resolved then 108 | for _, cb in ipairs(self.cbs) do 109 | cb(self.result) 110 | end 111 | 112 | self.cbs = {} 113 | self.err_cbs = {} 114 | elseif self.is_errored then 115 | for _, cb in ipairs(self.err_cbs) do 116 | cb(self.result) 117 | end 118 | 119 | self.cbs = {} 120 | self.err_cbs = {} 121 | end 122 | end 123 | 124 | function M:_set_result(result, errored) 125 | local p = self 126 | 127 | set_timeout(0, function() 128 | if p.is_resolved or p.is_errored then 129 | return 130 | end 131 | 132 | if M.is_promise(result) then 133 | return result:then_(p._resolve, p._reject) 134 | end 135 | 136 | p.result = result 137 | 138 | if errored then 139 | p.is_errored = true 140 | else 141 | p.is_resolved = true 142 | end 143 | 144 | p:_exec_handlers() 145 | end) 146 | end 147 | 148 | function M.is_promise(v) 149 | return type(v) == "table" and type(v.then_) == "function" 150 | end 151 | 152 | return M 153 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground/query.lua: -------------------------------------------------------------------------------- 1 | local ts = require "nvim-treesitter.compat" 2 | local ts_query = require "nvim-treesitter.query" 3 | local parsers = require "nvim-treesitter.parsers" 4 | local locals = require "nvim-treesitter.locals" 5 | 6 | local M = {} 7 | 8 | function M.parse(bufnr, query, lang_tree) 9 | lang_tree = lang_tree or parsers.get_parser(bufnr) 10 | 11 | local success, parsed_query = pcall(function() 12 | return ts.parse_query(lang_tree:lang(), query) 13 | end) 14 | 15 | if not success then 16 | return {} 17 | end 18 | 19 | local results = {} 20 | 21 | for _, tree in ipairs(lang_tree:trees()) do 22 | local root = tree:root() 23 | local start_row, _, end_row, _ = root:range() 24 | 25 | for match in ts_query.iter_prepared_matches(parsed_query, root, bufnr, start_row, end_row) do 26 | locals.recurse_local_nodes(match, function(_, node, path) 27 | table.insert(results, { node = node, tag = path }) 28 | end) 29 | end 30 | end 31 | 32 | return results 33 | end 34 | 35 | return M 36 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground/query_linter.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local ts = require "nvim-treesitter.compat" 3 | local queries = require "nvim-treesitter.query" 4 | local parsers = require "nvim-treesitter.parsers" 5 | local utils = require "nvim-treesitter.utils" 6 | local configs = require "nvim-treesitter.configs" 7 | local ts_compat = require "nvim-treesitter.compat" 8 | 9 | local namespace = api.nvim_create_namespace "nvim-playground-lints" 10 | local MAGIC_NODE_NAMES = { "_", "ERROR" } 11 | local playground_module = require "nvim-treesitter-playground.internal" 12 | 13 | local M = {} 14 | 15 | M.lints = {} 16 | M.use_diagnostics = true 17 | M.lint_events = { "BufWrite", "CursorHold" } 18 | 19 | local function show_lints(buf, lints) 20 | if M.use_diagnostics then 21 | local diagnostics = vim.tbl_map(function(lint) 22 | return { 23 | lnum = lint.range[1], 24 | end_lnum = lint.range[3], 25 | col = lint.range[2], 26 | end_col = lint.range[4], 27 | severity = vim.diagnostic.ERROR, 28 | message = lint.message, 29 | } 30 | end, lints) 31 | vim.diagnostic.set(namespace, buf, diagnostics) 32 | end 33 | end 34 | 35 | local function add_lint_for_node(node, buf, error_type, complete_message) 36 | local node_text = ts_compat.get_node_text(node, buf):gsub("\n", " ") 37 | local error_text = complete_message or error_type .. ": " .. node_text 38 | local error_range = { node:range() } 39 | table.insert(M.lints[buf], { type = error_type, range = error_range, message = error_text, node_text = node_text }) 40 | end 41 | 42 | local function table_contains(predicate, table) 43 | for _, elt in pairs(table) do 44 | if predicate(elt) then 45 | return true 46 | end 47 | end 48 | return false 49 | end 50 | 51 | local function query_lang_from_playground_buf(buf) 52 | for lang_buf, entry in pairs(playground_module.get_entries() or {}) do 53 | if entry.query_bufnr == buf then 54 | if entry.focused_language_tree then 55 | return entry.focused_language_tree:lang() 56 | end 57 | 58 | return parsers.get_buf_lang(lang_buf) 59 | end 60 | end 61 | end 62 | 63 | function M.guess_query_lang(buf) 64 | local filename = api.nvim_buf_get_name(buf) 65 | local ok, query_lang = pcall(vim.fn.fnamemodify, filename, ":p:h:t") 66 | query_lang = filename ~= "" and query_lang 67 | query_lang = ok and query_lang 68 | if not query_lang then 69 | query_lang = query_lang_from_playground_buf(buf) 70 | end 71 | return parsers.ft_to_lang(query_lang) 72 | end 73 | 74 | function M.lint(query_buf) 75 | query_buf = query_buf or api.nvim_get_current_buf() 76 | M.clear_virtual_text(query_buf) 77 | M.lints[query_buf] = {} 78 | 79 | local query_lang = M.guess_query_lang(query_buf) 80 | 81 | local ok, parser_info = pcall(vim.treesitter.language.inspect, query_lang) 82 | 83 | if not ok then 84 | return 85 | end 86 | 87 | local matches = queries.get_matches(query_buf, "query-linter-queries") 88 | 89 | for _, m in pairs(matches) do 90 | local error_node = utils.get_at_path(m, "error.node") 91 | 92 | if error_node then 93 | add_lint_for_node(error_node, query_buf, "Syntax Error") 94 | end 95 | 96 | local toplevel_node = utils.get_at_path(m, "toplevel-query.node") 97 | if toplevel_node and query_lang then 98 | local query_text = ts_compat.get_node_text(toplevel_node, query_buf) 99 | local err 100 | ok, err = pcall(ts.parse_query, query_lang, query_text) 101 | if not ok then 102 | add_lint_for_node(toplevel_node, query_buf, "Invalid Query", err) 103 | end 104 | end 105 | 106 | if parser_info and parser_info.symbols then 107 | local named_node = utils.get_at_path(m, "named_node.node") 108 | local anonymous_node = utils.get_at_path(m, "anonymous_node.node") 109 | local node = named_node or anonymous_node 110 | if node then 111 | local node_type = ts_compat.get_node_text(node, query_buf) 112 | 113 | if anonymous_node then 114 | node_type = node_type:gsub('"(.*)".*$', "%1"):gsub("\\(.)", "%1") 115 | end 116 | 117 | local is_named = named_node ~= nil 118 | 119 | local found = vim.tbl_contains(MAGIC_NODE_NAMES, node_type) 120 | or table_contains(function(t) 121 | return node_type == t[1] and is_named == t[2] 122 | end, parser_info.symbols) 123 | 124 | if not found then 125 | add_lint_for_node(node, query_buf, "Invalid Node Type") 126 | end 127 | end 128 | 129 | local field_node = utils.get_at_path(m, "field.node") 130 | 131 | if field_node then 132 | local field_name = ts_compat.get_node_text(field_node, query_buf) 133 | local found = vim.tbl_contains(parser_info.fields, field_name) 134 | if not found then 135 | add_lint_for_node(field_node, query_buf, "Invalid Field") 136 | end 137 | end 138 | end 139 | end 140 | 141 | show_lints(query_buf, M.lints[query_buf]) 142 | return M.lints[query_buf] 143 | end 144 | 145 | function M.clear_virtual_text(buf) 146 | vim.diagnostic.reset(namespace, buf) 147 | end 148 | 149 | function M.attach(buf, _) 150 | M.lints[buf] = {} 151 | 152 | local config = configs.get_module "query_linter" 153 | M.use_diagnostics = config.use_diagnostics 154 | M.lint_events = config.lint_events 155 | 156 | vim.api.nvim_create_autocmd(M.lint_events, { 157 | group = vim.api.nvim_create_augroup("TSPlaygroundLint", {}), 158 | buffer = buf, 159 | callback = function() 160 | require("nvim-treesitter-playground.query_linter").lint(buf) 161 | end, 162 | desc = "TSPlayground: lint query", 163 | }) 164 | end 165 | 166 | function M.detach(buf) 167 | M.lints[buf] = nil 168 | M.clear_virtual_text(buf) 169 | vim.api.nvim_clear_autocmds { group = "TSPlaygroundLint", buffer = buf, event = M.lint_events } 170 | end 171 | 172 | return M 173 | -------------------------------------------------------------------------------- /lua/nvim-treesitter-playground/utils.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local highlighter = require "vim.treesitter.highlighter" 3 | 4 | local M = {} 5 | 6 | function M.debounce(fn, debounce_time) 7 | local timer = vim.loop.new_timer() 8 | local is_debounce_fn = type(debounce_time) == "function" 9 | 10 | return function(...) 11 | timer:stop() 12 | 13 | local time = debounce_time 14 | local args = { ... } 15 | 16 | if is_debounce_fn then 17 | time = debounce_time() 18 | end 19 | 20 | timer:start( 21 | time, 22 | 0, 23 | vim.schedule_wrap(function() 24 | fn(unpack(args)) 25 | end) 26 | ) 27 | end 28 | end 29 | 30 | function M.get_hl_groups_at_position(bufnr, row, col) 31 | local buf_highlighter = highlighter.active[bufnr] 32 | 33 | if not buf_highlighter then 34 | return {} 35 | end 36 | 37 | local matches = {} 38 | 39 | buf_highlighter.tree:for_each_tree(function(tstree, tree) 40 | if not tstree then 41 | return 42 | end 43 | 44 | local root = tstree:root() 45 | local root_start_row, _, root_end_row, _ = root:range() 46 | 47 | -- Only worry about trees within the line range 48 | if root_start_row > row or root_end_row < row then 49 | return 50 | end 51 | 52 | local query = buf_highlighter:get_query(tree:lang()) 53 | 54 | -- Some injected languages may not have highlight queries. 55 | if not query:query() then 56 | return 57 | end 58 | 59 | local iter = query:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1) 60 | 61 | for capture, node, metadata in iter do 62 | local hl = query.hl_cache[capture] 63 | 64 | if hl and vim.treesitter.is_in_node_range(node, row, col) then 65 | local c = query._query.captures[capture] -- name of the capture in the query 66 | if c ~= nil then 67 | table.insert(matches, { capture = c, priority = metadata.priority }) 68 | end 69 | end 70 | end 71 | end, true) 72 | return matches 73 | end 74 | 75 | function M.for_each_buf_window(bufnr, fn) 76 | if not api.nvim_buf_is_loaded(bufnr) then 77 | return 78 | end 79 | 80 | for _, window in ipairs(vim.fn.win_findbuf(bufnr)) do 81 | fn(window) 82 | end 83 | end 84 | 85 | function M.to_lookup_table(list, key_mapper) 86 | local result = {} 87 | 88 | for i, v in ipairs(list) do 89 | local key = v 90 | 91 | if key_mapper then 92 | key = key_mapper(v, i) 93 | end 94 | 95 | result[key] = v 96 | end 97 | 98 | return result 99 | end 100 | 101 | function M.node_contains(node, range) 102 | local start_row, start_col, end_row, end_col = node:range() 103 | local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) 104 | local end_fits = end_row > range[3] or (end_row == range[3] and end_col >= range[4]) 105 | 106 | return start_fits and end_fits 107 | end 108 | 109 | --- Returns a tuple with the position of the last line and last column (0-indexed). 110 | function M.get_end_pos(bufnr) 111 | local bufnr = bufnr or api.nvim_get_current_buf() 112 | local last_row = api.nvim_buf_line_count(bufnr) - 1 113 | local last_line = api.nvim_buf_get_lines(bufnr, last_row, last_row + 1, true)[1] 114 | local last_col = last_line and #last_line or 0 115 | return last_row, last_col 116 | end 117 | 118 | return M 119 | -------------------------------------------------------------------------------- /lua/nvim_treesitter_playground_query_omnifunc.lua: -------------------------------------------------------------------------------- 1 | local query_linter = require "nvim-treesitter-playground.query_linter" 2 | local tsc = require "vim.treesitter.query" 3 | 4 | local M = {} 5 | 6 | function M.omnifunc(findstart, base) 7 | if findstart == 1 then 8 | local start = vim.fn.col "." - 1 9 | local result = vim.fn.matchstrpos(vim.fn.getline("."):sub(1, start), '\\v(["#\\-]|\\w)*$') 10 | return result[2] 11 | end 12 | 13 | local buf = vim.api.nvim_get_current_buf() 14 | local query_lang = query_linter.guess_query_lang(buf) 15 | 16 | local ok, parser_info = pcall(vim.treesitter.language.inspect, query_lang) 17 | 18 | if ok then 19 | local items = {} 20 | for _, f in pairs(parser_info.fields) do 21 | if f:find(base, 1, true) == 1 then 22 | table.insert(items, f .. ":") 23 | end 24 | end 25 | for _, p in pairs(tsc.list_predicates()) do 26 | local text = "#" .. p 27 | local found = text:find(base, 1, true) 28 | if found and found <= 2 then -- with or without '#' 29 | table.insert(items, text) 30 | end 31 | text = "#not-" .. p 32 | found = text:find(base, 1, true) 33 | if found and found <= 2 then -- with or without '#' 34 | table.insert(items, text) 35 | end 36 | end 37 | for _, s in pairs(parser_info.symbols) do 38 | local text = s[2] and s[1] or '"' .. vim.fn.escape(s[1], "\\") .. '"' 39 | if text:find(base, 1, true) == 1 then 40 | table.insert(items, text) 41 | end 42 | end 43 | return { words = items, refresh = "always" } 44 | else 45 | return -2 46 | end 47 | end 48 | 49 | return M 50 | -------------------------------------------------------------------------------- /plugin/nvim-treesitter-playground.lua: -------------------------------------------------------------------------------- 1 | -- setup playground module 2 | require("nvim-treesitter-playground").init() 3 | local api = vim.api 4 | 5 | -- define highlights 6 | local highlights = { 7 | TSPlaygroundFocus = { link = "Visual", default = true }, 8 | TSQueryLinterError = { link = "Error", default = true }, 9 | TSPlaygroundLang = { link = "String", default = true }, 10 | } 11 | for k, v in pairs(highlights) do 12 | api.nvim_set_hl(0, k, v) 13 | end 14 | 15 | -- define commands 16 | api.nvim_create_user_command("TSPlaygroundToggle", function() 17 | require("nvim-treesitter-playground.internal").toggle() 18 | end, {}) 19 | api.nvim_create_user_command("TSNodeUnderCursor", function() 20 | require("nvim-treesitter-playground.hl-info").show_ts_node() 21 | end, {}) 22 | api.nvim_create_user_command("TSCaptureUnderCursor", function() 23 | require("nvim-treesitter-playground.hl-info").show_hl_captures() 24 | end, {}) 25 | ---@deprecated 26 | api.nvim_create_user_command("TSHighlightCapturesUnderCursor", function() 27 | require("nvim-treesitter-playground.hl-info").show_hl_captures() 28 | end, {}) 29 | -------------------------------------------------------------------------------- /queries/query/captures.scm: -------------------------------------------------------------------------------- 1 | (capture name: (identifier) @capture.name) @capture.def 2 | -------------------------------------------------------------------------------- /queries/query/query-linter-queries.scm: -------------------------------------------------------------------------------- 1 | (program [(named_node) (list) (grouping)] @toplevel-query) 2 | (named_node 3 | name: _ @named_node) 4 | (anonymous_node 5 | name: _ @anonymous_node) 6 | (field_definition 7 | name: (identifier) @field) 8 | (ERROR) @error 9 | -------------------------------------------------------------------------------- /syntax/tsplayground.vim: -------------------------------------------------------------------------------- 1 | if exists('b:current_syntax') 2 | finish 3 | endif 4 | 5 | syn match nodeType "[a-zA-Z_]\+" 6 | syn match nodeNumber "\d\+" 7 | syn match nodeOp "[,\-\)]\+" 8 | syn match nodeTag "\k\+:" 9 | syn match nodeAnonymous "\".\+\"" 10 | 11 | hi def link nodeType Identifier 12 | hi def link nodeNumber Number 13 | hi def link nodeOp Operator 14 | hi def link nodeTag Tag 15 | hi def link nodeAnonymous String 16 | 17 | let b:current_syntax = 'tsplayground' 18 | --------------------------------------------------------------------------------