├── .github └── workflows │ ├── lint.yml │ ├── panvimdoc.yml │ └── spec.yml ├── .gitignore ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .stylua.toml ├── .tool-versions ├── README.md ├── doc └── ts-node-action.txt ├── lua └── ts-node-action │ ├── actions │ ├── conceal_string.lua │ ├── cycle_case.lua │ ├── cycle_quotes.lua │ ├── init.lua │ ├── toggle_boolean.lua │ ├── toggle_int_readability.lua │ ├── toggle_multiline.lua │ └── toggle_operator.lua │ ├── filetypes │ ├── c_sharp.lua │ ├── git_rebase.lua │ ├── global.lua │ ├── html.lua │ ├── init.lua │ ├── javascript.lua │ ├── json.lua │ ├── julia.lua │ ├── lua.lua │ ├── php.lua │ ├── python.lua │ ├── r.lua │ ├── ruby.lua │ ├── rust.lua │ ├── sql.lua │ └── yaml.lua │ ├── helpers.lua │ ├── init.lua │ └── repeat.lua ├── run_spec └── spec ├── filetypes ├── c_sharp.lua ├── git_rebase │ └── cycle_command_spec.lua ├── javascript_spec.lua ├── julia_spec.lua ├── lua_spec.lua ├── python │ ├── comparison_operator_spec.lua │ ├── conditional_expression_spec.lua │ ├── conditional_padding_spec.lua │ ├── if_statement_spec.lua │ └── quotes_spec.lua ├── r_spec.lua ├── ruby_spec.lua ├── sql_spec.lua └── yaml_spec.lua ├── init.lua └── spec_helper.lua /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting and style checking 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | 9 | jobs: 10 | stylua: 11 | name: StyLua 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: JohnnyMorganz/stylua-action@v3 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | version: latest 19 | args: --check . 20 | -------------------------------------------------------------------------------- /.github/workflows/panvimdoc.yml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | name: pandoc to vimdoc 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: panvimdoc 15 | uses: kdheepak/panvimdoc@main 16 | with: 17 | vimdoc: ts-node-action 18 | - uses: stefanzweifel/git-auto-commit-action@v4 19 | with: 20 | commit_message: "Auto generate docs" 21 | branch: ${{ github.head_ref }} 22 | -------------------------------------------------------------------------------- /.github/workflows/spec.yml: -------------------------------------------------------------------------------- 1 | name: Spec 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | branches: 10 | - "master" 11 | 12 | # Cancel any in-progress CI runs for a PR if it is updated 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | check_compilation: 19 | strategy: 20 | fail-fast: false 21 | name: Run tests 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Install tree-sitter CLI 26 | run: npm i -g tree-sitter-cli 27 | - name: Test Dependencies 28 | run: | 29 | mkdir -p ~/.local/share/nvim/site/pack/plenary.nvim/start 30 | cd ~/.local/share/nvim/site/pack/plenary.nvim/start 31 | git clone https://github.com/nvim-lua/plenary.nvim 32 | 33 | mkdir -p ~/.local/share/nvim/site/pack/nvim-treesitter.nvim/start 34 | cd ~/.local/share/nvim/site/pack/nvim-treesitter.nvim/start 35 | git clone https://github.com/nvim-treesitter/nvim-treesitter 36 | - name: Install and prepare Neovim 37 | run: | 38 | wget https://github.com/neovim/neovim/releases/download/stable/nvim-linux64.tar.gz 39 | tar -zxf nvim-linux64.tar.gz 40 | sudo ln -s $(pwd)/nvim-linux64/bin/nvim /usr/local/bin 41 | # - name: Setup Parsers Cache 42 | # id: parsers-cache 43 | # uses: actions/cache@v3 44 | # with: 45 | # path: | 46 | # ~/.local/share/nvim/site/pack/nvim-treesitter/start/nvim-treesitter/parser/ 47 | # key: parsers-v1-${{ hashFiles('~/.local/share/nvim/site/pack/nvim-treesitter/start/nvim-treesitter/lockfile.json') }} 48 | 49 | - name: Compile parsers 50 | run: | 51 | nvim --headless -c "TSInstallSync c_sharp ruby python lua javascript julia yaml sql r git_rebase" -c "q" 52 | - name: Tests 53 | env: 54 | ci: "1" 55 | run: | 56 | nvim --headless --noplugin -u spec/init.lua -c "PlenaryBustedDirectory ./spec/ { minimal_init = './spec/init.lua' }" 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spec/support/**/* 2 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | default: true 3 | 4 | # MD033/no-inline-html 5 | MD033: 6 | allowed_elements: 7 | - summary 8 | - details 9 | - a 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/JohnnyMorganz/StyLua 4 | rev: v0.18.1 5 | hooks: 6 | - id: stylua-github 7 | 8 | - repo: https://github.com/executablebooks/mdformat 9 | rev: 0.7.16 10 | hooks: 11 | - id: mdformat 12 | additional_dependencies: 13 | - mdformat-gfm 14 | - mdformat-frontmatter 15 | - mdformat-footnote 16 | - mdformat-tables 17 | - mdformat-toc 18 | 19 | - repo: https://github.com/igorshubovych/markdownlint-cli 20 | rev: v0.35.0 21 | hooks: 22 | - id: markdownlint-fix 23 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 80 2 | indent_width = 2 3 | indent_type = "Spaces" 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | rust 1.71.0 2 | python 3.10.12 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TS Node Action 2 | 3 | A framework for running functions on Tree-sitter nodes, and updating the buffer 4 | with the result. 5 | 6 | 7 | 8 | - [Installation](#installation) 9 | - [Usage](#usage) 10 | - [Configuration](#configuration) 11 | - [Multiple Actions for a Node Type](#multiple-actions-for-a-node-type) 12 | - [Writing your own Node Actions](#writing-your-own-node-actions) 13 | - [Options](#options) 14 | - [API](#api) 15 | - [null-ls Integration](#null-ls-integration) 16 | - [Helpers](#helpers) 17 | - [Builtin Actions](#builtin-actions) 18 | - [Testing](#testing) 19 | - [Contributing](#contributing) 20 | 21 | 22 | 23 | ![cycle case](https://user-images.githubusercontent.com/7228095/210154055-8851210e-e8e1-4ba3-a474-0be373df8d1b.gif) 24 | 25 | ![multiline](https://user-images.githubusercontent.com/7228095/210153839-5009dbed-db7a-4b1c-b5c9-879b90f32a64.gif) 26 | 27 | ![condition formatting](https://user-images.githubusercontent.com/7228095/210153712-8be29018-00a3-427f-8a59-959e705e12c6.gif) 28 | 29 | ![ternerizing](https://user-images.githubusercontent.com/7228095/210153716-2fde6101-352b-4ef9-ba00-0842e6749201.gif) 30 | 31 | ![operator flipflop](https://user-images.githubusercontent.com/7228095/210153726-3f5da644-ae1f-4288-b52b-e12a9c757293.gif) 32 | 33 | ![split join blocks](https://user-images.githubusercontent.com/7228095/210153731-a2c2a717-e7ae-4330-9664-11ba4ed3c005.gif) 34 | 35 | ## Installation 36 | 37 | `Lazy.nvim`: 38 | 39 | ```lua 40 | { 41 | 'ckolkey/ts-node-action', 42 | opts = {}, 43 | }, 44 | ``` 45 | 46 | `packer`: 47 | 48 | ```lua 49 | use({ 50 | 'ckolkey/ts-node-action', 51 | config = function() 52 | require("ts-node-action").setup({}) 53 | end 54 | }) 55 | ``` 56 | 57 | > \[!NOTE\] 58 | > It's not required to call `require("ts-node-action").setup()` to 59 | > initialize the plugin, but a table can be passed into the setup function to 60 | > specify new actions for nodes or additional langs. 61 | 62 | ## Usage 63 | 64 | Bind `require("ts-node-action").node_action` to something. This is left up to 65 | the user. 66 | 67 | For example, this would bind the function to `K`: 68 | 69 | ```lua 70 | vim.keymap.set( 71 | { "n" }, 72 | "K", 73 | require("ts-node-action").node_action, 74 | { desc = "Trigger Node Action" }, 75 | ) 76 | ``` 77 | 78 | If `tpope/vim-repeat` is installed, calling `node_action()` is dot-repeatable. 79 | 80 | If `setup()` is called, user commands `:NodeAction` and `:NodeActionDebug` are 81 | defined. 82 | 83 | See `available_actions()` below for how to set this up with LSP Code Actions. 84 | 85 | ## Configuration 86 | 87 | The `setup()` function accepts a table that conforms to the following schema: 88 | 89 | ```lua 90 | { 91 | ['*'] = { -- Global table is checked for all langs 92 | ["node_type"] = fn, 93 | ... 94 | }, 95 | lang = { 96 | ["node_type"] = fn, 97 | ... 98 | }, 99 | ... 100 | } 101 | ``` 102 | 103 | - `lang` should be the treesitter parser lang, or `'*'` for the global table 104 | - `node_type` should be the value of `vim.treesitter.get_node_at_cursor()` 105 | 106 | A definition on the `lang` table will take precedence over the `*` (global) 107 | table. 108 | 109 | ### Multiple Actions for a Node Type 110 | 111 | To define multiple actions for a node type, structure your `node_type` value as 112 | a table of tables, like so: 113 | 114 | ```lua 115 | ["node_type"] = { 116 | { function_one, name = "Action One" }, 117 | { function_two, name = "Action Two" }, 118 | } 119 | ``` 120 | 121 | `vim.ui.select` will use the value of `name` to when prompting you on which 122 | action to perform. 123 | 124 | If you want to bypass `vim.ui.select` and instead just want all actions to be 125 | applied without prompting, you can pass `ask = false` as an argument in the 126 | `node_type` value. Using the same example as above, it would look like this: 127 | 128 | ```lua 129 | ["node_type"] = { 130 | { function_one, name = "Action One" }, 131 | { function_two, name = "Action Two" }, 132 | ask = false, 133 | } 134 | ``` 135 | 136 | ## Writing your own Node Actions 137 | 138 | All node actions should be a function that takes one argument: the tree-sitter 139 | node under the cursor. 140 | 141 | You can read more about their API via `:help tsnode` 142 | 143 | This function can return one or two values: 144 | 145 | - The first being the text to replace the node with. The replacement text can be 146 | either a `"string"` or `{ "table", "of", "strings" }`. With a table of 147 | strings, each string will be on it's own line. 148 | 149 | - The second (optional) returned value is a table of options. Supported keys 150 | are: `cursor`, `callback`, `format`, and `target`. 151 | 152 | Here's how that can look. 153 | 154 | ```lua 155 | { 156 | cursor = { row = 0, col = 0 }, 157 | callback = function() ... end, 158 | format = true, 159 | target = 160 | } 161 | ``` 162 | 163 | ### Options 164 | 165 | #### `cursor` 166 | 167 | If the `cursor` key is present with an empty table value, the cursor will be 168 | moved to the start of the line where the current node is (`row = 0` `col = 0` 169 | relative to node `start_row` and `start_col`). 170 | 171 | #### `callback` 172 | 173 | If `callback` is present, it will simply get called without arguments after the 174 | buffer has been updated, and after the cursor has been positioned. 175 | 176 | #### `format` 177 | 178 | Boolean value. If `true`, will run `=` operator on new buffer text. Requires 179 | `indentexpr` to be set. 180 | 181 | #### `target` 182 | 183 | TSNode or list of TSNodes. If present, this node will be used as the target for replacement instead 184 | of the node under your cursor. 185 | If list of nodes their combined range will be used for replacement. Note that in this case if the target nodes specified are not next to each other, any thing in between will also be replaced. 186 | 187 | Here's a simplified example of how a node-action function gets called: 188 | 189 | ```lua 190 | local action = node_actions[lang][node:type()] 191 | local replacement, opts = action(node) 192 | replace_node(node, replacement, opts or {}) 193 | ``` 194 | 195 | ## API 196 | 197 | `require("ts-node-action").node_action()` Main function for plugin. Should be 198 | assigned by user, and when called will attempt to run the assigned function for 199 | the node your cursor is currently on. 200 | 201 | ______________________________________________________________________ 202 | 203 | `require("ts-node-action").debug()` Prints some helpful information about the 204 | current node, as well as the loaded node actions for all langs 205 | 206 | ______________________________________________________________________ 207 | 208 | `require("ts-node-action").available_actions()` Exposes the function assigned to 209 | the node your cursor is currently on, as well as its name 210 | 211 | ______________________________________________________________________ 212 | 213 | ## null-ls Integration 214 | 215 | Users can set up integration with 216 | [null-ls](https://github.com/jose-elias-alvarez/null-ls.nvim) and use it to 217 | display available node actions by registering the builtin `ts_node_action` code 218 | action source 219 | 220 | ```lua 221 | local null_ls = require("null-ls") 222 | null_ls.setup({ 223 | sources = { 224 | null_ls.builtins.code_actions.ts_node_action, 225 | ... 226 | } 227 | }) 228 | ``` 229 | 230 | This will present the available node action(s) for the node under your cursor 231 | alongside your `lsp`/`null-ls` code actions. 232 | 233 | ______________________________________________________________________ 234 | 235 | ## Helpers 236 | 237 | ```lua 238 | require("ts-node-action.helpers").node_text(node) 239 | ``` 240 | 241 | ```lua 242 | @node: tsnode 243 | @return: string 244 | ``` 245 | 246 | Returns the text of the specified node. 247 | 248 | ______________________________________________________________________ 249 | 250 | ```lua 251 | require("ts-node-action.helpers").node_is_multiline(node) 252 | ``` 253 | 254 | ```lua 255 | @node: tsnode 256 | @return: boolean 257 | ``` 258 | 259 | Returns true if node spans multiple lines, and false if it's a single line. 260 | 261 | ______________________________________________________________________ 262 | 263 | ```lua 264 | require("ts-node-action.helpers").padded_node_text(node, padding) 265 | ``` 266 | 267 | ```lua 268 | @node: tsnode 269 | @padding: table 270 | @return: string 271 | ``` 272 | 273 | For formatting unnamed tsnodes. For example, if you pass in an unnamed node 274 | representing the text `,`, you could pass in a `padding` table (below) to add a 275 | trailing whitespace to `,` nodes. 276 | 277 | ```lua 278 | { [","] = "%s " } 279 | ``` 280 | 281 | Nodes not specified in table are returned unchanged. 282 | 283 | ## Builtin Actions 284 | 285 |
286 | 287 |

Cycle Case

288 | 289 | ```lua 290 | require("ts-node-action.actions").cycle_case(formats) 291 | ``` 292 | 293 | ```lua 294 | @param formats table|nil 295 | ``` 296 | 297 | `formats` param can be a table of strings specifying the different formats to 298 | cycle through. By default it's 299 | 300 | ```lua 301 | { "snake_case", "pascal_case", "screaming_snake_case", "camel_case" } 302 | ``` 303 | 304 | A table can also be used in place of a string to implement a custom formatter. 305 | Every format is a table that implements the following interface: 306 | 307 | - pattern (string) 308 | - apply (function) 309 | - standardize (function) 310 | 311 | #### `pattern` 312 | 313 | A Lua pattern (string) that matches the format 314 | 315 | #### `apply` 316 | 317 | A function that takes a _table_ of standardized strings as it's argument, and 318 | returns a _string_ in the format 319 | 320 | #### `standardize` 321 | 322 | A function that takes a _string_ in this format, and returns a table of strings, 323 | all lower case, no special chars. ie: 324 | 325 | ```lua 326 | standardize("ts_node_action") -> { "ts", "node", "action" } 327 | standardize("tsNodeAction") -> { "ts", "node", "action" } 328 | standardize("TsNodeAction") -> { "ts", "node", "action" } 329 | standardize("TS_NODE_ACTION") -> { "ts", "node", "action" } 330 | ``` 331 | 332 | > \[!NOTE\] 333 | > The order of formats can be important, as some identifiers are the same 334 | > for multiple formats. Take the string 'action' for example. This is a match for 335 | > both snake*case \_and* camel_case. It's therefore important to place a format 336 | > between those two so we can correctly change the string. 337 | 338 | ______________________________________________________________________ 339 | 340 |
341 | 342 | Builtin actions are all higher-order functions so they can easily have options 343 | overridden on a per-lang basis. Check out the implementations under 344 | `lua/filetypes/` to see how! 345 | 346 | 347 | 348 | | | (\*) | Ruby | js/ts/tsx/jsx | Lua | Python | PHP | Rust | C# | JSON | HTML | YAML | R | 349 | | -------------------------- | ---- | ---- | ------------- | --- | ------ | --- | ---- | --- | ---- | ---- | ---- | --- | 350 | | `toggle_boolean()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | ✅ | ✅ | 351 | | `cycle_case()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | | ✅ | 352 | | `cycle_quotes()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | ✅ | 353 | | `toggle_multiline()` | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | | | ✅ | 354 | | `toggle_operator()` | | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | | | | ✅ | 355 | | `toggle_int_readability()` | | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | | | | 356 | | `toggle_block()` | | ✅ | | | | | | | | | | | 357 | | if/else \<-> ternary | | ✅ | | | ✅ | | | | | | | | 358 | | if block/postfix | | ✅ | | | | | | | | | | | 359 | | `toggle_hash_style()` | | ✅ | | | | | | | | | | | 360 | | `conceal_string()` | | | ✅ | | | | | | | ✅ | | | 361 | 362 | 363 | 364 | ## Testing 365 | 366 | To run the test suite, clone the repo and run `./run_spec`. It should pull all 367 | dependencies into `spec/support/` on first run, then execute the tests. 368 | 369 | This is still a little WIP. 370 | 371 | ## Contributing 372 | 373 | If you come up with something that would be a good fit, pull requests for node 374 | actions are welcome! 375 | 376 | Visit: 377 | -------------------------------------------------------------------------------- /doc/ts-node-action.txt: -------------------------------------------------------------------------------- 1 | *ts-node-action.txt* For NVIM v0.8.0 Last change: 2025 January 23 2 | 3 | ============================================================================== 4 | Table of Contents *ts-node-action-table-of-contents* 5 | 6 | 1. TS Node Action |ts-node-action-ts-node-action| 7 | - Installation |ts-node-action-ts-node-action-installation| 8 | - Usage |ts-node-action-ts-node-action-usage| 9 | - Configuration |ts-node-action-ts-node-action-configuration| 10 | - Writing your own Node Actions|ts-node-action-ts-node-action-writing-your-own-node-actions| 11 | - API |ts-node-action-ts-node-action-api| 12 | - null-ls Integration |ts-node-action-ts-node-action-null-ls-integration| 13 | - Helpers |ts-node-action-ts-node-action-helpers| 14 | - Builtin Actions |ts-node-action-ts-node-action-builtin-actions| 15 | - Testing |ts-node-action-ts-node-action-testing| 16 | - Contributing |ts-node-action-ts-node-action-contributing| 17 | 2. Links |ts-node-action-links| 18 | 19 | ============================================================================== 20 | 1. TS Node Action *ts-node-action-ts-node-action* 21 | 22 | A framework for running functions on Tree-sitter nodes, and updating the buffer 23 | with the result. 24 | 25 | - |ts-node-action-installation| 26 | - |ts-node-action-usage| 27 | - |ts-node-action-configuration| 28 | - |ts-node-action-multiple-actions-for-a-node-type| 29 | - |ts-node-action-writing-your-own-node-actions| 30 | - |ts-node-action-options| 31 | - |ts-node-action-api| 32 | - |ts-node-action-null-ls-integration| 33 | - |ts-node-action-helpers| 34 | - |ts-node-action-builtin-actions| 35 | - |ts-node-action-testing| 36 | - |ts-node-action-contributing| 37 | 38 | 39 | INSTALLATION *ts-node-action-ts-node-action-installation* 40 | 41 | `Lazy.nvim`: 42 | 43 | >lua 44 | { 45 | 'ckolkey/ts-node-action', 46 | opts = {}, 47 | }, 48 | < 49 | 50 | `packer`: 51 | 52 | >lua 53 | use({ 54 | 'ckolkey/ts-node-action', 55 | config = function() 56 | require("ts-node-action").setup({}) 57 | end 58 | }) 59 | < 60 | 61 | 62 | [!NOTE] It’s not required to call `require("ts-node-action").setup()` to 63 | initialize the plugin, but a table can be passed into the setup function to 64 | specify new actions for nodes or additional langs. 65 | 66 | USAGE *ts-node-action-ts-node-action-usage* 67 | 68 | Bind `require("ts-node-action").node_action` to something. This is left up to 69 | the user. 70 | 71 | For example, this would bind the function to `K`: 72 | 73 | >lua 74 | vim.keymap.set( 75 | { "n" }, 76 | "K", 77 | require("ts-node-action").node_action, 78 | { desc = "Trigger Node Action" }, 79 | ) 80 | < 81 | 82 | If `tpope/vim-repeat` is installed, calling `node_action()` is dot-repeatable. 83 | 84 | If `setup()` is called, user commands `:NodeAction` and `:NodeActionDebug` are 85 | defined. 86 | 87 | See `available_actions()` below for how to set this up with LSP Code Actions. 88 | 89 | 90 | CONFIGURATION *ts-node-action-ts-node-action-configuration* 91 | 92 | The `setup()` function accepts a table that conforms to the following schema: 93 | 94 | >lua 95 | { 96 | ['*'] = { -- Global table is checked for all langs 97 | ["node_type"] = fn, 98 | ... 99 | }, 100 | lang = { 101 | ["node_type"] = fn, 102 | ... 103 | }, 104 | ... 105 | } 106 | < 107 | 108 | - `lang` should be the treesitter parser lang, or `'*'` for the global table 109 | - `node_type` should be the value of `vim.treesitter.get_node_at_cursor()` 110 | 111 | A definition on the `lang` table will take precedence over the `*` (global) 112 | table. 113 | 114 | 115 | MULTIPLE ACTIONS FOR A NODE TYPE ~ 116 | 117 | To define multiple actions for a node type, structure your `node_type` value as 118 | a table of tables, like so: 119 | 120 | >lua 121 | ["node_type"] = { 122 | { function_one, name = "Action One" }, 123 | { function_two, name = "Action Two" }, 124 | } 125 | < 126 | 127 | `vim.ui.select` will use the value of `name` to when prompting you on which 128 | action to perform. 129 | 130 | If you want to bypass `vim.ui.select` and instead just want all actions to be 131 | applied without prompting, you can pass `ask = false` as an argument in the 132 | `node_type` value. Using the same example as above, it would look like this: 133 | 134 | >lua 135 | ["node_type"] = { 136 | { function_one, name = "Action One" }, 137 | { function_two, name = "Action Two" }, 138 | ask = false, 139 | } 140 | < 141 | 142 | 143 | WRITING YOUR OWN NODE ACTIONS*ts-node-action-ts-node-action-writing-your-own-node-actions* 144 | 145 | All node actions should be a function that takes one argument: the tree-sitter 146 | node under the cursor. 147 | 148 | You can read more about their API via `:help tsnode` 149 | 150 | This function can return one or two values: 151 | 152 | - The first being the text to replace the node with. The replacement text can be 153 | either a `"string"` or `{ "table", "of", "strings" }`. With a table of strings, 154 | each string will be on it’s own line. 155 | - The second (optional) returned value is a table of options. Supported keys are: 156 | `cursor`, `callback`, `format`, and `target`. 157 | 158 | Here’s how that can look. 159 | 160 | >lua 161 | { 162 | cursor = { row = 0, col = 0 }, 163 | callback = function() ... end, 164 | format = true, 165 | target = 166 | } 167 | < 168 | 169 | 170 | OPTIONS ~ 171 | 172 | 173 | CURSOR 174 | 175 | If the `cursor` key is present with an empty table value, the cursor will be 176 | moved to the start of the line where the current node is (`row = 0` `col = 0` 177 | relative to node `start_row` and `start_col`). 178 | 179 | 180 | CALLBACK 181 | 182 | If `callback` is present, it will simply get called without arguments after the 183 | buffer has been updated, and after the cursor has been positioned. 184 | 185 | 186 | FORMAT 187 | 188 | Boolean value. If `true`, will run `=` operator on new buffer text. Requires 189 | `indentexpr` to be set. 190 | 191 | 192 | TARGET 193 | 194 | TSNode or list of TSNodes. If present, this node will be used as the target for 195 | replacement instead of the node under your cursor. If list of nodes their 196 | combined range will be used for replacement. Note that in this case if the 197 | target nodes specified are not next to each other, any thing in between will 198 | also be replaced. 199 | 200 | Here’s a simplified example of how a node-action function gets called: 201 | 202 | >lua 203 | local action = node_actions[lang][node:type()] 204 | local replacement, opts = action(node) 205 | replace_node(node, replacement, opts or {}) 206 | < 207 | 208 | 209 | API *ts-node-action-ts-node-action-api* 210 | 211 | `require("ts-node-action").node_action()` Main function for plugin. Should be 212 | assigned by user, and when called will attempt to run the assigned function for 213 | the node your cursor is currently on. 214 | 215 | ------------------------------------------------------------------------------ 216 | `require("ts-node-action").debug()` Prints some helpful information about the 217 | current node, as well as the loaded node actions for all langs 218 | 219 | ------------------------------------------------------------------------------ 220 | `require("ts-node-action").available_actions()` Exposes the function assigned 221 | to the node your cursor is currently on, as well as its name 222 | 223 | ------------------------------------------------------------------------------ 224 | 225 | NULL-LS INTEGRATION *ts-node-action-ts-node-action-null-ls-integration* 226 | 227 | Users can set up integration with null-ls 228 | and use it to display 229 | available node actions by registering the builtin `ts_node_action` code action 230 | source 231 | 232 | >lua 233 | local null_ls = require("null-ls") 234 | null_ls.setup({ 235 | sources = { 236 | null_ls.builtins.code_actions.ts_node_action, 237 | ... 238 | } 239 | }) 240 | < 241 | 242 | This will present the available node action(s) for the node under your cursor 243 | alongside your `lsp`/`null-ls` code actions. 244 | 245 | ------------------------------------------------------------------------------ 246 | 247 | HELPERS *ts-node-action-ts-node-action-helpers* 248 | 249 | >lua 250 | require("ts-node-action.helpers").node_text(node) 251 | < 252 | 253 | >lua 254 | @node: tsnode 255 | @return: string 256 | < 257 | 258 | Returns the text of the specified node. 259 | 260 | ------------------------------------------------------------------------------ 261 | >lua 262 | require("ts-node-action.helpers").node_is_multiline(node) 263 | < 264 | 265 | >lua 266 | @node: tsnode 267 | @return: boolean 268 | < 269 | 270 | Returns true if node spans multiple lines, and false if it’s a single line. 271 | 272 | ------------------------------------------------------------------------------ 273 | >lua 274 | require("ts-node-action.helpers").padded_node_text(node, padding) 275 | < 276 | 277 | >lua 278 | @node: tsnode 279 | @padding: table 280 | @return: string 281 | < 282 | 283 | For formatting unnamed tsnodes. For example, if you pass in an unnamed node 284 | representing the text `,`, you could pass in a `padding` table (below) to add a 285 | trailing whitespace to `,` nodes. 286 | 287 | >lua 288 | { [","] = "%s " } 289 | < 290 | 291 | Nodes not specified in table are returned unchanged. 292 | 293 | 294 | BUILTIN ACTIONS *ts-node-action-ts-node-action-builtin-actions* 295 | 296 | Cycle Case ~ 297 | 298 | >lua 299 | require("ts-node-action.actions").cycle_case(formats) 300 | < 301 | 302 | >lua 303 | @param formats table|nil 304 | < 305 | 306 | `formats` param can be a table of strings specifying the different formats to 307 | cycle through. By default it’s 308 | 309 | >lua 310 | { "snake_case", "pascal_case", "screaming_snake_case", "camel_case" } 311 | < 312 | 313 | A table can also be used in place of a string to implement a custom formatter. 314 | Every format is a table that implements the following interface: 315 | 316 | - pattern (string) 317 | - apply (function) 318 | - standardize (function) 319 | 320 | 321 | PATTERN 322 | 323 | A Lua pattern (string) that matches the format 324 | 325 | 326 | APPLY 327 | 328 | A function that takes a _table_ of standardized strings as it’s argument, and 329 | returns a _string_ in the format 330 | 331 | 332 | STANDARDIZE 333 | 334 | A function that takes a _string_ in this format, and returns a table of 335 | strings, all lower case, no special chars. ie: 336 | 337 | >lua 338 | standardize("ts_node_action") -> { "ts", "node", "action" } 339 | standardize("tsNodeAction") -> { "ts", "node", "action" } 340 | standardize("TsNodeAction") -> { "ts", "node", "action" } 341 | standardize("TS_NODE_ACTION") -> { "ts", "node", "action" } 342 | < 343 | 344 | 345 | [!NOTE] The order of formats can be important, as some identifiers are the same 346 | for multiple formats. Take the string 'action' for example. This is a match for 347 | both snake_case _and_ camel_case. It’s therefore important to place a format 348 | between those two so we can correctly change the string. 349 | ------------------------------------------------------------------------------ 350 | Builtin actions are all higher-order functions so they can easily have options 351 | overridden on a per-lang basis. Check out the implementations under 352 | `lua/filetypes/` to see how! 353 | 354 | ------------------------------------------------------------------------------------------------------------------ 355 | (*) Ruby js/ts/tsx/jsx Lua Python PHP Rust C# JSON HTML YAML R 356 | -------------------------- ----- ------ --------------- ----- -------- ----- ------ ---- ------ ------ ------ ---- 357 | toggle_boolean() ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ 358 | 359 | cycle_case() ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ 360 | 361 | cycle_quotes() ✅ ✅ ✅ ✅ ✅ ✅ ✅ 362 | 363 | toggle_multiline() ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ 364 | 365 | toggle_operator() ✅ ✅ ✅ ✅ ✅ ✅ ✅ 366 | 367 | toggle_int_readability() ✅ ✅ ✅ ✅ ✅ ✅ ✅ 368 | 369 | toggle_block() ✅ 370 | 371 | if/else <-> ternary ✅ ✅ 372 | 373 | if block/postfix ✅ 374 | 375 | toggle_hash_style() ✅ 376 | 377 | conceal_string() ✅ ✅ 378 | ------------------------------------------------------------------------------------------------------------------ 379 | 380 | TESTING *ts-node-action-ts-node-action-testing* 381 | 382 | To run the test suite, clone the repo and run `./run_spec`. It should pull all 383 | dependencies into `spec/support/` on first run, then execute the tests. 384 | 385 | This is still a little WIP. 386 | 387 | 388 | CONTRIBUTING *ts-node-action-ts-node-action-contributing* 389 | 390 | If you come up with something that would be a good fit, pull requests for node 391 | actions are welcome! 392 | 393 | Visit: 394 | 395 | ============================================================================== 396 | 2. Links *ts-node-action-links* 397 | 398 | 1. *cycle case*: https://user-images.githubusercontent.com/7228095/210154055-8851210e-e8e1-4ba3-a474-0be373df8d1b.gif 399 | 2. *multiline*: https://user-images.githubusercontent.com/7228095/210153839-5009dbed-db7a-4b1c-b5c9-879b90f32a64.gif 400 | 3. *condition formatting*: https://user-images.githubusercontent.com/7228095/210153712-8be29018-00a3-427f-8a59-959e705e12c6.gif 401 | 4. *ternerizing*: https://user-images.githubusercontent.com/7228095/210153716-2fde6101-352b-4ef9-ba00-0842e6749201.gif 402 | 5. *operator flipflop*: https://user-images.githubusercontent.com/7228095/210153726-3f5da644-ae1f-4288-b52b-e12a9c757293.gif 403 | 6. *split join blocks*: https://user-images.githubusercontent.com/7228095/210153731-a2c2a717-e7ae-4330-9664-11ba4ed3c005.gif 404 | 405 | Generated by panvimdoc 406 | 407 | vim:tw=78:ts=8:noet:ft=help:norl: 408 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/conceal_string.lua: -------------------------------------------------------------------------------- 1 | local namespace = vim.api.nvim_create_namespace("ts_node_action_conceal") 2 | 3 | return function(char, level, cursor) 4 | char = char or "" 5 | level = level or 2 6 | cursor = cursor or "nc" 7 | 8 | local function action(node) 9 | vim.api.nvim_win_set_option(0, "concealcursor", cursor) 10 | vim.api.nvim_win_set_option(0, "conceallevel", level) 11 | 12 | local start_row, start_col, end_row, end_col = node:range() 13 | local extmark_id = unpack( 14 | vim.api.nvim_buf_get_extmarks( 15 | 0, 16 | namespace, 17 | { start_row, start_col }, 18 | { end_row, end_col }, 19 | {} 20 | )[1] or {} 21 | ) 22 | 23 | if extmark_id then 24 | vim.api.nvim_buf_del_extmark(0, namespace, extmark_id) 25 | else 26 | vim.api.nvim_buf_set_extmark( 27 | 0, 28 | namespace, 29 | start_row, 30 | start_col, 31 | { end_row = end_row, end_col = end_col, conceal = char } 32 | ) 33 | end 34 | end 35 | 36 | return { { action, name = "Conceal String" } } 37 | end 38 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/cycle_case.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | 3 | -- API Notes: 4 | -- Every format is a table that implements the following three keys: 5 | -- - pattern 6 | -- - apply 7 | -- - standardize 8 | -- 9 | -- # Pattern 10 | -- A Lua pattern (string) that matches the format 11 | -- 12 | -- # Apply 13 | -- A function that takes a _table_ of standardized strings as it's argument, and returns a _string_ in the format 14 | -- 15 | -- # Standardize 16 | -- A function that takes a _string_ in this format, and returns a table of strings, all lower case, no special chars. 17 | -- ie: standardize("ts_node_action") -> { "ts", "node", "action" } 18 | -- standardize("tsNodeAction") -> { "ts", "node", "action" } 19 | -- standardize("TsNodeAction") -> { "ts", "node", "action" } 20 | -- standardize("TS_NODE_ACTION") -> { "ts", "node", "action" } 21 | -- 22 | -- NOTE: The order of formats can be important, as some identifiers are the same for multiple formats. 23 | -- Take the string 'action' for example. This is a match for both snake_case _and_ camel_case. It's 24 | -- therefore important to place a format between those two so we can correcly change the string. 25 | 26 | local format_table = { 27 | snake_case = { 28 | pattern = "^%l+[%l_]*$", 29 | apply = function(tbl) 30 | return string.lower(table.concat(tbl, "_")) 31 | end, 32 | standardize = function(text) 33 | return vim.split(string.lower(text), "_", { trimempty = true }) 34 | end, 35 | }, 36 | camel_case = { 37 | pattern = "^%l+[%u%l]*$", 38 | apply = function(tbl) 39 | local tmp = vim.tbl_map(function(word) 40 | return word:gsub("^.", string.upper) 41 | end, tbl) 42 | local value, _ = table.concat(tmp, ""):gsub("^.", string.lower) 43 | return value 44 | end, 45 | standardize = function(text) 46 | return vim.split( 47 | text 48 | :gsub(".%f[%l]", " %1") 49 | :gsub("%l%f[%u]", "%1 ") 50 | :gsub("^.", string.upper), 51 | " ", 52 | { trimempty = true } 53 | ) 54 | end, 55 | }, 56 | pascal_case = { 57 | pattern = "^%u%l+[%u%l]*$", 58 | apply = function(tbl) 59 | local value, _ = table.concat( 60 | vim.tbl_map(function(word) 61 | return word:gsub("^.", string.upper) 62 | end, tbl), 63 | "" 64 | ) 65 | return value 66 | end, 67 | standardize = function(text) 68 | return vim.split( 69 | text 70 | :gsub(".%f[%l]", " %1") 71 | :gsub("%l%f[%u]", "%1 ") 72 | :gsub("^.", string.upper), 73 | " ", 74 | { trimempty = true } 75 | ) 76 | end, 77 | }, 78 | screaming_snake_case = { 79 | pattern = "^%u+[%u_]*$", 80 | apply = function(tbl) 81 | local value, _ = table.concat( 82 | vim.tbl_map(function(word) 83 | return word:upper() 84 | end, tbl), 85 | "_" 86 | ) 87 | 88 | return value 89 | end, 90 | standardize = function(text) 91 | return vim.split(string.lower(text), "_", { trimempty = true }) 92 | end, 93 | }, 94 | } 95 | 96 | local function check_pattern(text, pattern) 97 | return not not string.find(text, pattern) 98 | end 99 | 100 | local default_formats = 101 | { "snake_case", "pascal_case", "screaming_snake_case", "camel_case" } 102 | 103 | return function(user_formats) 104 | user_formats = user_formats or default_formats 105 | 106 | local formats = {} 107 | for _, format in ipairs(user_formats) do 108 | if type(format) == "string" then 109 | format = format_table[format] 110 | end 111 | 112 | if format then 113 | table.insert(formats, format) 114 | else 115 | print("TS:NodeAction:CycleCase - Format '" .. format .. "' is invalid") 116 | end 117 | end 118 | 119 | local function action(node) 120 | local text = helpers.node_text(node) 121 | 122 | for i, format in ipairs(formats) do 123 | if check_pattern(text, format.pattern) then 124 | local next_i = i + 1 > #formats and 1 or i + 1 125 | local apply = formats[next_i].apply 126 | local standardize = format.standardize 127 | 128 | return apply(standardize(text)) 129 | end 130 | end 131 | end 132 | 133 | return { { action, name = "Cycle Case" } } 134 | end 135 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/cycle_quotes.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | 3 | return function(quotes) 4 | quotes = quotes or { { "'", "'" }, { '"', '"' } } 5 | 6 | local function action(node) 7 | local text = helpers.node_text(node) 8 | for i, char in ipairs(quotes) do 9 | if string.sub(text, 1, #char[1]) == char[1] then 10 | local next = quotes[i + 1 > #quotes and 1 or i + 1] 11 | local substring = string.sub(text, #char[1] + 1, -(#char[2] + 1)) 12 | 13 | return next[1] .. substring .. next[2], { cursor = {} } 14 | end 15 | end 16 | end 17 | 18 | return { { action, name = "Cycle Quotes" } } 19 | end 20 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/init.lua: -------------------------------------------------------------------------------- 1 | return { 2 | cycle_case = require("ts-node-action.actions.cycle_case"), 3 | toggle_boolean = require("ts-node-action.actions.toggle_boolean"), 4 | toggle_multiline = require("ts-node-action.actions.toggle_multiline"), 5 | toggle_operator = require("ts-node-action.actions.toggle_operator"), 6 | cycle_quotes = require("ts-node-action.actions.cycle_quotes"), 7 | conceal_string = require("ts-node-action.actions.conceal_string"), 8 | toggle_int_readability = require( 9 | "ts-node-action.actions.toggle_int_readability" 10 | ), 11 | } 12 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/toggle_boolean.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | 3 | local boolean_pair_default = { 4 | ["true"] = "false", 5 | ["false"] = "true", 6 | ["True"] = "False", 7 | ["False"] = "True", 8 | ["TRUE"] = "FALSE", 9 | ["FALSE"] = "TRUE", 10 | } 11 | 12 | return function(boolean_pair_override) 13 | local boolean_pair = vim.tbl_deep_extend( 14 | "force", 15 | boolean_pair_default, 16 | boolean_pair_override or {} 17 | ) 18 | 19 | local function action(node) 20 | return boolean_pair[helpers.node_text(node)] or helpers.node_text(node) 21 | end 22 | 23 | return { { action, name = "Toggle Boolean" } } 24 | end 25 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/toggle_int_readability.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | 3 | local function group_string(string, group_size) 4 | local groups = {} 5 | 6 | while #string > group_size do 7 | table.insert(groups, string:sub(1, group_size)) 8 | string = string:sub(group_size + 1) 9 | end 10 | 11 | table.insert(groups, string) 12 | 13 | return groups 14 | end 15 | 16 | return function(delimiter) 17 | delimiter = delimiter or "_" 18 | 19 | local function action(node) 20 | local text = helpers.node_text(node) 21 | if #text > 3 then 22 | if string.find(text, delimiter) then 23 | text = text:gsub(delimiter, "") 24 | else 25 | text = 26 | table.concat(group_string(text:reverse(), 3), delimiter):reverse() 27 | end 28 | end 29 | 30 | return text 31 | end 32 | 33 | return { { action, name = "Toggle Integer Format" } } 34 | end 35 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/toggle_multiline.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | 3 | ---@param padding table Used to specify string formatting for unnamed nodes 4 | ---@param uncollapsible table Used to specify "base" types that shouldn't be collapsed further. 5 | ---@return function 6 | local function collapse_child_nodes(padding, uncollapsible) 7 | local function can_be_collapsed(child) 8 | return child:named_child_count() > 0 and not uncollapsible[child:type()] 9 | end 10 | 11 | return function(node) 12 | local replacement = {} 13 | 14 | for child, _ in node:iter_children() do 15 | if can_be_collapsed(child) then 16 | local child_text = collapse_child_nodes(padding, uncollapsible)(child) 17 | if not child_text then 18 | return 19 | end -- We found a comment, abort 20 | 21 | table.insert(replacement, child_text) 22 | elseif child:extra() then -- Comment node 23 | return 24 | elseif child:named() then -- identifiers, strings, numbers, etc. 25 | table.insert(replacement, helpers.node_text(child)) 26 | else 27 | table.insert(replacement, helpers.padded_node_text(child, padding)) 28 | end 29 | end 30 | 31 | return table.concat(vim.tbl_flatten(replacement)) 32 | end 33 | end 34 | 35 | ---@param node TSNode 36 | ---@return table 37 | local function expand_child_nodes(node) 38 | local replacement = {} 39 | 40 | for child in node:iter_children() do 41 | if child:named() then 42 | table.insert(replacement, helpers.node_text(child)) 43 | else 44 | if child:next_sibling() and child:prev_sibling() then 45 | replacement[#replacement] = replacement[#replacement] 46 | .. helpers.node_text(child) 47 | elseif not child:prev_sibling() then -- Opening brace 48 | table.insert(replacement, helpers.node_text(child)) 49 | else -- Closing brace 50 | table.insert(replacement, helpers.node_text(child)) 51 | end 52 | end 53 | end 54 | 55 | return replacement 56 | end 57 | 58 | ---@param padding? table 59 | ---@param uncollapsible? table 60 | ---@return table 61 | return function(padding, uncollapsible) 62 | padding = padding or {} 63 | uncollapsible = uncollapsible or {} 64 | 65 | local function action(node) 66 | local fn 67 | if helpers.node_is_multiline(node) then 68 | fn = collapse_child_nodes(padding, uncollapsible) 69 | else 70 | fn = expand_child_nodes 71 | end 72 | 73 | return fn(node), { cursor = {}, format = true } 74 | end 75 | 76 | return { { action, name = "Toggle Multiline" } } 77 | end 78 | -------------------------------------------------------------------------------- /lua/ts-node-action/actions/toggle_operator.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | 3 | local default_operators = { 4 | ["!="] = "==", 5 | ["=="] = "!=", 6 | [">"] = "<", 7 | ["<"] = ">", 8 | [">="] = "<=", 9 | ["<="] = ">=", 10 | } 11 | 12 | return function(operator_override) 13 | local operators = 14 | vim.tbl_extend("force", default_operators, operator_override or {}) 15 | 16 | local function action(node) 17 | if node:child_count() == 0 then 18 | local text = helpers.node_text(node) 19 | if operators[text] then 20 | return operators[text] 21 | end 22 | else 23 | for child, _ in node:iter_children() do 24 | if child:named() == false then 25 | local text = helpers.node_text(child) 26 | if operators[text] then 27 | return operators[text], { target = child } 28 | end 29 | end 30 | end 31 | end 32 | end 33 | 34 | return { { action, name = "Toggle Operator" } } 35 | end 36 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/c_sharp.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | local operators = { 4 | ["!="] = "==", 5 | ["=="] = "!=", 6 | [">"] = "<", 7 | ["<"] = ">", 8 | [">="] = "<=", 9 | ["<="] = ">=", 10 | ["-"] = "+", 11 | ["+"] = "-", 12 | ["*"] = "/", 13 | ["/"] = "*", 14 | ["+="] = "-=", 15 | ["-="] = "+=", 16 | ["++"] = "--", 17 | ["--"] = "++", 18 | ["||"] = "&&", 19 | ["&&"] = "||", 20 | } 21 | 22 | local modifiers = { 23 | ["public"] = "private", 24 | ["private"] = "public", 25 | ["struct"] = "class", 26 | ["class"] = "struct", 27 | } 28 | 29 | return { 30 | ["boolean_literal"] = actions.toggle_boolean(), 31 | ["binary"] = actions.toggle_operator(operators), 32 | ["modifier"] = actions.toggle_operator(modifiers), 33 | ["struct_declaration"] = actions.toggle_operator(modifiers), 34 | ["class_declaration"] = actions.toggle_operator(modifiers), 35 | ["binary_expression"] = actions.toggle_operator(operators), 36 | ["assignment_operator"] = actions.toggle_operator(operators), 37 | ["postfix_unary_expression"] = actions.toggle_operator(operators), 38 | ["integer_literal"] = actions.toggle_int_readability(), 39 | } 40 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/git_rebase.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | 3 | local function cycle_command(node) 4 | local text = helpers.node_text(node) 5 | if text == "pick" or text == "p" then 6 | return "fixup" 7 | elseif text == "fixup" or text == "f" then 8 | return "reword" 9 | elseif text == "reword" or text == "r" then 10 | return "edit" 11 | elseif text == "edit" or text == "e" then 12 | return "squash" 13 | elseif text == "squash" or text == "s" then 14 | return "exec" 15 | elseif text == "exec" or text == "x" then 16 | return "break" 17 | elseif text == "break" or text == "b" then 18 | return "drop" 19 | elseif text == "drop" or text == "d" then 20 | return "label" 21 | elseif text == "label" or text == "l" then 22 | return "reset" 23 | elseif text == "reset" or text == "t" then 24 | return "merge" 25 | elseif text == "merge" or text == "m" then 26 | return "pick" 27 | end 28 | end 29 | 30 | return { 31 | ["command"] = cycle_command, 32 | } 33 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/global.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | return { 4 | ["true"] = actions.toggle_boolean(), 5 | ["false"] = actions.toggle_boolean(), 6 | ["boolean"] = actions.toggle_boolean(), 7 | ["identifier"] = actions.cycle_case(), 8 | ["variable_name"] = actions.cycle_case(), 9 | ["string"] = actions.cycle_quotes(), 10 | } 11 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/html.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | return { 4 | ["attribute_value"] = actions.conceal_string(), 5 | } 6 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/init.lua: -------------------------------------------------------------------------------- 1 | return { 2 | ["*"] = require("ts-node-action.filetypes.global"), 3 | lua = require("ts-node-action.filetypes.lua"), 4 | json = require("ts-node-action.filetypes.json"), 5 | julia = require("ts-node-action.filetypes.julia"), 6 | yaml = require("ts-node-action.filetypes.yaml"), 7 | ruby = require("ts-node-action.filetypes.ruby"), 8 | eruby = require("ts-node-action.filetypes.ruby"), 9 | python = require("ts-node-action.filetypes.python"), 10 | php = require("ts-node-action.filetypes.php"), 11 | php_only = require("ts-node-action.filetypes.php"), 12 | rust = require("ts-node-action.filetypes.rust"), 13 | html = require("ts-node-action.filetypes.html"), 14 | javascript = require("ts-node-action.filetypes.javascript"), 15 | javascriptreact = require("ts-node-action.filetypes.javascript"), 16 | typescript = require("ts-node-action.filetypes.javascript"), 17 | typescriptreact = require("ts-node-action.filetypes.javascript"), 18 | svelte = require("ts-node-action.filetypes.javascript"), 19 | sql = require("ts-node-action.filetypes.sql"), 20 | r = require("ts-node-action.filetypes.r"), 21 | git_rebase = require("ts-node-action.filetypes.git_rebase"), 22 | c_sharp = require("ts-node-action.filetypes.c_sharp"), 23 | } 24 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/javascript.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | local operators = { 4 | ["!="] = "==", 5 | ["!=="] = "===", 6 | ["=="] = "!=", 7 | ["==="] = "!==", 8 | [">"] = "<", 9 | ["<"] = ">", 10 | [">="] = "<=", 11 | ["<="] = ">=", 12 | } 13 | 14 | local padding = { 15 | [","] = "%s ", 16 | [":"] = "%s ", 17 | ["{"] = "%s ", 18 | ["}"] = " %s", 19 | } 20 | 21 | return { 22 | ["property_identifier"] = actions.cycle_case(), 23 | ["string_fragment"] = actions.conceal_string(), 24 | ["binary_expression"] = actions.toggle_operator(operators), 25 | ["object"] = actions.toggle_multiline(padding), 26 | ["array"] = actions.toggle_multiline(padding), 27 | ["statement_block"] = actions.toggle_multiline(padding), 28 | ["object_pattern"] = actions.toggle_multiline(padding), 29 | ["object_type"] = actions.toggle_multiline(padding), 30 | ["formal_parameters"] = actions.toggle_multiline(padding), 31 | ["arguments"] = actions.toggle_multiline(padding), 32 | ["number"] = actions.toggle_int_readability(), 33 | } 34 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/json.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | local padding = { 4 | [","] = "%s ", 5 | [":"] = "%s ", 6 | } 7 | 8 | return { 9 | ["object"] = actions.toggle_multiline(padding), 10 | ["array"] = actions.toggle_multiline(padding), 11 | } 12 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/julia.lua: -------------------------------------------------------------------------------- 1 | local operators = { 2 | [">"] = "<", 3 | ["<"] = ">", 4 | [">="] = "<=", 5 | ["<="] = ">=", 6 | ["+"] = "-", 7 | ["-"] = "+", 8 | ["*"] = "/", 9 | ["/"] = "*", 10 | ["!="] = "==", 11 | ["=="] = "!=", 12 | ["∉"] = "∈", 13 | ["∈"] = "∉", 14 | } 15 | 16 | local boolean = { 17 | ["true"] = "false", 18 | ["false"] = "true", 19 | } 20 | 21 | local padding = { 22 | [","] = "%s ", 23 | [";"] = "%s ", 24 | } 25 | 26 | local actions = require("ts-node-action.actions") 27 | return { 28 | ["identifier"] = actions.cycle_case(), 29 | ["boolean_literal"] = actions.toggle_boolean(boolean), 30 | ["integer_literal"] = actions.toggle_int_readability(), 31 | ["argument_list"] = actions.toggle_multiline(padding, {}), 32 | ["vector_expression"] = actions.toggle_multiline(padding, {}), 33 | ["tuple_expression"] = actions.toggle_multiline(padding, {}), 34 | ["operator"] = actions.toggle_operator(operators), 35 | } 36 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/lua.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | local helpers = require("ts-node-action.helpers") 3 | 4 | local padding = { 5 | [","] = "%s ", 6 | ["{"] = "%s ", 7 | ["}"] = " %s", 8 | ["="] = " %s ", 9 | ["or"] = " %s ", 10 | ["and"] = " %s ", 11 | ["+"] = " %s ", 12 | ["-"] = " %s ", 13 | ["*"] = " %s ", 14 | ["/"] = " %s ", 15 | [".."] = " %s ", 16 | } 17 | 18 | local operator_override = { 19 | ["=="] = "~=", 20 | ["~="] = "==", 21 | } 22 | 23 | local quote_override = { 24 | { "'", "'" }, 25 | { '"', '"' }, 26 | { "[[", "]]" }, 27 | } 28 | 29 | local uncollapsible = { 30 | ["string"] = true, 31 | } 32 | 33 | local function toggle_function(node) 34 | local struct = helpers.destructure_node(node) 35 | if type(struct.body) == "table" then 36 | return 37 | end 38 | 39 | if helpers.node_is_multiline(node) then 40 | local body = struct.body and (struct.body .. " ") or "" 41 | return "function" .. struct.parameters .. " " .. body .. "end" 42 | else 43 | return { "function" .. struct.parameters, struct.body or "", "end" }, { 44 | format = true, 45 | cursor = {}, 46 | } 47 | end 48 | end 49 | 50 | local function toggle_named_function(node) 51 | local struct = helpers.destructure_node(node) 52 | if type(struct.body) == "table" then 53 | return 54 | end 55 | 56 | if helpers.node_is_multiline(node) then 57 | return (struct["local"] and "local " or "") 58 | .. "function " 59 | .. struct.name 60 | .. struct.parameters 61 | .. " " 62 | .. struct.body 63 | .. " end" 64 | else 65 | return { 66 | (struct["local"] and "local " or "") 67 | .. "function " 68 | .. struct.name 69 | .. struct.parameters, 70 | struct.body, 71 | "end", 72 | }, { format = true, cursor = { col = struct["local"] and 6 or 0 } } 73 | end 74 | end 75 | 76 | return { 77 | ["table_constructor"] = actions.toggle_multiline(padding, uncollapsible), 78 | ["arguments"] = actions.toggle_multiline(padding, uncollapsible), 79 | ["binary_expression"] = actions.toggle_operator(operator_override), 80 | ["string"] = actions.cycle_quotes(quote_override), 81 | ["function_definition"] = { { toggle_function, "Toggle Function" } }, 82 | ["function_declaration"] = { { toggle_named_function, "Toggle Function" } }, 83 | } 84 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/php.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | local padding = { 4 | [","] = "%s ", 5 | ["=>"] = " %s ", 6 | ["="] = " %s ", 7 | ["["] = "%s", 8 | ["]"] = "%s", 9 | ["}"] = " %s", 10 | ["{"] = "%s ", 11 | ["||"] = " %s ", 12 | ["&&"] = " %s ", 13 | ["."] = " %s ", 14 | ["+"] = " %s ", 15 | ["*"] = " %s ", 16 | ["-"] = " %s ", 17 | ["/"] = " %s ", 18 | } 19 | 20 | local operators = { 21 | ["!="] = "==", 22 | ["!=="] = "===", 23 | ["=="] = "!=", 24 | ["==="] = "!==", 25 | [">"] = "<", 26 | ["<"] = ">", 27 | [">="] = "<=", 28 | ["<="] = ">=", 29 | } 30 | 31 | return { 32 | ["array_creation_expression"] = actions.toggle_multiline(padding), 33 | ["formal_parameters"] = actions.toggle_multiline(padding), 34 | ["arguments"] = actions.toggle_multiline(padding), 35 | ["subscript_expression"] = actions.toggle_multiline(padding), 36 | ["compound_statement"] = actions.toggle_multiline(padding), 37 | ["name"] = actions.cycle_case(), 38 | ["encapsed_string"] = actions.cycle_quotes(), 39 | ["binary_expression"] = actions.toggle_operator(operators), 40 | ["integer"] = actions.toggle_int_readability(), 41 | } 42 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/python.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | local actions = require("ts-node-action.actions") 3 | 4 | -- Special cases: 5 | -- Because "is" and "not" are valid by themselves, they are seen as separate 6 | -- nodes by TS. This means that without special handling, a config of: 7 | -- { 8 | -- ["is"] = " %s ", 9 | -- ["not"] = " %s " 10 | -- } 11 | -- would be padded as ` is not `. To avoid this, we can make them smarter by 12 | -- defining a padding rule for the case of when "not" is seen after "is". 13 | -- 14 | -- There is also the identical case of "not in". However, "-" is both a 15 | -- unary and binary operator. When it is used as a binary operator, it is 16 | -- a normal case. For unary, we don't want any padding, and generally (always?) 17 | -- it is preceded by a named node. When padding, we see these as 18 | -- prev_text=nil, so we can use that to detect the unary case with a special 19 | -- key "prev_nil", to represent it. 20 | local padding = { 21 | [","] = "%s ", 22 | [":"] = "%s ", 23 | ["{"] = "%s", 24 | ["}"] = "%s", 25 | ["for"] = " %s ", 26 | ["if"] = " %s ", 27 | ["else"] = " %s ", 28 | ["and"] = " %s ", 29 | ["or"] = " %s ", 30 | ["is"] = " %s ", 31 | ["not"] = { " %s ", ["is"] = "%s " }, 32 | ["in"] = { " %s ", ["not"] = "%s " }, 33 | ["=="] = " %s ", 34 | ["!="] = " %s ", 35 | [">="] = " %s ", 36 | ["<="] = " %s ", 37 | [">"] = " %s ", 38 | ["<"] = " %s ", 39 | ["+"] = " %s ", 40 | ["-"] = { " %s ", ["prev_nil"] = "%s" }, 41 | ["*"] = " %s ", 42 | ["/"] = " %s ", 43 | ["//"] = " %s ", 44 | ["%"] = " %s ", 45 | ["**"] = " %s ", 46 | ["lambda"] = " %s ", 47 | ["with"] = " %s ", 48 | ["as"] = " %s ", 49 | ["import"] = " %s ", 50 | ["from"] = "%s ", 51 | ["not in"] = " %s ", 52 | } 53 | 54 | --- @param node TSNode 55 | local function node_trim_whitespace(node) 56 | local start_row, _, end_row, _ = node:range() 57 | vim.cmd( 58 | "silent! keeppatterns " 59 | .. (start_row + 1) 60 | .. "," 61 | .. (end_row + 1) 62 | .. "s/\\s\\+$//g" 63 | ) 64 | end 65 | 66 | -- When inlined, these nodes must be parenthesized to avoid changing the 67 | -- meaning of the code and to avoid syntax errors. 68 | -- eg: x = lambda y: y + 1 if y else 0 69 | -- x = (lambda y: y + 1) if y else 0 70 | -- Both are valid, but the first is not equivalent to the second. 71 | -- Unlike the second, the first can not be expanded to: 72 | -- if y: 73 | -- x = lambda y: y + 1 74 | -- else: 75 | -- x = 0 76 | -- because "1 if y else 0" is inside the lambda. 77 | local node_types_to_parenthesize = { 78 | ["conditional_expression"] = true, 79 | ["boolean_operator"] = true, 80 | ["lambda"] = true, 81 | } 82 | 83 | local function parenthesize_if_needed(node, text) 84 | if node_types_to_parenthesize[node:type()] and text:sub(1, 1) ~= "(" then 85 | return "(" .. text .. ")" 86 | end 87 | 88 | return text 89 | end 90 | 91 | -- Recreating actions.toggle_multiline.collapse_child_nodes() here because 92 | -- it is not exported. It was not possible to use helpers.node_text() on a 93 | -- multiline node because it will include the "\n", which is invalid for the 94 | -- replacement text. 95 | -- 96 | --- @param padding_override table 97 | --- @return function 98 | local function collapse_child_nodes(padding_override) 99 | --- @param node TSNode 100 | --- @return string 101 | local function action(node) 102 | if not helpers.node_is_multiline(node) then 103 | return helpers.node_text(node) 104 | end 105 | 106 | local tbl = actions.toggle_multiline(padding_override) 107 | local replacement = tbl[1][1](node) 108 | 109 | return replacement 110 | end 111 | 112 | return action 113 | end 114 | 115 | -- Helper that returns the text of the left and right hand sides of a 116 | -- statement. For example, the left hand side of: 117 | -- 118 | -- - `return 1` is `return` and the right hand side is `1`. 119 | -- - `x = 1` is `x = ` and the right hand side is `1`. 120 | -- - `x = y = z = 1` is `x = y = z = ` and the right hand side is `1`. 121 | -- - `print(3)` is "" and the right hand side is `print(3)`. 122 | -- 123 | --- @param node TSNode 124 | --- @return string|nil, string|nil, string 125 | local function node_text_lhs_rhs(node, padding_override) 126 | local lhs = nil 127 | local rhs = nil 128 | local type = node:type() 129 | local child = node:named_child(0) 130 | local collapse = collapse_child_nodes(padding_override) 131 | 132 | if type == "return_statement" then 133 | lhs = "return " 134 | rhs = collapse(child) 135 | elseif type == "expression_statement" then 136 | type = child:type() 137 | lhs = "" 138 | 139 | if type == "assignment" then 140 | local identifiers = {} 141 | -- handle multiple assignments, eg: x = y = z = 1 142 | while child:type() == "assignment" do 143 | table.insert(identifiers, helpers.node_text(child:named_child(0))) 144 | child = child:named_child(1) 145 | end 146 | lhs = table.concat(identifiers, " = ") .. " = " 147 | rhs = collapse(child) 148 | elseif type == "call" then 149 | local identifier = helpers.node_text(child:named_child(0)) 150 | child = child:named_child(1) 151 | rhs = identifier .. collapse(child) 152 | elseif type == "boolean_operator" or type == "parenthesized_expression" then 153 | rhs = collapse(child) 154 | end 155 | end 156 | 157 | return lhs, rhs, type, child 158 | end 159 | 160 | -- The if/conditional_expression that we are expanding can find itself on 161 | -- the same row as an inlined for or if statement. 162 | -- For example: 163 | -- 164 | -- `for x in range(10): x = 1 if x > 5 else x + 1` 165 | -- `if x > 0: x = 1 if x > 5 else x + 1` 166 | -- 167 | -- Contrived, hopefully, but this handles it, by detecting if there is a 168 | -- for/if statement on the same row as our current parent. 169 | -- 170 | --- @param parent TSNode 171 | --- @param parent_type string 172 | --- @param start_row number 173 | --- @return TSNode, string 174 | --- @return nil 175 | local function find_row_parent(parent, parent_type, start_row) 176 | while 177 | parent ~= nil 178 | and parent_type ~= "if_statement" 179 | and parent_type ~= "for_statement" 180 | do 181 | parent = parent:parent() 182 | if parent == nil then 183 | return nil 184 | end 185 | parent_type = parent:type() 186 | if select(1, parent:start()) ~= start_row then 187 | return nil 188 | end 189 | end 190 | 191 | if parent_type == "if_statement" or parent_type == "for_statement" then 192 | return parent, parent_type 193 | end 194 | 195 | return nil 196 | end 197 | 198 | -- We detect if it's safe to expand an inline if/else surrounded by parens 199 | -- and remove them by skipping to it's parent, because the parent is 200 | -- replaced by this action, with the expanded if/else. 201 | -- 202 | -- Cases considered safe: 203 | -- `x = (conditional_expression)` 204 | -- `return (conditional_expression)` 205 | -- 206 | --- @param parent TSNode 207 | --- @param parent_type string 208 | --- @return TSNode, string 209 | local function skip_parens_by_reparenting(parent, parent_type) 210 | if parent_type == "parenthesized_expression" then 211 | local paren_parent = parent:parent() 212 | local paren_parent_type = paren_parent:type() 213 | if 214 | paren_parent_type == "assignment" 215 | or paren_parent_type == "return_statement" 216 | then 217 | parent = paren_parent 218 | parent_type = paren_parent_type 219 | end 220 | end 221 | return parent, parent_type 222 | end 223 | 224 | --- @param node TSNode 225 | --- @param comments table 226 | --- @return nil (mutates comments) 227 | local function deep_collect_comments(node, comments) 228 | for child in node:iter_children() do 229 | if child:named() then 230 | if child:type() == "comment" then 231 | table.insert(comments, child) 232 | else 233 | deep_collect_comments(child, comments) 234 | end 235 | end 236 | end 237 | end 238 | 239 | --- @param parent TSNode 240 | --- @param children table 241 | --- @param comments table 242 | --- @return nil (mutates children and comments) 243 | local function collect_named_children(parent, children, comments) 244 | for child in parent:iter_children() do 245 | if child:named() then 246 | if child:type() == "comment" then 247 | table.insert(comments, child) 248 | else 249 | table.insert(children, child) 250 | deep_collect_comments(child, comments) 251 | end 252 | end 253 | end 254 | end 255 | 256 | --- @param if_statement TSNode 257 | --- @return table 258 | local function destructure_if_statement(if_statement) 259 | local condition 260 | local consequence = {} 261 | local alternative = {} 262 | local comments = {} 263 | 264 | for child in if_statement:iter_children() do 265 | if child:named() then 266 | local child_type = child:type() 267 | 268 | if child_type == "comment" then 269 | table.insert(comments, child) 270 | elseif child_type == "block" then 271 | collect_named_children(child, consequence, comments) 272 | elseif child_type == "else_clause" then 273 | local block = {} 274 | collect_named_children(child, block, comments) 275 | collect_named_children(block[1], alternative, comments) 276 | else 277 | condition = child 278 | end 279 | end 280 | end 281 | 282 | return { 283 | node = if_statement, 284 | condition = condition, 285 | consequence = consequence, 286 | alternative = alternative, 287 | comments = comments, 288 | } 289 | end 290 | 291 | --- @param node TSNode 292 | --- @return table 293 | local function destructure_conditional_expression(node) 294 | local comments = {} 295 | local children = {} 296 | 297 | collect_named_children(node, children, comments) 298 | 299 | return { 300 | node = node, 301 | condition = children[2], 302 | consequence = { children[1] }, -- as a table for consistency 303 | alternative = { children[3] }, -- which allows for sharing 304 | comments = comments, 305 | } 306 | end 307 | 308 | --- @param stmt table 309 | --- @return string, table, TSNode 310 | --- @return nil 311 | local function expand_cond_expr(stmt, padding_override) 312 | local parent = stmt.node:parent() 313 | local parent_type = parent:type() 314 | 315 | parent, parent_type = skip_parens_by_reparenting(parent, parent_type) 316 | 317 | local lhs 318 | if parent_type == "return_statement" then 319 | lhs = "return " 320 | elseif parent_type == "assignment" then 321 | local identifiers = {} 322 | -- handle multiple assignments, eg: x = y = z = 1 323 | while parent:type() == "assignment" do 324 | table.insert(identifiers, 1, helpers.node_text(parent:named_child(0))) 325 | parent = parent:parent() 326 | end 327 | lhs = table.concat(identifiers, " = ") .. " = " 328 | elseif parent_type == "expression_statement" then 329 | lhs = "" 330 | elseif parent_type == "block" or parent_type == "module" then 331 | lhs = "" 332 | parent = stmt.node 333 | else 334 | -- parent context is not yet supported, eg: y = 3 or (4 if x > 0 else 5) 335 | return 336 | end 337 | 338 | local start_row, start_col = parent:start() 339 | local row_parent = find_row_parent(parent, parent_type, start_row) 340 | local cursor = {} 341 | -- when we are embedded on the end of an inlined if/for statement, we need 342 | -- to expand on to the next line and shift the cursor/indent 343 | local if_indent = "" 344 | local else_indent = "" 345 | if row_parent then 346 | local _, row_start_col = row_parent:start() 347 | -- cursor position is relative to the node being replaced (parent) 348 | cursor = { row = 1, col = row_start_col - start_col + 4 } 349 | if_indent = string.rep(" ", row_start_col + 4) 350 | else_indent = if_indent 351 | else 352 | else_indent = string.rep(" ", start_col) 353 | end 354 | local body_indent = else_indent .. string.rep(" ", 4) 355 | 356 | local collapse = collapse_child_nodes(padding_override) 357 | local replacement = { 358 | if_indent .. "if " .. collapse(stmt.condition) .. ":", 359 | body_indent .. lhs .. collapse(stmt.consequence[1]), 360 | } 361 | 362 | if #stmt.alternative > 0 then 363 | table.insert(replacement, else_indent .. "else:") 364 | table.insert( 365 | replacement, 366 | body_indent .. lhs .. collapse(stmt.alternative[1]) 367 | ) 368 | end 369 | 370 | local callback = nil 371 | if row_parent then 372 | table.insert(replacement, 1, "") 373 | callback = function() 374 | node_trim_whitespace(parent) 375 | end 376 | end 377 | 378 | return replacement, 379 | { 380 | cursor = cursor, 381 | callback = callback, 382 | format = true, 383 | target = parent, 384 | } 385 | end 386 | 387 | --- @param stmt table { node, condition, consequence, alternative, comments } 388 | --- @param padding_override table 389 | --- @return string, table, TSNode 390 | --- @return nil 391 | local function inline_if(stmt, padding_override) 392 | local lhs, rhs, _, child = 393 | node_text_lhs_rhs(stmt.consequence[1], padding_override) 394 | if lhs == nil then 395 | return 396 | end 397 | rhs = parenthesize_if_needed(child, rhs) 398 | 399 | local cond_text = collapse_child_nodes(padding_override)(stmt.condition) 400 | 401 | local replacement = { "if " .. cond_text .. ": " .. lhs .. rhs } 402 | return replacement, { cursor = {} } 403 | end 404 | 405 | --- @param cons_type string 406 | --- @param alt_type string 407 | --- @param cons_lhs string 408 | --- @param alt_lhs string 409 | --- @return boolean 410 | local function body_types_are_inlineable(cons_type, alt_type, cons_lhs, alt_lhs) 411 | -- strict match 412 | if cons_type == "assignment" or alt_type == "assignment" then 413 | return cons_type == alt_type and cons_lhs == alt_lhs 414 | elseif cons_type == "return_statement" or alt_type == "return_statement" then 415 | return cons_type == alt_type 416 | end 417 | -- these do not depend on a common lhs and can freely appear on either side 418 | local mixable_match_body_types = { 419 | ["call"] = true, 420 | ["boolean_operator"] = true, 421 | ["parenthesized_expression"] = true, 422 | } 423 | return mixable_match_body_types[cons_type] 424 | and mixable_match_body_types[alt_type] 425 | end 426 | 427 | --- @param stmt table { node, condition, consequence, alternative, comments } 428 | --- @param padding_override table 429 | --- @return string, table, TSNode 430 | --- @return nil 431 | local function inline_ifelse(stmt, padding_override) 432 | local cons_lhs, cons_rhs, cons_type, cons_child = 433 | node_text_lhs_rhs(stmt.consequence[1], padding_override) 434 | if cons_lhs == nil then 435 | return 436 | end 437 | cons_rhs = parenthesize_if_needed(cons_child, cons_rhs) 438 | 439 | local alt_lhs, alt_rhs, alt_type, alt_child = 440 | node_text_lhs_rhs(stmt.alternative[1], padding_override) 441 | if 442 | alt_rhs == nil 443 | or not body_types_are_inlineable(cons_type, alt_type, cons_lhs, alt_lhs) 444 | then 445 | return 446 | end 447 | 448 | alt_rhs = parenthesize_if_needed(alt_child, alt_rhs) 449 | 450 | local cond_text = collapse_child_nodes(padding_override)(stmt.condition) 451 | 452 | local replacement = cons_lhs 453 | .. cons_rhs 454 | .. " if " 455 | .. cond_text 456 | .. " else " 457 | .. alt_rhs 458 | 459 | return replacement, 460 | { 461 | cursor = { col = string.len(cons_lhs .. cons_rhs) + 1 }, 462 | } 463 | end 464 | 465 | --- @param padding_override table 466 | --- @return table 467 | local function inline_if_statement(padding_override) 468 | padding_override = padding_override or padding 469 | 470 | --- @param if_statement TSNode 471 | --- @return string, table, TSNode 472 | local function action(if_statement) 473 | local stmt = destructure_if_statement(if_statement) 474 | -- we can't inline multiple statements within a block 475 | if #stmt.consequence > 1 or #stmt.alternative > 1 then 476 | return 477 | end 478 | 479 | if #stmt.comments > 0 then 480 | return 481 | end 482 | 483 | if helpers.node_is_multiline(if_statement) then 484 | local fn 485 | if #stmt.alternative ~= 0 then 486 | fn = inline_ifelse 487 | else 488 | fn = inline_if 489 | end 490 | return fn(stmt, padding_override) 491 | else 492 | -- an if_statement of the form `if True: print(1)` 493 | -- and this knows how to expand it 494 | return expand_cond_expr(stmt, padding_override) 495 | end 496 | end 497 | 498 | return { action, name = "Inline Conditional" } 499 | end 500 | 501 | --- @param padding_override table 502 | --- @return table|nil 503 | local function expand_conditional_expression(padding_override) 504 | padding_override = padding_override or padding 505 | 506 | --- @param conditional_expression TSNode 507 | --- @return string, table, TSNode 508 | local function action(conditional_expression) 509 | local stmt = destructure_conditional_expression(conditional_expression) 510 | if #stmt.comments > 0 then 511 | return 512 | end 513 | 514 | return expand_cond_expr(stmt, padding_override) 515 | end 516 | 517 | return { action, name = "Expand Conditional" } 518 | end 519 | 520 | local function cycle_quotes() 521 | quotes = { ["'"] = '"', ['"'] = "'" } 522 | 523 | local function _replace(node) 524 | local text = helpers.node_text(node) 525 | assert(type(text) == "string") 526 | local quote = text:match("[" .. [['"]] .. "]") 527 | local replacement = quotes[quote] 528 | return text:gsub(quote, replacement) 529 | end 530 | 531 | local function toggle_start(node) 532 | while node:type() ~= "string_start" do 533 | node = node:prev_sibling() 534 | end 535 | return _replace(node), { target = node } 536 | end 537 | 538 | local function toggle_end(node) 539 | while node:type() ~= "string_end" do 540 | node = node:next_sibling() 541 | end 542 | return _replace(node), { target = node } 543 | end 544 | 545 | return { 546 | { toggle_start, name = "Toggle string start quote" }, 547 | { toggle_end, name = "Toggle string end quote" }, 548 | ask = false, 549 | } 550 | end 551 | 552 | return { 553 | ["dictionary"] = actions.toggle_multiline(padding), 554 | ["set"] = actions.toggle_multiline(padding), 555 | ["list"] = actions.toggle_multiline(padding), 556 | ["tuple"] = actions.toggle_multiline(padding), 557 | ["argument_list"] = actions.toggle_multiline(padding), 558 | ["parameters"] = actions.toggle_multiline(padding), 559 | ["list_comprehension"] = actions.toggle_multiline(padding), 560 | ["set_comprehension"] = actions.toggle_multiline(padding), 561 | ["dictionary_comprehension"] = actions.toggle_multiline(padding), 562 | ["generator_expression"] = actions.toggle_multiline(padding), 563 | ["comparison_operator"] = actions.toggle_operator(), 564 | ["integer"] = actions.toggle_int_readability(), 565 | ["conditional_expression"] = { expand_conditional_expression(padding) }, 566 | ["if_statement"] = { inline_if_statement(padding) }, 567 | ["string_start"] = cycle_quotes(), 568 | ["string_content"] = cycle_quotes(), 569 | ["string_end"] = cycle_quotes(), 570 | } 571 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/r.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | local helpers = require("ts-node-action.helpers") 3 | 4 | local operators = { 5 | ["!="] = "==", 6 | ["=="] = "!=", 7 | [">"] = "<", 8 | ["<"] = ">", 9 | [">="] = "<=", 10 | ["<="] = ">=", 11 | ["+"] = "-", 12 | ["-"] = "+", 13 | ["*"] = "/", 14 | ["/"] = "*", 15 | ["|"] = "&", 16 | ["&"] = "|", 17 | ["||"] = "&&", 18 | ["&&"] = "||", 19 | } 20 | 21 | local padding = { 22 | [","] = "%s ", 23 | ["="] = " %s ", 24 | } 25 | 26 | --- @param node TSNode 27 | local function toggle_multiline_args(node) 28 | local structure = helpers.destructure_node(node) 29 | if 30 | (type(structure["arguments"]) == "table") 31 | or (type(structure["arguments"]) == "string") 32 | then 33 | else 34 | vim.print("No arguments") 35 | return 36 | end 37 | 38 | local range_end = {} 39 | range_end = { node:named_child(0):range() } 40 | local replacement 41 | 42 | if helpers.node_is_multiline(node) then 43 | local tbl = actions.toggle_multiline(padding) 44 | replacement = tbl[1][1](node) 45 | else 46 | replacement = { structure["function"] .. "(" } 47 | for k in string.gmatch(structure.arguments, "([^,]+)") do 48 | table.insert(replacement, k .. ",") 49 | end 50 | replacement[#replacement] = 51 | string.gsub(replacement[#replacement], "(.*)%,$", "%1") 52 | table.insert(replacement, ")") 53 | end 54 | 55 | return replacement, 56 | { cursor = { col = range_end[4] - range_end[2] }, format = true } 57 | end 58 | return { 59 | ["binary"] = actions.toggle_operator(operators), 60 | ["call"] = { { toggle_multiline_args, name = "Toggle Multiline Arguments" } }, 61 | ["formal_parameters"] = actions.toggle_multiline(padding), 62 | } 63 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/ruby.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("ts-node-action.helpers") 2 | local actions = require("ts-node-action.actions") 3 | 4 | local padding = { 5 | [","] = "%s ", 6 | [":"] = { "%s ", ["next_nil"] = "%s" }, 7 | ["{"] = "%s ", 8 | ["=>"] = " %s ", 9 | ["="] = " %s ", 10 | ["}"] = " %s", 11 | ["+"] = " %s ", 12 | ["-"] = " %s ", 13 | ["*"] = " %s ", 14 | ["/"] = " %s ", 15 | } 16 | 17 | local identifier_formats = 18 | { "snake_case", "pascal_case", "screaming_snake_case" } 19 | 20 | local uncollapsible = { 21 | ["conditional"] = true, 22 | } 23 | 24 | local function toggle_block(node) 25 | local structure = helpers.destructure_node(node) 26 | if type(structure.body) == "table" then 27 | return 28 | end 29 | 30 | local replacement 31 | 32 | if helpers.node_is_multiline(node) then 33 | if structure.parameters then 34 | replacement = "{ " 35 | .. structure.parameters 36 | .. " " 37 | .. structure.body 38 | .. " }" 39 | else 40 | replacement = "{ " .. structure.body .. " }" 41 | end 42 | else 43 | if structure.parameters then 44 | replacement = { "do " .. structure.parameters, structure.body, "end" } 45 | else 46 | replacement = { "do", structure.body, "end" } 47 | end 48 | end 49 | 50 | return replacement, { cursor = {}, format = true } 51 | end 52 | 53 | local function inline_conditional(structure) 54 | if type(structure.consequence) == "table" then 55 | return 56 | end 57 | 58 | local replacement = { 59 | structure.consequence, 60 | structure["if"] or structure["unless"], 61 | structure.condition, 62 | } 63 | 64 | return table.concat(replacement, " "), 65 | { cursor = { col = #structure.consequence + 1 } } 66 | end 67 | 68 | local function collapse_ternary(structure) 69 | local replacement = { 70 | structure.condition, 71 | " ? ", 72 | structure.consequence, 73 | " : ", 74 | structure.alternative[2], 75 | } 76 | 77 | return table.concat(replacement), { cursor = { col = #replacement[1] + 1 } } 78 | end 79 | 80 | local function handle_conditional(node) 81 | local structure = helpers.destructure_node(node) 82 | local fn 83 | if structure.alternative then 84 | fn = collapse_ternary 85 | else 86 | fn = inline_conditional 87 | end 88 | 89 | return fn(structure) 90 | end 91 | 92 | local function expand_ternary(node) 93 | local structure = helpers.destructure_node(node) 94 | local replacement = { 95 | "if " .. structure.condition, 96 | structure.consequence, 97 | "else", 98 | structure.alternative, 99 | "end", 100 | } 101 | 102 | return replacement, { cursor = {}, format = true } 103 | end 104 | 105 | local function multiline_conditional(node) 106 | local structure = helpers.destructure_node(node) 107 | local replacement = { 108 | (structure["if"] or structure["unless"]) .. " " .. structure.condition, 109 | structure.body, 110 | "end", 111 | } 112 | 113 | return replacement, { cursor = {}, format = true } 114 | end 115 | 116 | local function toggle_hash_style(node) 117 | local styles = { ["=>"] = ": ", [":"] = " => " } 118 | local structure = helpers.destructure_node(node) 119 | 120 | -- Not handling non string/symbol keys 121 | if 122 | not structure.key:sub(1):match([[^"']]) 123 | and not structure.key:sub(1):match("%a") 124 | then 125 | return 126 | end 127 | 128 | -- Fixes for symbol/string/int keys keys 129 | if structure[":"] and structure.key:sub(1):match("^%a") then 130 | structure.key = ":" .. structure.key 131 | elseif structure.key:sub(1, 1) == ":" then 132 | structure.key = structure.key:sub(2) 133 | end 134 | 135 | local replacement = structure.key 136 | .. styles[structure[":"] or structure["=>"]] 137 | .. structure.value 138 | local opts = { 139 | cursor = { col = structure[":"] and #structure.key + 1 or #structure.key }, 140 | } 141 | 142 | return replacement, opts 143 | end 144 | 145 | return { 146 | ["identifier"] = actions.cycle_case(identifier_formats), 147 | ["constant"] = actions.cycle_case(identifier_formats), 148 | ["binary"] = actions.toggle_operator(), 149 | ["array"] = actions.toggle_multiline(padding, uncollapsible), 150 | ["hash"] = actions.toggle_multiline(padding, uncollapsible), 151 | ["argument_list"] = actions.toggle_multiline(padding, uncollapsible), 152 | ["method_parameters"] = actions.toggle_multiline(padding, uncollapsible), 153 | ["integer"] = actions.toggle_int_readability(), 154 | ["block"] = { { toggle_block, name = "Toggle Block" } }, 155 | ["do_block"] = { { toggle_block, name = "Toggle Block" } }, 156 | ["if"] = { { handle_conditional, name = "Handle Conditional" } }, 157 | ["unless"] = { { handle_conditional, name = "Handle Conditional" } }, 158 | ["if_modifier"] = { 159 | { multiline_conditional, name = "Multiline Conditional" }, 160 | }, 161 | ["unless_modifier"] = { 162 | { multiline_conditional, name = "Multiline Conditional" }, 163 | }, 164 | ["conditional"] = { { expand_ternary, name = "Expand Ternary" } }, 165 | ["pair"] = { { toggle_hash_style, name = "Toggle Hash Style" } }, 166 | } 167 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/rust.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | local padding = { 4 | [","] = "%s ", 5 | [":"] = "%s ", 6 | ["{"] = "%s ", 7 | ["=>"] = " %s ", 8 | ["="] = " %s ", 9 | ["}"] = " %s", 10 | ["+"] = " %s ", 11 | ["-"] = " %s ", 12 | ["*"] = " %s ", 13 | ["/"] = " %s ", 14 | } 15 | 16 | return { 17 | ["field_declaration_list"] = actions.toggle_multiline(padding), 18 | ["parameters"] = actions.toggle_multiline(padding), 19 | ["enum_variant_list"] = actions.toggle_multiline(padding), 20 | ["block"] = actions.toggle_multiline(padding), 21 | ["array_expression"] = actions.toggle_multiline(padding), 22 | ["tuple_expression"] = actions.toggle_multiline(padding), 23 | ["tuple_pattern"] = actions.toggle_multiline(padding), 24 | ["boolean_literal"] = actions.toggle_boolean(), 25 | ["integer_literal"] = actions.toggle_int_readability(), 26 | } 27 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/sql.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | local operators = { 4 | ["!="] = "=", 5 | ["="] = "!=", 6 | ["AND"] = "OR", 7 | ["OR"] = "AND", 8 | ["or"] = "and", 9 | ["and"] = "or", 10 | [">"] = "<", 11 | ["<"] = ">", 12 | [">="] = "<=", 13 | ["<="] = ">=", 14 | ["+"] = "-", 15 | ["-"] = "+", 16 | ["*"] = "/", 17 | ["/"] = "*", 18 | } 19 | 20 | local padding = { 21 | [","] = "%s ", 22 | } 23 | 24 | local uncollapsible = { 25 | ["term"] = true, 26 | ["column_definition"] = true, 27 | } 28 | 29 | return { 30 | ["keyword_true"] = actions.toggle_boolean(), 31 | ["keyword_false"] = actions.toggle_boolean(), 32 | ["binary_expression"] = actions.toggle_operator(operators), 33 | ["keyword_and"] = actions.toggle_operator(operators), 34 | ["keyword_or"] = actions.toggle_operator(operators), 35 | ["select_expression"] = actions.toggle_multiline(padding, uncollapsible), 36 | ["column_definitions"] = actions.toggle_multiline(padding, uncollapsible), 37 | } 38 | -------------------------------------------------------------------------------- /lua/ts-node-action/filetypes/yaml.lua: -------------------------------------------------------------------------------- 1 | local actions = require("ts-node-action.actions") 2 | 3 | return { 4 | ["boolean_scalar"] = actions.toggle_boolean(), 5 | } 6 | -------------------------------------------------------------------------------- /lua/ts-node-action/helpers.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Returns node text as a string if single-line, or table if multi-line 4 | -- 5 | --- @param node? TSNode 6 | --- @return table|string|nil 7 | function M.node_text(node) 8 | if not node then 9 | return 10 | end 11 | 12 | local text 13 | if vim.treesitter.get_node_text then 14 | text = vim.trim( 15 | vim.treesitter.get_node_text(node, vim.api.nvim_get_current_buf()) 16 | ) 17 | else 18 | -- TODO: Remove in 0.10 19 | text = vim.trim( 20 | vim.treesitter.query.get_node_text(node, vim.api.nvim_get_current_buf()) 21 | ) 22 | end 23 | 24 | if text:match("\n") then 25 | return vim.tbl_map(vim.trim, vim.split(text, "\n")) 26 | else 27 | return text 28 | end 29 | end 30 | 31 | -- Determine if a node spans multiple lines 32 | -- 33 | --- @param node TSNode 34 | --- @return boolean 35 | function M.node_is_multiline(node) 36 | local start_row, _, end_row, _ = node:range() 37 | return start_row ~= end_row 38 | end 39 | 40 | -- Adds whitespace to some unnamed nodes for nicer formatting 41 | -- `padding` is a table where the key is the text of the unnamed node, and the 42 | -- value is a format string. The following would add a space after commas: 43 | -- { [","] = "%s " } 44 | -- 45 | -- The prev_text is used for rare cases where the padding of an unnamed node 46 | -- is different depending on the text of the previous node. For example, in 47 | -- python, `is` and `not` are separate unnamed nodes, even when seen 48 | -- together as `is not`. So we can write a padding rule that includes the 49 | -- previous node's text as: 50 | -- { 51 | -- ["is"] = " %s ", 52 | -- ["not"] = { 53 | -- " %s ", 54 | -- ["is"] = "%s ", 55 | -- }, 56 | -- } 57 | -- The ["is"] key under "not" overrides the format to remove the space when the 58 | -- previous text is "is". 59 | -- A ["prev_nil"] key will match when there is no previous node text 60 | -- A ["next_nil"] key will match when there is no next node text 61 | -- If none of the context_prev's apply, the string in index 1 will be used 62 | -- See filetypes/python.lua or filetypes/ruby.lua for more examples 63 | -- 64 | --- @param node TSNode 65 | --- @param padding table 66 | --- @return string|table|nil 67 | function M.padded_node_text(node, padding) 68 | local text = M.node_text(node) 69 | local format = padding[text] 70 | 71 | if not format then 72 | return text 73 | end 74 | 75 | if type(format) == "table" then 76 | local context_prev = M.node_text(node:prev_sibling()) 77 | local context_next = M.node_text(node:next_sibling()) 78 | 79 | if format[context_prev] then 80 | format = format[context_prev] 81 | elseif not context_prev and format["prev_nil"] then 82 | format = format["prev_nil"] 83 | elseif format[context_next] then 84 | format = format[context_next] 85 | elseif not context_next and format["next_nil"] then 86 | format = format["next_nil"] 87 | else 88 | format = format[1] 89 | end 90 | end 91 | 92 | return string.format(format, text) 93 | end 94 | 95 | -- Prints out a node's tree, showing each child's index, type, text, and ID 96 | -- 97 | --- @param node TSNode 98 | --- @return nil 99 | function M.debug_print_tree(node) 100 | local tree = {} 101 | local index = 1 102 | for child, id in node:iter_children() do 103 | tree[tostring(index)] = 104 | { type = child:type(), text = M.node_text(child), id = id } 105 | index = index + 1 106 | end 107 | 108 | vim.print(tree) 109 | end 110 | 111 | -- Disassembles a node tree into it's named and unnamed parts 112 | -- 113 | --- @param node TSNode 114 | --- @return table 115 | function M.destructure_node(node) 116 | local structure = {} 117 | for child, id in node:iter_children() do 118 | structure[id or child:type()] = M.node_text(child) 119 | end 120 | 121 | return structure 122 | end 123 | 124 | return M 125 | -------------------------------------------------------------------------------- /lua/ts-node-action/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local ts = vim.treesitter 4 | 5 | --- @private 6 | --- @param targets TSNode[] 7 | --- @return integer start_row 8 | --- @return integer start_col 9 | --- @return integer end_row 10 | --- @return integer end_col 11 | local function combined_range(targets) 12 | local start_row, start_col, end_row, end_col 13 | for _, target in ipairs(targets) do 14 | local sr, sc, er, ec = target:range() 15 | if start_row == nil or sr < start_row then 16 | start_row = sr 17 | end 18 | if start_col == nil or sc < start_col then 19 | start_col = sc 20 | end 21 | if end_row == nil or er > end_row then 22 | end_row = er 23 | end 24 | if end_col == nil or ec > end_col then 25 | end_col = ec 26 | end 27 | end 28 | return start_row, start_col, end_row, end_col 29 | end 30 | 31 | --- @private 32 | --- @param replacement string|table 33 | --- @param opts { cursor: { col: number, row: number }, callback: function, format: boolean, target: TSNode | TSNode[] } 34 | --- All opts fields are optional 35 | local function replace_node(node, replacement, opts) 36 | if type(replacement) ~= "table" then 37 | replacement = { replacement } 38 | end 39 | 40 | local start_row, start_col, end_row, end_col 41 | if vim.islist(opts.target) then 42 | start_row, start_col, end_row, end_col = combined_range(opts.target) 43 | else 44 | start_row, start_col, end_row, end_col = (opts.target or node):range() 45 | end 46 | vim.api.nvim_buf_set_text( 47 | vim.api.nvim_get_current_buf(), 48 | start_row, 49 | start_col, 50 | end_row, 51 | end_col, 52 | replacement 53 | ) 54 | 55 | if opts.cursor then 56 | vim.api.nvim_win_set_cursor(vim.api.nvim_get_current_win(), { 57 | start_row + (opts.cursor.row or 0) + 1, 58 | start_col + (opts.cursor.col or 0), 59 | }) 60 | end 61 | 62 | if opts.format then 63 | vim.cmd("silent! normal! " .. #replacement .. "==") 64 | end 65 | 66 | if opts.callback then 67 | opts.callback() 68 | end 69 | end 70 | 71 | --- @private 72 | --- @param message string 73 | --- @return nil 74 | local function info(message) 75 | vim.notify( 76 | message, 77 | vim.log.levels.INFO, 78 | { title = "Node Action", icon = " " } 79 | ) 80 | end 81 | 82 | --- @private 83 | --- @param action function 84 | --- @param node TSNode 85 | --- @return nil 86 | local function do_action(action, node) 87 | local replacement, opts = action(node) 88 | if replacement then 89 | replace_node(node, replacement, opts or {}) 90 | end 91 | end 92 | 93 | --- @param node TSNode 94 | --- @param lang string 95 | --- @return function|nil 96 | local function find_action(node, lang) 97 | local type = node:type() 98 | if M.node_actions[lang] and M.node_actions[lang][type] then 99 | return M.node_actions[lang][type] 100 | else 101 | return M.node_actions["*"][type] 102 | end 103 | end 104 | 105 | M.node_actions = require("ts-node-action.filetypes") 106 | 107 | --- @param opts? table 108 | --- @return nil 109 | function M.setup(opts) 110 | M.node_actions = vim.tbl_deep_extend("force", M.node_actions, opts or {}) 111 | 112 | vim.api.nvim_create_user_command( 113 | "NodeAction", 114 | M.node_action, 115 | { desc = "Performs action on the node under the cursor." } 116 | ) 117 | 118 | vim.api.nvim_create_user_command( 119 | "NodeActionDebug", 120 | M.debug, 121 | { desc = "Prints debug information for Ts-Node-Action Plugin" } 122 | ) 123 | end 124 | 125 | --- @private 126 | --- @return TSNode|nil, string|nil 127 | function M._get_node() 128 | -- stylua: ignore 129 | local parser = (vim.fn.has("nvim-0.12") == 1 and ts.get_parser()) 130 | or (vim.fn.has("nvim-0.11") == 1 and ts.get_parser(nil, nil, { error = false })) 131 | or (type(ts.get_parser) == "function" and ts.get_parser(nil, nil)) 132 | 133 | if not parser then 134 | return 135 | end 136 | 137 | local lnum, col = unpack(vim.api.nvim_win_get_cursor(0)) 138 | local range4 = { lnum - 1, col, lnum - 1, col } 139 | local langtree = parser:language_for_range(range4) 140 | local node = langtree:named_node_for_range(range4) 141 | return node, langtree:lang() 142 | end 143 | 144 | M.node_action = require("ts-node-action.repeat").set(function() 145 | local node, lang = M._get_node() 146 | if not node then 147 | info("No node found at cursor") 148 | return 149 | end 150 | 151 | local action = find_action(node, lang) 152 | if type(action) == "function" then 153 | do_action(action, node) 154 | elseif type(action) == "table" then 155 | if action.ask == false or #action == 1 then 156 | for _, act in ipairs(action) do 157 | do_action(act[1], node) 158 | end 159 | else 160 | vim.ui.select(action, { 161 | prompt = "Select Action", 162 | format_item = function(choice) 163 | return choice.name 164 | end, 165 | }, function(choice) 166 | do_action(choice[1], node) 167 | end) 168 | end 169 | else 170 | info( 171 | "No action defined for '" 172 | .. lang 173 | .. "' node type: '" 174 | .. node:type() 175 | .. "'" 176 | ) 177 | end 178 | end) 179 | 180 | function M.available_actions() 181 | local node, lang = M._get_node() 182 | if not node then 183 | info("No node found at cursor") 184 | return 185 | end 186 | 187 | local function format_action(tbl) 188 | return { 189 | action = function() 190 | do_action(tbl[1], node) 191 | end, 192 | title = tbl.name or "Anonymous Node Action", 193 | } 194 | end 195 | 196 | local action = find_action(node, lang) 197 | if type(action) == "function" then 198 | return { format_action({ action }) } 199 | elseif type(action) == "table" then 200 | return vim.tbl_map(format_action, action) 201 | end 202 | end 203 | 204 | function M.debug() 205 | local node, lang = M._get_node() 206 | if not node then 207 | info("No node found at cursor") 208 | return 209 | end 210 | 211 | print(vim.inspect({ 212 | node = { 213 | lang = lang, 214 | filetype = vim.o.filetype, 215 | node_type = node:type(), 216 | named = node:named(), 217 | named_children = node:named_child_count(), 218 | }, 219 | plugin = { 220 | node_actions = M.node_actions, 221 | }, 222 | })) 223 | end 224 | 225 | return M 226 | -------------------------------------------------------------------------------- /lua/ts-node-action/repeat.lua: -------------------------------------------------------------------------------- 1 | -- https://github.com/lewis6991/gitsigns.nvim/blob/main/lua/gitsigns/repeat.lua 2 | local M = {} 3 | 4 | function M.set(fn) 5 | return function(...) 6 | local args = { ... } 7 | local nargs = select("#", ...) 8 | vim.go.operatorfunc = "v:lua.require'ts-node-action.repeat'.repeat_action" 9 | 10 | M.repeat_action = function() 11 | fn(unpack(args, 1, nargs)) 12 | 13 | local action = vim.api.nvim_replace_termcodes( 14 | string.format("call %s()", vim.go.operatorfunc), 15 | true, 16 | true, 17 | true 18 | ) 19 | 20 | pcall(vim.fn["repeat#set"], action, -1) 21 | end 22 | 23 | vim.cmd("normal! g@l") 24 | end 25 | end 26 | 27 | return M 28 | -------------------------------------------------------------------------------- /run_spec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | nvim -n --clean --noplugin -u NORC --headless -S "./spec/init.lua" 4 | -------------------------------------------------------------------------------- /spec/filetypes/c_sharp.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("c_sharp", { shiftwidth = 4 }) 4 | 5 | describe("boolean", function() 6 | it("turns 'true' into 'false'", function() 7 | assert.are.same( 8 | { "bool bool = true;" }, 9 | Helper:call({ "bool bool = false;" }, { 1, 13 }) 10 | ) 11 | end) 12 | 13 | it("turns 'false' into 'true'", function() 14 | assert.are.same( 15 | { "bool bool = false;" }, 16 | Helper:call({ "bool bool = true;" }, { 1, 13 }) 17 | ) 18 | end) 19 | end) 20 | 21 | describe("integer", function() 22 | it("adds underscores to long int", function() 23 | assert.are.same({ "1_000_000" }, Helper:call("1000000")) 24 | end) 25 | 26 | it("removes underscores from long int", function() 27 | assert.are.same({ "1000000" }, Helper:call("1_000_000")) 28 | end) 29 | 30 | it("doesn't change ints less than four places", function() 31 | assert.are.same({ "100" }, Helper:call("100")) 32 | end) 33 | end) 34 | 35 | describe("operator", function() 36 | it("toggles '<= into '>='", function() 37 | assert.are.same({ "i <= 8" }, Helper:call({ "i >= 8" }, { 1, 3 })) 38 | end) 39 | 40 | it("toggles '>=' into '<='", function() 41 | assert.are.same({ "i >= 8" }, Helper:call({ "i <= 8" }, { 1, 3 })) 42 | end) 43 | 44 | it("toggles '>' into '<'", function() 45 | assert.are.same({ "i > 8" }, Helper:call({ "i < 8" }, { 1, 3 })) 46 | end) 47 | 48 | it("toggles '<' into '>'", function() 49 | assert.are.same({ "i < 8" }, Helper:call({ "i > 8" }, { 1, 3 })) 50 | end) 51 | 52 | it("toggles '+' into '-'", function() 53 | assert.are.same({ "i + 8" }, Helper:call({ "i - 8" }, { 1, 3 })) 54 | end) 55 | 56 | it("toggles '-' into '+'", function() 57 | assert.are.same({ "i - 8" }, Helper:call({ "i + 8" }, { 1, 3 })) 58 | end) 59 | 60 | it("toggles '*' into '/'", function() 61 | assert.are.same({ "i * 8" }, Helper:call({ "i / 8" }, { 1, 3 })) 62 | end) 63 | 64 | it("toggles '/' into '*'", function() 65 | assert.are.same({ "i / 8" }, Helper:call({ "i * 8" }, { 1, 3 })) 66 | end) 67 | 68 | it("toggles '+=' into '-='", function() 69 | assert.are.same({ "i += 8" }, Helper:call({ "i -= 8" }, { 1, 3 })) 70 | end) 71 | 72 | it("toggles '-=' into '+='", function() 73 | assert.are.same({ "i -= 8" }, Helper:call({ "i += 8" }, { 1, 3 })) 74 | end) 75 | 76 | it("toggles '++' into '--'", function() 77 | assert.are.same({ "i++" }, Helper:call({ "i--" }, { 1, 2 })) 78 | end) 79 | 80 | it("toggles '--' into '++'", function() 81 | assert.are.same({ "i--" }, Helper:call({ "i++" }, { 1, 2 })) 82 | end) 83 | 84 | it("toggles '==' into '!='", function() 85 | assert.are.same({ "i == 8" }, Helper:call({ "i != 8" }, { 1, 3 })) 86 | end) 87 | 88 | it("toggles '!=' into '=='", function() 89 | assert.are.same({ "i != 8" }, Helper:call({ "i == 8" }, { 1, 3 })) 90 | end) 91 | 92 | it("toggles '&&' into '||'", function() 93 | assert.are.same( 94 | { "i == 8 && x == 9" }, 95 | Helper:call({ "i == 8 || x == 9" }, { 1, 8 }) 96 | ) 97 | end) 98 | 99 | it("toggles '||' into '&&'", function() 100 | assert.are.same( 101 | { "i == 8 || x == 9" }, 102 | Helper:call({ "i == 8 && x == 9" }, { 1, 8 }) 103 | ) 104 | end) 105 | end) 106 | -------------------------------------------------------------------------------- /spec/filetypes/git_rebase/cycle_command_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("git_rebase") 4 | 5 | describe("command", function() 6 | it("turns 'pick' into 'fixup'", function() 7 | assert.are.same({ "fixup" }, Helper:call("pick")) 8 | end) 9 | 10 | it("turns 'fixup' into 'reword'", function() 11 | assert.are.same({ "reword" }, Helper:call("fixup")) 12 | end) 13 | 14 | it("turns 'reword' into 'edit'", function() 15 | assert.are.same({ "edit" }, Helper:call("reword")) 16 | end) 17 | 18 | it("turns 'edit' into 'squash'", function() 19 | assert.are.same({ "squash" }, Helper:call("edit")) 20 | end) 21 | 22 | it("turns 'squash' into 'exec'", function() 23 | assert.are.same({ "exec" }, Helper:call("squash")) 24 | end) 25 | 26 | it("turns 'exec' into 'break'", function() 27 | assert.are.same({ "break" }, Helper:call("exec")) 28 | end) 29 | 30 | it("turns 'break' into 'drop'", function() 31 | assert.are.same({ "drop" }, Helper:call("break")) 32 | end) 33 | 34 | it("turns 'drop' into 'label'", function() 35 | assert.are.same({ "label" }, Helper:call("drop")) 36 | end) 37 | 38 | it("turns 'label' into 'reset'", function() 39 | assert.are.same({ "reset" }, Helper:call("label")) 40 | end) 41 | 42 | it("turns 'reset' into 'merge'", function() 43 | assert.are.same({ "merge" }, Helper:call("reset")) 44 | end) 45 | 46 | it("turns 'merge' into 'pick'", function() 47 | assert.are.same({ "pick" }, Helper:call("merge")) 48 | end) 49 | 50 | it("turns 'p' into 'fixup'", function() 51 | assert.are.same({ "fixup" }, Helper:call("p")) 52 | end) 53 | 54 | it("turns 'f' into 'reword'", function() 55 | assert.are.same({ "reword" }, Helper:call("f")) 56 | end) 57 | 58 | it("turns 'r' into 'edit'", function() 59 | assert.are.same({ "edit" }, Helper:call("r")) 60 | end) 61 | 62 | it("turns 'e' into 'squash'", function() 63 | assert.are.same({ "squash" }, Helper:call("e")) 64 | end) 65 | 66 | it("turns 's' into 'exec'", function() 67 | assert.are.same({ "exec" }, Helper:call("s")) 68 | end) 69 | 70 | it("turns 'x' into 'break'", function() 71 | assert.are.same({ "break" }, Helper:call("x")) 72 | end) 73 | 74 | it("turns 'b' into 'drop'", function() 75 | assert.are.same({ "drop" }, Helper:call("b")) 76 | end) 77 | 78 | it("turns 'd' into 'label'", function() 79 | assert.are.same({ "label" }, Helper:call("d")) 80 | end) 81 | 82 | it("turns 'l' into 'reset'", function() 83 | assert.are.same({ "reset" }, Helper:call("l")) 84 | end) 85 | 86 | it("turns 't' into 'merge'", function() 87 | assert.are.same({ "merge" }, Helper:call("t")) 88 | end) 89 | 90 | it("turns 'm' into 'pick'", function() 91 | assert.are.same({ "pick" }, Helper:call("m")) 92 | end) 93 | end) 94 | -------------------------------------------------------------------------------- /spec/filetypes/javascript_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("javascript", { shiftwidth = 2 }) 4 | 5 | describe("integer", function() 6 | it("adds underscores to long int", function() 7 | assert.are.same({ "1_000_000" }, Helper:call("1000000")) 8 | end) 9 | 10 | it("removes underscores from long int", function() 11 | assert.are.same({ "1000000" }, Helper:call("1_000_000")) 12 | end) 13 | 14 | it("doesn't change ints less than four places", function() 15 | assert.are.same({ "100" }, Helper:call("100")) 16 | end) 17 | end) 18 | 19 | describe("boolean", function() 20 | it("turns 'true' into 'false'", function() 21 | assert.are.same({ "false" }, Helper:call({ "true" })) 22 | end) 23 | 24 | it("turns 'false' into 'true'", function() 25 | assert.are.same({ "true" }, Helper:call({ "false" })) 26 | end) 27 | end) 28 | 29 | describe("array", function() 30 | it("expands single line array to multiple lines", function() 31 | assert.are.same({ 32 | "[", 33 | " 1,", 34 | " 2,", 35 | " 3", 36 | "]", 37 | }, Helper:call({ "[1, 2, 3]" })) 38 | end) 39 | 40 | it("doesn't expand child arrays", function() 41 | assert.are.same({ 42 | "[", 43 | " 1,", 44 | " 2,", 45 | " [3, 4, 5]", 46 | "]", 47 | }, Helper:call({ "[1, 2, [3, 4, 5]]" })) 48 | end) 49 | 50 | it("collapses multi-line array to single line", function() 51 | assert.are.same( 52 | { "[1, 2, 3]" }, 53 | Helper:call({ 54 | "[", 55 | " 1,", 56 | " 2,", 57 | " 3", 58 | "]", 59 | }) 60 | ) 61 | end) 62 | 63 | it("collapses child arrays", function() 64 | assert.are.same( 65 | { "[1, 2, [3, 4, 5]]" }, 66 | Helper:call({ 67 | "[", 68 | " 1,", 69 | " 2,", 70 | " [", 71 | " 3,", 72 | " 4,", 73 | " 5", 74 | " ]", 75 | "]", 76 | }) 77 | ) 78 | end) 79 | end) 80 | 81 | describe("arguments", function() 82 | it("expands arguments into multiple lines", function() 83 | assert.are.same({ 84 | "x(", 85 | " 1,", 86 | " 2,", 87 | " 3", 88 | ")", 89 | }, Helper:call({ "x(1, 2, 3)" }, { 1, 2 })) 90 | end) 91 | 92 | it("collapses into single line", function() 93 | assert.are.same(Helper:call({ "x(1, 2, 3)" }, { 1, 2 }), { 94 | "x(", 95 | " 1,", 96 | " 2,", 97 | " 3", 98 | ")", 99 | }) 100 | end) 101 | 102 | it("do not expand inner array/object", function() 103 | assert.are.same({ 104 | "x(", 105 | " 1,", 106 | " 2,", 107 | " [4, 5, 6]", 108 | ")", 109 | }, Helper:call({ "x(1, 2, [4, 5, 6])" }, { 1, 2 })) 110 | end) 111 | 112 | it("collapses inner array/object", function() 113 | assert.are.same( 114 | { "x(1, 2, [4, 5, 6])" }, 115 | Helper:call({ 116 | "x(", 117 | " 1,", 118 | " 2,", 119 | " [", 120 | " 4,", 121 | " 5,", 122 | " 6", 123 | " ]", 124 | ")", 125 | }, { 1, 2 }) 126 | ) 127 | end) 128 | end) 129 | -------------------------------------------------------------------------------- /spec/filetypes/julia_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("julia", { shiftwidth = 2 }) 4 | 5 | describe("boolean", function() 6 | it("toggles 'true' into 'false'", function() 7 | assert.are.same({ "i = true" }, Helper:call({ "i = false" }, { 1, 6 })) 8 | end) 9 | 10 | it("toggles 'false' into 'true'", function() 11 | assert.are.same({ "i = false" }, Helper:call({ "i = true" }, { 1, 6 })) 12 | end) 13 | end) 14 | 15 | describe("operator", function() 16 | it("toggles '<= into '>='", function() 17 | assert.are.same({ "i <= 8" }, Helper:call({ "i >= 8" }, { 1, 3 })) 18 | end) 19 | 20 | it("toggles '>=' into '<='", function() 21 | assert.are.same({ "i >= 8" }, Helper:call({ "i <= 8" }, { 1, 3 })) 22 | end) 23 | 24 | it("toggles '>' into '<'", function() 25 | assert.are.same({ "i > 8" }, Helper:call({ "i < 8" }, { 1, 3 })) 26 | end) 27 | 28 | it("toggles '<' into '>'", function() 29 | assert.are.same({ "i < 8" }, Helper:call({ "i > 8" }, { 1, 3 })) 30 | end) 31 | 32 | it("toggles '∉' into '∈'", function() 33 | assert.are.same({ "i ∈ 8" }, Helper:call({ "i ∉ 8" }, { 1, 3 })) 34 | end) 35 | 36 | it("toggles '∈' into '∉'", function() 37 | assert.are.same({ "i ∈ 8" }, Helper:call({ "i ∉ 8" }, { 1, 3 })) 38 | end) 39 | 40 | it("toggles '+' into '-'", function() 41 | assert.are.same({ "i + 8" }, Helper:call({ "i - 8" }, { 1, 3 })) 42 | end) 43 | 44 | it("toggles '*' into '/'", function() 45 | assert.are.same({ "i * 8" }, Helper:call({ "i / 8" }, { 1, 3 })) 46 | end) 47 | 48 | it("toggles '==' into '!='", function() 49 | assert.are.same({ "i == 8" }, Helper:call({ "i != 8" }, { 1, 3 })) 50 | end) 51 | 52 | it("toggles '!=' into '=='", function() 53 | assert.are.same({ "i != 8" }, Helper:call({ "i == 8" }, { 1, 3 })) 54 | end) 55 | end) 56 | 57 | describe("expand/collapse", function() 58 | it("expands vector_expression", function() 59 | assert.are.same( 60 | { "x = [1, 2]" }, 61 | Helper:call({ 62 | "x = [", 63 | " 1,", 64 | " 2", 65 | "]", 66 | }, { 1, 5 }) 67 | ) 68 | end) 69 | 70 | it("collapses vector_expression", function() 71 | assert.are.same( 72 | { "x = [", " 1,", " 2", "]" }, 73 | Helper:call({ "x = [1, 2]" }, { 1, 5 }) 74 | ) 75 | end) 76 | 77 | it("expands function definition", function() 78 | assert.are.same( 79 | { "fn(1, 2; x=true)" }, 80 | Helper:call({ 81 | "fn(", 82 | " 1,", 83 | " 2;", 84 | " x=true", 85 | ")", 86 | }, { 1, 3 }) 87 | ) 88 | end) 89 | 90 | it("collapses function definition", function() 91 | assert.are.same( 92 | { "fn(", " 1,", " 2;", " x=true", ")" }, 93 | Helper:call({ "fn(1, 2; x=true)" }, { 1, 3 }) 94 | ) 95 | end) 96 | 97 | it("expands tuple_expression", function() 98 | assert.are.same( 99 | { "x = (1, 2)" }, 100 | Helper:call({ 101 | "x = (", 102 | " 1,", 103 | " 2", 104 | ")", 105 | }, { 1, 5 }) 106 | ) 107 | end) 108 | 109 | it("collapses tuple_expression", function() 110 | assert.are.same( 111 | { "x = (", " 1,", " 2", ")" }, 112 | Helper:call({ "x = (1, 2)" }, { 1, 5 }) 113 | ) 114 | end) 115 | 116 | it("expands dict", function() 117 | assert.are.same( 118 | { 'Dict("key1"=>1, "key2"=>2)' }, 119 | Helper:call({ 120 | "Dict(", 121 | ' "key1"=>1,', 122 | ' "key2"=>2', 123 | ")", 124 | }, { 1, 5 }) 125 | ) 126 | end) 127 | 128 | it("collapses dict", function() 129 | assert.are.same( 130 | { "Dict(", ' "key1"=>1,', ' "key2"=>2', ")" }, 131 | Helper:call({ 'Dict("key1"=>1, "key2"=>2)' }, { 1, 5 }) 132 | ) 133 | end) 134 | end) 135 | 136 | describe("friendly integers", function() 137 | it("1 million to friendly", function() 138 | assert.are.same( 139 | { "x = 1000000" }, 140 | Helper:call({ 141 | "x = 1_000_000", 142 | }, { 1, 5 }) 143 | ) 144 | end) 145 | 146 | it("1 million to unfriendly", function() 147 | assert.are.same( 148 | { "x = 1_000_000" }, 149 | Helper:call({ 150 | "x = 1000000", 151 | }, { 1, 5 }) 152 | ) 153 | end) 154 | end) 155 | -------------------------------------------------------------------------------- /spec/filetypes/lua_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("lua", { shiftwidth = 4 }) 4 | 5 | describe("boolean", function() 6 | it("turns 'true' into 'false'", function() 7 | assert.are.same( 8 | { "local bool = false" }, 9 | Helper:call({ "local bool = true" }, { 1, 14 }) 10 | ) 11 | end) 12 | 13 | it("turns 'false' into 'true'", function() 14 | assert.are.same( 15 | { "local bool = true" }, 16 | Helper:call({ "local bool = false" }, { 1, 14 }) 17 | ) 18 | end) 19 | end) 20 | 21 | describe("table_constructor", function() 22 | it("expands single line table to multiple lines", function() 23 | assert.are.same({ 24 | "local tbl = {", 25 | " 1,", 26 | " 2,", 27 | " 3", 28 | "}", 29 | }, Helper:call({ "local tbl = { 1, 2, 3 }" }, { 1, 13 })) 30 | end) 31 | 32 | it("collapses multi line table to single lines", function() 33 | assert.are.same( 34 | { "local tbl = { 1, 2, 3 }" }, 35 | Helper:call({ 36 | "local tbl = {", 37 | " 1,", 38 | " 2,", 39 | " 3", 40 | "}", 41 | }, { 1, 13 }) 42 | ) 43 | end) 44 | 45 | it("expands single line table to multiple lines", function() 46 | assert.are.same({ 47 | "local tbl = {", 48 | " a = 1,", 49 | " b = 2,", 50 | " ['c'] = 3", 51 | "}", 52 | }, Helper:call({ "local tbl = { a = 1, b = 2, ['c'] = 3 }" }, { 1, 13 })) 53 | end) 54 | 55 | it("collapses multi line table to single lines", function() 56 | assert.are.same( 57 | { "local tbl = { a = 1, b = 2, ['c'] = 3 }" }, 58 | Helper:call({ 59 | "local tbl = {", 60 | " a = 1,", 61 | " b = 2,", 62 | " ['c'] = 3", 63 | "}", 64 | }, { 1, 13 }) 65 | ) 66 | end) 67 | 68 | it("collapsing doesn't change string values", function() 69 | assert.are.same( 70 | { [[local tbl = { a = 1, b = 2, ['c'] = 3, d = "-" }]] }, 71 | Helper:call({ 72 | "local tbl = {", 73 | " a = 1,", 74 | " b = 2,", 75 | " ['c'] = 3,", 76 | ' d = "-"', 77 | "}", 78 | }, { 1, 13 }) 79 | ) 80 | end) 81 | 82 | it("expanding doesn't change string values", function() 83 | assert.are.same( 84 | { 85 | "local tbl = {", 86 | " a = 1,", 87 | " b = 2,", 88 | " ['c'] = 3,", 89 | ' d = "-"', 90 | "}", 91 | }, 92 | Helper:call( 93 | { [[local tbl = { a = 1, b = 2, ['c'] = 3, d = "-" }]] }, 94 | { 1, 13 } 95 | ) 96 | ) 97 | end) 98 | end) 99 | 100 | describe("function_definition (anon)", function() 101 | it("collapses multi-line function to single line", function() 102 | assert.are.same( 103 | { "local a = function(a, b, c) return 1 end" }, 104 | Helper:call({ 105 | "local a = function(a, b, c)", 106 | " return 1", 107 | "end", 108 | }, { 1, 11 }) 109 | ) 110 | end) 111 | 112 | it("expands single-line function to multi-line", function() 113 | assert.are.same({ 114 | "local a = function(a, b, c)", 115 | " return 1", 116 | "end", 117 | }, Helper:call( 118 | { "local a = function(a, b, c) return 1 end" }, 119 | { 1, 11 } 120 | )) 121 | end) 122 | 123 | it("doesn't collapse function with multi-line body", function() 124 | local text = { 125 | "local a = function(a, b, c)", 126 | " local d = a + b + c", 127 | " return d", 128 | "end", 129 | } 130 | 131 | assert.are.same(text, Helper:call(text, { 1, 11 })) 132 | end) 133 | 134 | it("expands single-line function to multi-line (no-body)", function() 135 | assert.are.same({ 136 | "local a = function(a, b, c)", 137 | "", 138 | "end", 139 | }, Helper:call({ "local a = function(a, b, c) end" }, { 1, 11 })) 140 | end) 141 | 142 | it("collapses multi-line function to single-line (no-body)", function() 143 | assert.are.same( 144 | { 145 | "local a = function(a, b, c) end", 146 | }, 147 | Helper:call({ 148 | "local a = function(a, b, c)", 149 | "", 150 | "end", 151 | }, { 1, 11 }) 152 | ) 153 | end) 154 | end) 155 | 156 | describe("function_declaration (named)", function() 157 | it("collapses multi-line function to single line", function() 158 | assert.are.same( 159 | { "local function a(a, b, c) return 1 end" }, 160 | Helper:call({ 161 | "local function a(a, b, c)", 162 | " return 1", 163 | "end", 164 | }, { 1, 11 }) 165 | ) 166 | end) 167 | 168 | it("expands single-line function to multi-line", function() 169 | assert.are.same({ 170 | "local function a(a, b, c)", 171 | " return 1", 172 | "end", 173 | }, Helper:call({ "local function a(a, b, c) return 1 end" }, { 1, 11 })) 174 | end) 175 | 176 | it("doesn't collapse function with multi-line body", function() 177 | local text = { 178 | "local function a(a, b, c)", 179 | " local d = a + b + c", 180 | " return d", 181 | "end", 182 | } 183 | 184 | assert.are.same(text, Helper:call(text, { 1, 11 })) 185 | end) 186 | end) 187 | -------------------------------------------------------------------------------- /spec/filetypes/python/comparison_operator_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("./spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("python", { shiftwidth = 4 }) 4 | 5 | describe("comparison_operator", function() 6 | it("toggles operator in multiline context", function() 7 | assert.are.same( 8 | { 9 | [[if (100 <]], 10 | [[ foo(x,]], 11 | [[ y)):]], 12 | [[ x = 1]], 13 | }, 14 | Helper:call({ 15 | [[if (100 >]], 16 | [[ foo(x,]], 17 | [[ y)):]], 18 | [[ x = 1]], 19 | }, { 1, 9 }) 20 | ) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /spec/filetypes/python/conditional_expression_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("./spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("python", { shiftwidth = 4 }) 4 | 5 | describe("conditional_expression", function() 6 | it("expands with a single assignment", function() 7 | assert.are.same({ 8 | [[if foo(y):]], 9 | [[ x = 1]], 10 | [[else:]], 11 | [[ x = 2]], 12 | }, Helper:call({ [[x = 1 if foo(y) else 2]] }, { 1, 7 })) 13 | end) 14 | 15 | it("expands with a multi assignment", function() 16 | assert.are.same({ 17 | [[if foo(y):]], 18 | [[ x = y = z = 1]], 19 | [[else:]], 20 | [[ x = y = z = 2]], 21 | }, Helper:call({ [[x = y = z = 1 if foo(y) else 2]] }, { 1, 15 })) 22 | end) 23 | 24 | it("expands with a return", function() 25 | assert.are.same({ 26 | [[if foo(y):]], 27 | [[ return 1]], 28 | [[else:]], 29 | [[ return 2]], 30 | }, Helper:call({ [[return 1 if foo(y) else 2]] }, { 1, 10 })) 31 | end) 32 | 33 | it("expands with function calls", function() 34 | assert.are.same({ 35 | [[if foo(y):]], 36 | [[ bar()]], 37 | [[else:]], 38 | [[ baz()]], 39 | }, Helper:call({ [[bar() if foo(y) else baz()]] }, { 1, 7 })) 40 | end) 41 | 42 | it("expands with both parenthesized lambda expr", function() 43 | assert.are.same( 44 | { 45 | [[if z is not None:]], 46 | [[ x = (lambda y: y + 1)]], 47 | [[else:]], 48 | [[ x = (lambda y: y - 1)]], 49 | }, 50 | Helper:call( 51 | { [[x = (lambda y: y + 1) if z is not None else (lambda y: y - 1)]] }, 52 | { 1, 23 } 53 | ) 54 | ) 55 | end) 56 | 57 | it( 58 | "doesn't expand with a bare consequence lambda expr ('if' is inside lambda)", 59 | function() 60 | local text = { 61 | [[x = lambda y: y + 1 if z is not None else lambda y: y - 1]], 62 | } 63 | assert.are.same(text, Helper:call(text, { 1, 23 })) 64 | end 65 | ) 66 | 67 | it("expands with a parenthesized consequence lambda expr", function() 68 | assert.are.same( 69 | { 70 | [[if z is not None:]], 71 | [[ x = (lambda y: y + 1)]], 72 | [[else:]], 73 | [[ x = lambda y: y - 1]], 74 | }, 75 | Helper:call( 76 | { [[x = (lambda y: y + 1) if z is not None else lambda y: y - 1]] }, 77 | { 1, 23 } 78 | ) 79 | ) 80 | end) 81 | 82 | it("expands when after a for_statement", function() 83 | assert.are.same( 84 | { 85 | [[for x in range(10):]], 86 | [[ if x % 2 == 0:]], 87 | [[ print(x)]], 88 | [[ else:]], 89 | [[ print(x + 1)]], 90 | }, 91 | Helper:call( 92 | { [[for x in range(10): print(x) if x % 2 == 0 else print(x + 1)]] }, 93 | { 1, 30 } 94 | ) 95 | ) 96 | end) 97 | 98 | it("expands when after an if_statement", function() 99 | assert.are.same( 100 | { 101 | [[if x % 2 == 0:]], 102 | [[ if x > 10:]], 103 | [[ print(x)]], 104 | [[ else:]], 105 | [[ print(x + 1)]], 106 | }, 107 | Helper:call( 108 | { [[if x % 2 == 0: print(x) if x > 10 else print(x + 1)]] }, 109 | { 1, 25 } 110 | ) 111 | ) 112 | end) 113 | 114 | it("expands with multiline parenthesized_expression", function() 115 | assert.are.same( 116 | { 117 | [[if (foo() > 100 or foo() < 200):]], 118 | [[ y = x = (1 or 3)]], 119 | [[else:]], 120 | [[ y = x = (2 or 4)]], 121 | }, 122 | Helper:call({ 123 | [[y = x = (1 or]], 124 | [[ 3) if (foo() > 100 or]], 125 | [[ foo() < 200) else (2 or]], 126 | [[ 4)]], 127 | }, { 2, 13 }) 128 | ) 129 | end) 130 | 131 | it("expands with multiline structures and fn args", function() 132 | assert.are.same( 133 | { 134 | [[if foo(x, y):]], 135 | [=[ return [3, 4, 5]]=], 136 | [[else:]], 137 | [[ return {4, 5, 6}]], 138 | }, 139 | Helper:call({ 140 | [[return []], 141 | [[ 3,]], 142 | [[ 4,]], 143 | [[ 5]], 144 | [[] if foo(x,]], 145 | [[ y) else {]], 146 | [[ 4,]], 147 | [[ 5,]], 148 | [[ 6]], 149 | [[}]], 150 | }, { 5, 3 }) 151 | ) 152 | end) 153 | 154 | it("expands a multiline expr with trailing comment", function() 155 | assert.are.same( 156 | { 157 | [[if foo(x, y):]], 158 | [=[ return [3, 4, 5]]=], 159 | [[else:]], 160 | [[ return {4, 5, 6} # j]], 161 | }, 162 | Helper:call({ 163 | [[return []], 164 | [[ 3,]], 165 | [[ 4,]], 166 | [[ 5]], 167 | [[] if foo(x,]], 168 | [[ y) else {]], 169 | [[ 4,]], 170 | [[ 5,]], 171 | [[ 6]], 172 | [[} # j]], 173 | }, { 5, 3 }) 174 | ) 175 | end) 176 | 177 | it("doesn't expand a multiline expr with embedded comments", function() 178 | local text = { 179 | [[return [ # a]], 180 | [[ 3, # b]], 181 | [[ 4, # c]], 182 | [[ 5 # d]], 183 | [[] if foo(x, # e]], 184 | [[ y) else { # f]], 185 | [[ 4, # g]], 186 | [[ 5, # h]], 187 | [[ 6 # i]], 188 | [[}]], 189 | } 190 | assert.are.same(text, Helper:call(text, { 5, 3 })) 191 | end) 192 | 193 | it("doesn't expand a condition inside a fn call", function() 194 | local text = { 195 | [[foo("param1", 4 if foo() > 100 else 5)]], 196 | } 197 | assert.are.same(text, Helper:call(text, { 1, 17 })) 198 | end) 199 | 200 | it("doesn't expand a condition inside a lambda inside a fn call", function() 201 | local text = { 202 | [[foo("param1", lambda x: 4 if foo() > 100 else 5)]], 203 | } 204 | assert.are.same(text, Helper:call(text, { 1, 26 })) 205 | end) 206 | 207 | it("doesn't expand inside a list comprehension", function() 208 | local text = { 209 | [[foo([x for x in range(10) if x % 2 == 0])]], 210 | } 211 | assert.are.same(text, Helper:call(text, { 1, 27 })) 212 | end) 213 | 214 | it("doesn't expand inside a list", function() 215 | local text = { 216 | [=[return [0, 123 if foo() > 100 else 456]]=], 217 | } 218 | assert.are.same(text, Helper:call(text, { 1, 16 })) 219 | end) 220 | end) 221 | -------------------------------------------------------------------------------- /spec/filetypes/python/conditional_padding_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("./spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("python", { shiftwidth = 4 }) 4 | 5 | describe("conditional padding", function() 6 | it("checking 'is not'", function() 7 | assert.are.same( 8 | { 9 | [[x = 1 if y is not None and foo() > 100 else 2]], 10 | }, 11 | Helper:call({ 12 | "if y is not None and foo() > 100:", 13 | " x = 1", 14 | "else:", 15 | " x = 2", 16 | }) 17 | ) 18 | end) 19 | 20 | it("checking unary and binary '-' operator", function() 21 | assert.are.same( 22 | { 23 | "xs = [x for x in range(10) if x + -3 or -x and x - 3 == 0 and abs(x - 1) < 2]", 24 | }, 25 | Helper:call({ 26 | "xs = [", 27 | " x", 28 | " for x in range(10)", 29 | " if x + -3 or -x and x - 3 == 0 and abs(x - 1) < 2", 30 | "]", 31 | }, { 1, 6 }) 32 | ) 33 | end) 34 | 35 | it("checking 'not in'", function() 36 | assert.are.same( 37 | { "print(5 not in list1)" }, 38 | Helper:call({ 39 | "print(", 40 | " 5 not in list1", 41 | ")", 42 | }, { 1, 6 }) 43 | ) 44 | end) 45 | end) 46 | -------------------------------------------------------------------------------- /spec/filetypes/python/if_statement_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("./spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("python", { shiftwidth = 4 }) 4 | 5 | describe("if_statement", function() 6 | it("if/else inlines with a single assignment", function() 7 | assert.are.same( 8 | { [[x = 1 if foo(y) else 2]] }, 9 | Helper:call({ 10 | [[if foo(y):]], 11 | [[ x = 1]], 12 | [[else:]], 13 | [[ x = 2]], 14 | }) 15 | ) 16 | end) 17 | 18 | it("if/else inlines with a multi assignment", function() 19 | assert.are.same( 20 | { [[x = y = z = 1 if foo(a) else 2]] }, 21 | Helper:call({ 22 | [[if foo(a):]], 23 | [[ x = y = z = 1]], 24 | [[else:]], 25 | [[ x = y = z = 2]], 26 | }) 27 | ) 28 | end) 29 | 30 | it( 31 | "if/else doesn't inline a multi assignment when identifiers differ (consequence)", 32 | function() 33 | local text = { 34 | [[if foo(a):]], 35 | [[ c = y = z = 1]], 36 | [[else:]], 37 | [[ x = y = z = 2]], 38 | } 39 | assert.are.same(text, text) 40 | end 41 | ) 42 | 43 | it( 44 | "if/else doesn't inline a multi assignment when identifiers differ (alternative)", 45 | function() 46 | local text = { 47 | [[if foo(a):]], 48 | [[ x = y = z = 1]], 49 | [[else:]], 50 | [[ x = y = c = 2]], 51 | } 52 | assert.are.same(text, text) 53 | end 54 | ) 55 | 56 | it("if/else inlines with a return", function() 57 | assert.are.same( 58 | { [[return 1 if foo(y) else 2]] }, 59 | Helper:call({ 60 | [[if foo(y):]], 61 | [[ return 1]], 62 | [[else:]], 63 | [[ return 2]], 64 | }) 65 | ) 66 | end) 67 | 68 | it("if/else inlines with function calls", function() 69 | assert.are.same( 70 | { [[bar() if foo(y) else baz()]] }, 71 | Helper:call({ 72 | [[if foo(y):]], 73 | [[ bar()]], 74 | [[else:]], 75 | [[ baz()]], 76 | }) 77 | ) 78 | end) 79 | 80 | it("if/else inlines with parenthesized lambda expr", function() 81 | assert.are.same( 82 | { [[x = (lambda y: y + 1) if z is not None else (lambda y: y - 1)]] }, 83 | Helper:call({ 84 | [[if z is not None:]], 85 | [[ x = (lambda y: y + 1)]], 86 | [[else:]], 87 | [[ x = (lambda y: y - 1)]], 88 | }) 89 | ) 90 | end) 91 | 92 | it("if/else inlines with bare lambda expr (auto parens)", function() 93 | assert.are.same( 94 | { [[x = (lambda y: y + 3) if z is not None else (lambda y: y - 4)]] }, 95 | Helper:call({ 96 | [[if z is not None:]], 97 | [[ x = lambda y: y + 3]], 98 | [[else:]], 99 | [[ x = lambda y: y - 4]], 100 | }) 101 | ) 102 | end) 103 | 104 | it("if/else inlines with bare boolean_operator (auto parens)", function() 105 | assert.are.same( 106 | { [[x = (a or b) if z is not None else (c or d)]] }, 107 | Helper:call({ 108 | [[if z is not None:]], 109 | [[ x = a or b]], 110 | [[else:]], 111 | [[ x = c or d]], 112 | }) 113 | ) 114 | end) 115 | 116 | it( 117 | "if/else inlines with bare conditional_expression (auto parens)", 118 | function() 119 | assert.are.same( 120 | { [[x = (3 if a else 4) if z is not None else (5 if b else 6)]] }, 121 | Helper:call({ 122 | [[if z is not None:]], 123 | [[ x = 3 if a else 4]], 124 | [[else:]], 125 | [[ x = 5 if b else 6]], 126 | }) 127 | ) 128 | end 129 | ) 130 | 131 | it( 132 | "if/else inlines with multiline parenthized fn args, boolean op, structures", 133 | function() 134 | assert.are.same( 135 | { 136 | [=[y = x = [1, 3] if (foo(a, b) > 100 or foo(c, d) < 200) else (False or True)]=], 137 | }, 138 | Helper:call({ 139 | [[if (foo(a,]], 140 | [[ b) > 100 or]], 141 | [[ foo(c,]], 142 | [[ d) < 200):]], 143 | [[ y = x = [1,]], 144 | [=[ 3]]=], 145 | [[else:]], 146 | [[ y = x = (False or]], 147 | [[ True)]], 148 | }) 149 | ) 150 | end 151 | ) 152 | 153 | it("if inlines with a single assignment", function() 154 | assert.are.same( 155 | { [[if foo(y): x = 1]] }, 156 | Helper:call({ 157 | [[if foo(y):]], 158 | [[ x = 1]], 159 | }) 160 | ) 161 | end) 162 | 163 | it("if inlines with a multi assignment", function() 164 | assert.are.same( 165 | { [[if foo(a): x = y = z = 1]] }, 166 | Helper:call({ 167 | [[if foo(a):]], 168 | [[ x = y = z = 1]], 169 | }) 170 | ) 171 | end) 172 | 173 | it("if inlines with a return", function() 174 | assert.are.same( 175 | { [[if foo(y): return 1]] }, 176 | Helper:call({ 177 | [[if foo(y):]], 178 | [[ return 1]], 179 | }) 180 | ) 181 | end) 182 | 183 | it("if inlines with function calls", function() 184 | assert.are.same( 185 | { [[if foo(y): bar()]] }, 186 | Helper:call({ 187 | [[if foo(y):]], 188 | [[ bar()]], 189 | }) 190 | ) 191 | end) 192 | 193 | it("if expands with a single assignment", function() 194 | assert.are.same( 195 | { 196 | [[if foo(y):]], 197 | [[ x = 1]], 198 | }, 199 | Helper:call({ 200 | [[if foo(y): x = 1]], 201 | }) 202 | ) 203 | end) 204 | 205 | it("if expands with a multi assignment", function() 206 | assert.are.same( 207 | { 208 | [[if foo(a):]], 209 | [[ x = y = z = 1]], 210 | }, 211 | Helper:call({ 212 | [[if foo(a): x = y = z = 1]], 213 | }) 214 | ) 215 | end) 216 | 217 | it("if expands with a return", function() 218 | assert.are.same( 219 | { 220 | [[if foo(y):]], 221 | [[ return 1]], 222 | }, 223 | Helper:call({ 224 | [[if foo(y): return 1]], 225 | }) 226 | ) 227 | end) 228 | 229 | it("if expands with function calls", function() 230 | assert.are.same( 231 | { 232 | [[if foo(y):]], 233 | [[ bar()]], 234 | }, 235 | Helper:call({ 236 | [[if foo(y): bar()]], 237 | }) 238 | ) 239 | end) 240 | 241 | it("if/else doesn't inline when there are comments", function() 242 | local text = { 243 | [[if foo(y):]], 244 | [[ # comment]], 245 | [[ x = 1]], 246 | [[else:]], 247 | [[ # comment]], 248 | [[ x = 2]], 249 | } 250 | assert.are.same(text, Helper:call(text)) 251 | end) 252 | 253 | it("if doesn't inline when there are comments", function() 254 | local text = { 255 | [[if foo(y):]], 256 | [[ # comment]], 257 | [[ x = 1]], 258 | } 259 | assert.are.same(text, Helper:call(text)) 260 | end) 261 | end) 262 | -------------------------------------------------------------------------------- /spec/filetypes/python/quotes_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("./spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("python", { shiftwidth = 4 }) 4 | 5 | local single = "'" 6 | local double = '"' 7 | local triple1 = "'''" 8 | local triple2 = '"""' 9 | 10 | describe("string", function() 11 | it("toggles " .. single .. " into " .. double, function() 12 | local input = { single .. "string" .. single } 13 | local want = { double .. "string" .. double } 14 | assert.are.same(want, Helper:call(input, { 1, 1 })) 15 | assert.are.same(want, Helper:call(input, { 1, 4 })) 16 | assert.are.same(want, Helper:call(input, { 1, #input })) 17 | end) 18 | it("toggles " .. double .. " into " .. single, function() 19 | local input = { double .. "string" .. double } 20 | local want = { single .. "string" .. single } 21 | assert.are.same(want, Helper:call(input, { 1, 1 })) 22 | assert.are.same(want, Helper:call(input, { 1, 4 })) 23 | assert.are.same(want, Helper:call(input, { 1, #input })) 24 | end) 25 | it("toggles " .. triple1 .. " into " .. triple2, function() 26 | local input = { triple1 .. "string" .. triple1 } 27 | local want = { triple2 .. "string" .. triple2 } 28 | assert.are.same(want, Helper:call(input, { 1, 1 })) 29 | assert.are.same(want, Helper:call(input, { 1, 4 })) 30 | assert.are.same(want, Helper:call(input, { 1, #input })) 31 | end) 32 | it("toggles " .. triple2 .. " into " .. triple1, function() 33 | local input = { triple2 .. "string" .. triple2 } 34 | local want = { triple1 .. "string" .. triple1 } 35 | assert.are.same(want, Helper:call(input, { 1, 1 })) 36 | assert.are.same(want, Helper:call(input, { 1, 4 })) 37 | assert.are.same(want, Helper:call(input, { 1, #input })) 38 | end) 39 | it( 40 | "toggles multi-line " .. triple2 .. " into multi-line " .. triple1, 41 | function() 42 | local input = { triple2, "string", triple2 } 43 | local want = { triple1, "string", triple1 } 44 | assert.are.same(want, Helper:call(input, { 1, 1 })) 45 | assert.are.same(want, Helper:call(input, { 2, 1 })) 46 | assert.are.same(want, Helper:call(input, { 3, 1 })) 47 | end 48 | ) 49 | it( 50 | "toggles multi-line " .. triple1 .. " into multi-line " .. triple2, 51 | function() 52 | local input = { triple1, "string", triple1 } 53 | local want = { triple2, "string", triple2 } 54 | assert.are.same(want, Helper:call(input, { 1, 1 })) 55 | assert.are.same(want, Helper:call(input, { 2, 1 })) 56 | assert.are.same(want, Helper:call(input, { 3, 1 })) 57 | end 58 | ) 59 | it("toggles f-strings", function() 60 | local input = { "f" .. single .. "string {foo}" .. single } 61 | local want = { "f" .. double .. "string {foo}" .. double } 62 | assert.are.same(want, Helper:call(input, { 1, 1 })) 63 | assert.are.same(want, Helper:call(input, { 1, 4 })) 64 | assert.are.same(want, Helper:call(input, { 1, #input })) 65 | end) 66 | it("toggles multi-line f-strings", function() 67 | local input = { "f" .. triple2, "string {foo}", triple2 } 68 | local want = { "f" .. triple1, "string {foo}", triple1 } 69 | assert.are.same(want, Helper:call(input, { 1, 1 })) 70 | assert.are.same(want, Helper:call(input, { 2, 1 })) 71 | assert.are.same(want, Helper:call(input, { 3, 1 })) 72 | end) 73 | it("toggles multi-line f-string with nested string", function() 74 | local input = { "f" .. triple2, 'string {"nested"}', triple2 } 75 | local want_outer = { "f" .. triple1, 'string {"nested"}', triple1 } 76 | assert.are.same(want_outer, Helper:call(input, { 1, 1 })) 77 | assert.are.same(want_outer, Helper:call(input, { 2, 1 })) 78 | assert.are.same(want_outer, Helper:call(input, { 3, 1 })) 79 | 80 | local want_nested = { "f" .. triple2, "string {'nested'}", triple2 } 81 | assert.are.same(want_nested, Helper:call(input, { 2, 12 })) 82 | end) 83 | end) 84 | -------------------------------------------------------------------------------- /spec/filetypes/r_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("r", { shiftwidth = 2 }) 4 | 5 | describe("boolean", function() 6 | it("turns 'TRUE' into 'FALSE'", function() 7 | assert.are.same({ "i == FALSE" }, Helper:call({ "i == TRUE" }, { 1, 7 })) 8 | end) 9 | 10 | it("turns 'FALSE' into 'TRUE'", function() 11 | assert.are.same({ "i == TRUE" }, Helper:call({ "i == FALSE" }, { 1, 7 })) 12 | end) 13 | end) 14 | 15 | describe("multiline", function() 16 | it("expand single line formal parameters to multiline", function() 17 | assert.are.same({ 18 | "foo <- function(", 19 | " bar,", 20 | " baz", 21 | ")", 22 | }, Helper:call({ "foo <- function(bar, baz)" }, { 1, 16 })) 23 | end) 24 | 25 | it("collapse multiline formal parameters to single line", function() 26 | assert.are.same( 27 | { "foo <- function(bar, baz)" }, 28 | Helper:call({ 29 | "foo <- function(", 30 | " bar,", 31 | " baz", 32 | ")", 33 | }, { 1, 16 }) 34 | ) 35 | end) 36 | end) 37 | 38 | describe("multiline_args", function() 39 | it("expand single line arguments to multiline", function() 40 | assert.are.same({ 41 | "foo(", 42 | " bar = buf,", 43 | " 'baz',", 44 | " 'bap'", 45 | ")", 46 | }, Helper:call({ "foo(bar = buf, 'baz', 'bap')" }, { 1, 4 })) 47 | end) 48 | 49 | it("collapse multiline arguments to single line", function() 50 | assert.are.same( 51 | { "foo(bar = buf, 'baz', 'bap')" }, 52 | Helper:call({ 53 | "foo(", 54 | " bar = buf,", 55 | " 'baz',", 56 | " 'bap'", 57 | " )", 58 | }, { 1, 4 }) 59 | ) 60 | end) 61 | end) 62 | -------------------------------------------------------------------------------- /spec/filetypes/ruby_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("ruby") 4 | 5 | describe("integer", function() 6 | it("adds underscores to long int", function() 7 | assert.are.same({ "1_000_000" }, Helper:call("1000000")) 8 | end) 9 | 10 | it("removes underscores from long int", function() 11 | assert.are.same({ "1000000" }, Helper:call("1_000_000")) 12 | end) 13 | 14 | it("doesn't change ints less than four places", function() 15 | assert.are.same({ "100" }, Helper:call("100")) 16 | end) 17 | end) 18 | 19 | describe("if", function() 20 | it("expands ternary to multiline expression", function() 21 | assert.are.same({ 22 | [[if greet?]], 23 | [[ puts("hello")]], 24 | [[else]], 25 | [[ puts("booooo")]], 26 | [[end]], 27 | }, Helper:call( 28 | { [[greet? ? puts("hello") : puts("booooo")]] }, 29 | { 1, 7 } 30 | )) 31 | end) 32 | 33 | it("inlines to ternary statement", function() 34 | assert.are.same( 35 | { [[greet? ? puts("hello") : puts("booooo")]] }, 36 | Helper:call({ 37 | [[if greet?]], 38 | [[ puts("hello")]], 39 | [[else]], 40 | [[ puts("booooo")]], 41 | [[end]], 42 | }) 43 | ) 44 | end) 45 | end) 46 | 47 | describe("if_modifier", function() 48 | it("expands from one line to three", function() 49 | assert.are.same({ 50 | [[if greet?]], 51 | [[ puts "hello"]], 52 | [[end]], 53 | }, Helper:call({ [[puts "hello" if greet?]] }, { 1, 13 })) 54 | end) 55 | 56 | it("collapses from three lines to one", function() 57 | assert.are.same( 58 | { [[puts "hello" if greet?]] }, 59 | Helper:call({ 60 | [[if greet?]], 61 | [[ puts "hello"]], 62 | [[end]], 63 | }) 64 | ) 65 | end) 66 | 67 | it("can handle more complex conditions", function() 68 | assert.are.same( 69 | { 70 | [[if greet? && 1 == 2 || something * 3 <= 10]], 71 | [[ puts "hello"]], 72 | [[end]], 73 | }, 74 | Helper:call( 75 | { [[puts "hello" if greet? && 1 == 2 || something * 3 <= 10]] }, 76 | { 1, 13 } 77 | ) 78 | ) 79 | end) 80 | 81 | it("doesn't change conditionals with multi-line bodies", function() 82 | local text = { 83 | [[if greet?]], 84 | [[ puts "hello"]], 85 | [[ puts "hello"]], 86 | [[ puts "hello"]], 87 | [[end]], 88 | } 89 | 90 | assert.are.same(text, Helper:call(text)) 91 | end) 92 | end) 93 | 94 | describe("unless_modifier", function() 95 | it("expands from one line to three", function() 96 | assert.are.same({ 97 | [[unless rude?]], 98 | [[ puts "hello"]], 99 | [[end]], 100 | }, Helper:call({ [[puts "hello" unless rude?]] }, { 1, 13 })) 101 | end) 102 | 103 | it("collapses from three lines to one", function() 104 | assert.are.same( 105 | { [[puts "hello" unless rude?]] }, 106 | Helper:call({ 107 | [[unless rude?]], 108 | [[ puts "hello"]], 109 | [[end]], 110 | }) 111 | ) 112 | end) 113 | 114 | it("can handle more complex conditions", function() 115 | assert.are.same( 116 | { 117 | [[unless rude? && 1 == 2 || something * 3 <= 10]], 118 | [[ puts "hello"]], 119 | [[end]], 120 | }, 121 | Helper:call( 122 | { [[puts "hello" unless rude? && 1 == 2 || something * 3 <= 10]] }, 123 | { 1, 13 } 124 | ) 125 | ) 126 | end) 127 | end) 128 | 129 | describe("binary", function() 130 | it("flips == into !=", function() 131 | assert.are.same({ "1 != 1" }, Helper:call({ "1 == 1" }, { 1, 3 })) 132 | end) 133 | 134 | it("flips != into ==", function() 135 | assert.are.same({ "1 == 1" }, Helper:call({ "1 != 1" }, { 1, 3 })) 136 | end) 137 | 138 | it("flips > into <", function() 139 | assert.are.same({ "1 < 1" }, Helper:call({ "1 > 1" }, { 1, 3 })) 140 | end) 141 | 142 | it("flips < into >", function() 143 | assert.are.same({ "1 > 1" }, Helper:call({ "1 < 1" }, { 1, 3 })) 144 | end) 145 | 146 | it("flips >= into <=", function() 147 | assert.are.same({ "1 <= 1" }, Helper:call({ "1 >= 1" }, { 1, 3 })) 148 | end) 149 | 150 | it("flips <= into >=", function() 151 | assert.are.same({ "1 >= 1" }, Helper:call({ "1 <= 1" }, { 1, 3 })) 152 | end) 153 | end) 154 | 155 | describe("boolean", function() 156 | it("turns 'true' into 'false'", function() 157 | assert.are.same({ "false" }, Helper:call({ "true" })) 158 | end) 159 | 160 | it("turns 'false' into 'true'", function() 161 | assert.are.same({ "true" }, Helper:call({ "false" })) 162 | end) 163 | end) 164 | 165 | describe("array", function() 166 | it("expands single line array to multiple lines", function() 167 | assert.are.same({ 168 | "[", 169 | " 1,", 170 | " 2,", 171 | " 3", 172 | "]", 173 | }, Helper:call({ "[1, 2, 3]" })) 174 | end) 175 | 176 | it("doesn't expand child arrays", function() 177 | assert.are.same({ 178 | "[", 179 | " 1,", 180 | " 2,", 181 | " [3, 4, 5]", 182 | "]", 183 | }, Helper:call({ "[1, 2, [3, 4, 5]]" })) 184 | end) 185 | 186 | it("collapses multi-line array to single line", function() 187 | assert.are.same( 188 | { "[1, 2, 3]" }, 189 | Helper:call({ 190 | "[", 191 | " 1,", 192 | " 2,", 193 | " 3", 194 | "]", 195 | }) 196 | ) 197 | end) 198 | 199 | it("collapses child arrays", function() 200 | assert.are.same( 201 | { "[1, 2, [3, 4, 5]]" }, 202 | Helper:call({ 203 | "[", 204 | " 1,", 205 | " 2,", 206 | " [", 207 | " 3,", 208 | " 4,", 209 | " 5", 210 | " ]", 211 | "]", 212 | }) 213 | ) 214 | end) 215 | 216 | it("doesn't collapse multi-line array with embedded comments", function() 217 | local text = { 218 | "[ # a", 219 | " 1, # b", 220 | " 2, # c", 221 | "=begin", 222 | "a multiline comment here", 223 | "and one more line", 224 | "=end", 225 | " 3 # d", 226 | "]", 227 | } 228 | 229 | assert.are.same(text, Helper:call(text)) 230 | end) 231 | 232 | it("doesn't collapse array with nested child comments", function() 233 | local text = { 234 | "[", 235 | " 1,", 236 | " 2,", 237 | " [ # a", 238 | " 3, # b", 239 | " 4, # c", 240 | "=begin", 241 | "a multiline comment here", 242 | "and one more line", 243 | "=end", 244 | " 5 # d", 245 | " ]", 246 | "]", 247 | } 248 | assert.are.same(text, Helper:call(text)) 249 | end) 250 | 251 | it("doesn't collapse array with inline comments", function() 252 | local text = { 253 | "[", 254 | " 1, # no comment", 255 | " 2,", 256 | "]", 257 | } 258 | assert.are.same(text, Helper:call(text)) 259 | end) 260 | end) 261 | 262 | describe("hash", function() 263 | it("expands single line hash to multiple lines", function() 264 | assert.are.same({ 265 | "{", 266 | " a: 1,", 267 | " b: 2,", 268 | " c: 3", 269 | "}", 270 | }, Helper:call({ "{ a: 1, b: 2, c: 3 }" })) 271 | end) 272 | 273 | it("collapses multi-line hash to single lines", function() 274 | assert.are.same( 275 | { "{ a: 1, b: 2, c: 3 }" }, 276 | Helper:call({ 277 | "{", 278 | " a: 1,", 279 | " b: 2,", 280 | " c: 3", 281 | "}", 282 | }) 283 | ) 284 | end) 285 | 286 | it("doesn't expand children", function() 287 | assert.are.same({ 288 | "{", 289 | " a: 1,", 290 | " b: ['foo', 'bar'],", 291 | " c: { d: 3, e: 4 }", 292 | "}", 293 | }, Helper:call({ "{ a: 1, b: ['foo', 'bar'], c: { d: 3, e: 4 } }" })) 294 | end) 295 | 296 | it("collapses nested children", function() 297 | assert.are.same( 298 | { "{ a: 1, b: ['foo', 'bar'], c: { d: 3, e: 4 } }" }, 299 | Helper:call({ 300 | "{", 301 | " a: 1,", 302 | " b: [", 303 | " 'foo',", 304 | " 'bar'", 305 | " ],", 306 | " c: { ", 307 | " d: 3,", 308 | " e: 4", 309 | " }", 310 | "}", 311 | }) 312 | ) 313 | end) 314 | end) 315 | 316 | describe("block", function() 317 | it("collapses a multi-line block into one line (with param)", function() 318 | assert.are.same( 319 | { "[1, 2, 3].each { |n| print n }" }, 320 | Helper:call({ 321 | "[1, 2, 3].each do |n|", 322 | " print n", 323 | "end", 324 | }, { 1, 16 }) 325 | ) 326 | end) 327 | 328 | it("collapses a multi-line block into one line (without param)", function() 329 | assert.are.same( 330 | { "[1, 2, 3].each { print n }" }, 331 | Helper:call({ 332 | "[1, 2, 3].each do", 333 | " print n", 334 | "end", 335 | }, { 1, 16 }) 336 | ) 337 | end) 338 | 339 | it( 340 | "collapses a multi-line block into one line (with destructured param)", 341 | function() 342 | assert.are.same( 343 | { "[1, 2, 3].each { |(a, b), c| print n }" }, 344 | Helper:call({ 345 | "[1, 2, 3].each do |(a, b), c|", 346 | " print n", 347 | "end", 348 | }, { 1, 16 }) 349 | ) 350 | end 351 | ) 352 | end) 353 | 354 | describe("do_block", function() 355 | it("expands a single-line block into multi line (with param)", function() 356 | assert.are.same({ 357 | "[1, 2, 3].each do |n|", 358 | " print n", 359 | "end", 360 | }, Helper:call({ "[1, 2, 3].each { |n| print n }" }, { 1, 16 })) 361 | end) 362 | 363 | it("expands a single-line block into multi line (without param)", function() 364 | assert.are.same({ 365 | "[1, 2, 3].each do", 366 | " print n", 367 | "end", 368 | }, Helper:call({ "[1, 2, 3].each { print n }" }, { 1, 16 })) 369 | end) 370 | 371 | it( 372 | "expands a single-line block into multi line (with destructured param)", 373 | function() 374 | assert.are.same({ 375 | "[1, 2, 3].each do |(a, b), c|", 376 | " print n", 377 | "end", 378 | }, Helper:call( 379 | { "[1, 2, 3].each { |(a, b), c| print n }" }, 380 | { 1, 16 } 381 | )) 382 | end 383 | ) 384 | end) 385 | 386 | describe("pair", function() 387 | it("converts old style hashes into new style", function() 388 | assert.are.same({ "{ a: 1 }" }, Helper:call({ "{ :a => 1 }" }, { 1, 6 })) 389 | end) 390 | 391 | it("converts new style hashes into old style", function() 392 | assert.are.same({ "{ :a => 1 }" }, Helper:call({ "{ a: 1 }" }, { 1, 4 })) 393 | end) 394 | 395 | it("doesn't change non-string/symbol keys", function() 396 | assert.are.same( 397 | { "{ [1, 2] => 1 }" }, 398 | Helper:call({ "{ [1, 2] => 1 }" }, { 1, 10 }) 399 | ) 400 | end) 401 | end) 402 | 403 | describe("argument_list", function() 404 | it("expands one positional arg", function() 405 | assert.are.same({ 406 | "call(", 407 | " a", 408 | ")", 409 | }, Helper:call({ "call(a)" }, { 1, 5 })) 410 | end) 411 | 412 | it("expands multiple positional args", function() 413 | assert.are.same({ 414 | "call(", 415 | " a,", 416 | " b", 417 | ")", 418 | }, Helper:call({ "call(a, b)" }, { 1, 5 })) 419 | end) 420 | 421 | it("expands one keyword arg with explicit value", function() 422 | assert.are.same({ 423 | "call(", 424 | [[ arg: "something"]], 425 | ")", 426 | }, Helper:call({ [[call(arg: "something")]] }, { 1, 5 })) 427 | end) 428 | 429 | it("expands keyword arg with implicit value", function() 430 | assert.are.same({ 431 | "call(", 432 | " arg:", 433 | ")", 434 | }, Helper:call({ [[call(arg:)]] }, { 1, 5 })) 435 | end) 436 | 437 | it("expands with passed block", function() 438 | assert.are.same({ 439 | "call(", 440 | " &block", 441 | ")", 442 | }, Helper:call({ [[call(&block)]] }, { 1, 5 })) 443 | end) 444 | 445 | it("expands with provided block", function() 446 | assert.are.same({ 447 | "call(", 448 | " arg", 449 | ") { |n| puts n }", 450 | }, Helper:call({ [[call(arg) { |n| puts n }]] }, { 1, 5 })) 451 | end) 452 | 453 | it("expands keyword arg with expression value", function() 454 | assert.are.same( 455 | { 456 | "call(", 457 | " arg: count? ? 1 : 2", 458 | ")", 459 | }, 460 | Helper:call({ 461 | [[call(arg: count? ? 1 : 2)]], 462 | }, { 1, 5 }) 463 | ) 464 | end) 465 | 466 | it("expands positional arg with expression value", function() 467 | assert.are.same( 468 | { 469 | "call(", 470 | " count? ? 1 : 2", 471 | ")", 472 | }, 473 | Helper:call({ 474 | [[call(count? ? 1 : 2)]], 475 | }, { 1, 5 }) 476 | ) 477 | end) 478 | 479 | it("expands with a mix of all", function() 480 | assert.are.same({ 481 | "call(", 482 | " a,", 483 | " b,", 484 | " arg: true,", 485 | " arg2:,", 486 | " &blk", 487 | ")", 488 | }, Helper:call({ [[call(a, b, arg: true, arg2:, &blk)]] }, { 1, 5 })) 489 | end) 490 | 491 | it("collapses one positional arg", function() 492 | assert.are.same( 493 | { "call(a)" }, 494 | Helper:call({ 495 | "call(", 496 | " a", 497 | ")", 498 | }, { 1, 5 }) 499 | ) 500 | end) 501 | 502 | it("collapses multiple positional args", function() 503 | assert.are.same( 504 | { "call(a, b)" }, 505 | Helper:call({ 506 | "call(", 507 | " a,", 508 | " b", 509 | ")", 510 | }, { 1, 5 }) 511 | ) 512 | end) 513 | 514 | it("collapses one keyword arg with explicit value", function() 515 | assert.are.same( 516 | { [[call(arg: "something")]] }, 517 | Helper:call({ 518 | "call(", 519 | [[ arg: "something"]], 520 | ")", 521 | }, { 1, 5 }) 522 | ) 523 | end) 524 | 525 | it("collapses keyword arg with implicit value", function() 526 | assert.are.same( 527 | { [[call(arg:)]] }, 528 | Helper:call({ 529 | "call(", 530 | " arg:", 531 | ")", 532 | }, { 1, 5 }) 533 | ) 534 | end) 535 | 536 | it("collapses keyword arg with expression value", function() 537 | assert.are.same( 538 | { [[call(arg: count? ? 1 : 2)]] }, 539 | Helper:call({ 540 | "call(", 541 | " arg: count? ? 1 : 2", 542 | ")", 543 | }, { 1, 5 }) 544 | ) 545 | end) 546 | 547 | it("collapses with passed block", function() 548 | assert.are.same( 549 | { [[call(&block)]] }, 550 | Helper:call({ 551 | "call(", 552 | " &block", 553 | ")", 554 | }, { 1, 5 }) 555 | ) 556 | end) 557 | 558 | it("collapses with provided block", function() 559 | assert.are.same( 560 | { [[call(arg) { |n| puts n }]] }, 561 | Helper:call({ 562 | "call(", 563 | " arg", 564 | ") { |n| puts n }", 565 | }, { 1, 5 }) 566 | ) 567 | end) 568 | 569 | it("collapses with a mix of all", function() 570 | assert.are.same( 571 | { [[call(a, b, arg: true, arg2:, &blk)]] }, 572 | Helper:call({ 573 | "call(", 574 | " a,", 575 | " b,", 576 | " arg: true,", 577 | " arg2:,", 578 | " &blk", 579 | ")", 580 | }, { 1, 5 }) 581 | ) 582 | end) 583 | end) 584 | 585 | describe("method_parameters", function() 586 | it("expands positional argument", function() 587 | assert.are.same( 588 | { 589 | "def something(", 590 | " a,", 591 | " b,", 592 | " c", 593 | ")", 594 | " puts a + b + c", 595 | "end", 596 | }, 597 | Helper:call({ 598 | "def something(a, b, c)", 599 | " puts a + b + c", 600 | "end", 601 | }, { 1, 14 }) 602 | ) 603 | end) 604 | 605 | it("expands keyword arg", function() 606 | assert.are.same( 607 | { 608 | "def something(", 609 | " a:,", 610 | " b:,", 611 | " c:", 612 | ")", 613 | " puts a + b + c", 614 | "end", 615 | }, 616 | Helper:call({ 617 | "def something(a:, b:, c:)", 618 | " puts a + b + c", 619 | "end", 620 | }, { 1, 14 }) 621 | ) 622 | end) 623 | 624 | it("expands block", function() 625 | assert.are.same( 626 | { 627 | "def something(", 628 | " &block", 629 | ")", 630 | " puts a + b + c", 631 | "end", 632 | }, 633 | Helper:call({ 634 | "def something(&block)", 635 | " puts a + b + c", 636 | "end", 637 | }, { 1, 14 }) 638 | ) 639 | end) 640 | 641 | it("expands keyword arg with default value", function() 642 | assert.are.same( 643 | { 644 | "def something(", 645 | " a: 1,", 646 | " b: 2", 647 | ")", 648 | " puts a + b + c", 649 | "end", 650 | }, 651 | Helper:call({ 652 | "def something(a: 1, b: 2)", 653 | " puts a + b + c", 654 | "end", 655 | }, { 1, 14 }) 656 | ) 657 | end) 658 | 659 | it("expands positional arg with default value", function() 660 | assert.are.same( 661 | { 662 | "def something(", 663 | " a = 1,", 664 | " b = 2", 665 | ")", 666 | " puts a + b + c", 667 | "end", 668 | }, 669 | Helper:call({ 670 | "def something(a = 1, b = 2)", 671 | " puts a + b + c", 672 | "end", 673 | }, { 1, 14 }) 674 | ) 675 | end) 676 | 677 | it("collapses positional argument", function() 678 | assert.are.same( 679 | { 680 | "def something(a, b, c)", 681 | " puts a + b + c", 682 | "end", 683 | }, 684 | Helper:call({ 685 | "def something(", 686 | " a,", 687 | " b,", 688 | " c", 689 | ")", 690 | " puts a + b + c", 691 | "end", 692 | }, { 1, 14 }) 693 | ) 694 | end) 695 | 696 | it("collapses keyword arg", function() 697 | assert.are.same( 698 | { 699 | "def something(a:, b:, c:)", 700 | " puts a + b + c", 701 | "end", 702 | }, 703 | Helper:call({ 704 | "def something(", 705 | " a:,", 706 | " b:,", 707 | " c:", 708 | ")", 709 | " puts a + b + c", 710 | "end", 711 | }, { 1, 14 }) 712 | ) 713 | end) 714 | 715 | it("collapses block", function() 716 | assert.are.same( 717 | { 718 | "def something(&block)", 719 | " puts a + b + c", 720 | "end", 721 | }, 722 | Helper:call({ 723 | "def something(", 724 | " &block", 725 | ")", 726 | " puts a + b + c", 727 | "end", 728 | }, { 1, 14 }) 729 | ) 730 | end) 731 | 732 | it("collapses keyword arg with default value", function() 733 | assert.are.same( 734 | { 735 | "def something(a: 1, b: 2)", 736 | " puts a + b + c", 737 | "end", 738 | }, 739 | Helper:call({ 740 | "def something(", 741 | " a: 1,", 742 | " b: 2", 743 | ")", 744 | " puts a + b + c", 745 | "end", 746 | }, { 1, 14 }) 747 | ) 748 | end) 749 | 750 | it("collapses positional arg with default value", function() 751 | assert.are.same( 752 | { 753 | "def something(a = 1, b = 2)", 754 | " puts a + b + c", 755 | "end", 756 | }, 757 | Helper:call({ 758 | "def something(", 759 | " a = 1,", 760 | " b = 2", 761 | ")", 762 | " puts a + b + c", 763 | "end", 764 | }, { 1, 14 }) 765 | ) 766 | end) 767 | end) 768 | 769 | describe("constant", function() 770 | it("transforms pascal case to screaming snake case (multi-word)", function() 771 | assert.are.same({ "TS_NODE_ACTION" }, Helper:call({ "TsNodeAction" })) 772 | end) 773 | 774 | it("transforms screaming snake case to snake case (multi-word)", function() 775 | assert.are.same({ "ts_node_action" }, Helper:call({ "TS_NODE_ACTION" })) 776 | end) 777 | 778 | it("transforms pascal case to screaming snake case (single-word)", function() 779 | assert.are.same({ "NODE" }, Helper:call({ "Node" })) 780 | end) 781 | 782 | it("transforms screaming snake case to snake case (single-word)", function() 783 | assert.are.same({ "node" }, Helper:call({ "NODE" })) 784 | end) 785 | end) 786 | 787 | describe("identifier", function() 788 | it("transforms snake case to pascal case (multi-word)", function() 789 | assert.are.same({ "TsNodeAction" }, Helper:call({ "ts_node_action" })) 790 | end) 791 | 792 | it("transforms snake case to pascal case (single-word)", function() 793 | assert.are.same({ "Node" }, Helper:call({ "node" })) 794 | end) 795 | end) 796 | -------------------------------------------------------------------------------- /spec/filetypes/sql_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("sql", { shiftwidth = 2 }) 4 | 5 | describe("boolean", function() 6 | it("turns 'true' into 'false'", function() 7 | assert.are.same( 8 | { "select true" }, 9 | Helper:call({ "select false" }, { 1, 8 }) 10 | ) 11 | end) 12 | 13 | it("turns 'false' into 'true'", function() 14 | assert.are.same( 15 | { "select false" }, 16 | Helper:call({ "select true" }, { 1, 8 }) 17 | ) 18 | end) 19 | 20 | it("turns 'TRUE' into 'FALSE'", function() 21 | assert.are.same( 22 | { "select TRUE" }, 23 | Helper:call({ "select FALSE" }, { 1, 8 }) 24 | ) 25 | end) 26 | 27 | it("turns 'FALSE' into 'true'", function() 28 | assert.are.same( 29 | { "select FALSE" }, 30 | Helper:call({ "select TRUE" }, { 1, 8 }) 31 | ) 32 | end) 33 | end) 34 | 35 | describe("operators", function() 36 | it("turns 'and' into 'or'", function() 37 | assert.are.same( 38 | { "select a from b where a < 5 and a > 1" }, 39 | Helper:call({ "select a from b where a < 5 or a > 1" }, { 1, 29 }) 40 | ) 41 | end) 42 | 43 | it("turns '=' into '!='", function() 44 | assert.are.same( 45 | { "select a from b where a = 1" }, 46 | Helper:call({ "select a from b where a != 1" }, { 1, 25 }) 47 | ) 48 | end) 49 | 50 | it("turns '!=' into '='", function() 51 | assert.are.same( 52 | { "select a from b where a != 1" }, 53 | Helper:call({ "select a from b where a = 1" }, { 1, 25 }) 54 | ) 55 | end) 56 | 57 | it("turns '<' into '>'", function() 58 | assert.are.same( 59 | { "select a from b where a < 1" }, 60 | Helper:call({ "select a from b where a > 1" }, { 1, 25 }) 61 | ) 62 | end) 63 | 64 | it("turns '>' into '<'", function() 65 | assert.are.same( 66 | { "select a from b where a > 1" }, 67 | Helper:call({ "select a from b where a < 1" }, { 1, 25 }) 68 | ) 69 | end) 70 | 71 | it("turns '<=' into '>='", function() 72 | assert.are.same( 73 | { "select a from b where a <= 1" }, 74 | Helper:call({ "select a from b where a >= 1" }, { 1, 25 }) 75 | ) 76 | end) 77 | 78 | it("turns '>=' into '<='", function() 79 | assert.are.same( 80 | { "select a from b where a >= 1" }, 81 | Helper:call({ "select a from b where a <= 1" }, { 1, 25 }) 82 | ) 83 | end) 84 | 85 | it("turns '+' into '-'", function() 86 | assert.are.same( 87 | { "select a + 1" }, 88 | Helper:call({ "select a - 1" }, { 1, 10 }) 89 | ) 90 | end) 91 | 92 | it("turns '-' into '+'", function() 93 | assert.are.same( 94 | { "select a - 1" }, 95 | Helper:call({ "select a + 1" }, { 1, 10 }) 96 | ) 97 | end) 98 | 99 | it("turns '*' into '/'", function() 100 | assert.are.same( 101 | { "select a * 1" }, 102 | Helper:call({ "select a / 1" }, { 1, 10 }) 103 | ) 104 | end) 105 | 106 | it("turns '/' into '*'", function() 107 | assert.are.same( 108 | { "select a / 1" }, 109 | Helper:call({ "select a * 1" }, { 1, 10 }) 110 | ) 111 | end) 112 | end) 113 | 114 | describe("expands and collapses: ", function() 115 | it("Expands select_expression", function() 116 | assert.are.same( 117 | { "select a as c1, b as c2" }, 118 | Helper:call({ 119 | "select a as c1,", 120 | "b as c2", 121 | }, { 1, 15 }) 122 | ) 123 | end) 124 | 125 | it("Collapses select_expression", function() 126 | got = Helper:call({ "select a as c1, b as c2" }, { 1, 15 }) 127 | got[2] = got[2]:match("^%s*(.*)") 128 | assert.are.same({ 129 | "select a as c1,", 130 | "b as c2", 131 | }, got) 132 | end) 133 | 134 | it("Expands select_expression with subquery", function() 135 | assert.are.same( 136 | { "select a as c1, (select 1) as sq" }, 137 | Helper:call({ 138 | "select a as c1,", 139 | "(select 1) as sq", 140 | }, { 1, 15 }) 141 | ) 142 | end) 143 | 144 | it("Collapses select_expression", function() 145 | got = Helper:call({ "select a as c1, (select 1) as sq" }, { 1, 15 }) 146 | got[2] = got[2]:match("^%s*(.*)") 147 | assert.are.same({ 148 | "select a as c1,", 149 | "(select 1) as sq", 150 | }, got) 151 | end) 152 | 153 | it("Expands column_definition in create table statement", function() 154 | assert.are.same( 155 | { "create table tab (a int, b float)" }, 156 | Helper:call({ 157 | "create table tab (", 158 | "a int,", 159 | "b float", 160 | ")", 161 | }, { 1, 18 }) 162 | ) 163 | end) 164 | 165 | it("Collapses column_definition in create table statement", function() 166 | assert.are.same({ 167 | "create table tab (", 168 | " a int,", 169 | " b float", 170 | ")", 171 | }, Helper:call({ "create table tab (a int, b float)" }, { 1, 18 })) 172 | end) 173 | end) 174 | -------------------------------------------------------------------------------- /spec/filetypes/yaml_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | local Helper = SpecHelper.new("yaml", { shiftwidth = 2 }) 4 | 5 | describe("boolean", function() 6 | it("turns 'true' into 'false'", function() 7 | assert.are.same({ "key: false" }, Helper:call({ "key: true" }, { 1, 6 })) 8 | end) 9 | 10 | it("turns 'false' into 'true'", function() 11 | assert.are.same({ "key: true" }, Helper:call({ "key: false" }, { 1, 6 })) 12 | end) 13 | end) 14 | -------------------------------------------------------------------------------- /spec/init.lua: -------------------------------------------------------------------------------- 1 | local function ensure_installed(repo) 2 | local name = repo:match(".+/(.+)$") 3 | local install_path = "spec/support/" .. name 4 | 5 | vim.opt.rtp:prepend(install_path) 6 | 7 | if not vim.loop.fs_stat(install_path) then 8 | print("* Downloading " .. name .. " to '" .. install_path .. "/'") 9 | vim.fn.system({ 10 | "git", 11 | "clone", 12 | "git@github.com:" .. repo .. ".git", 13 | install_path, 14 | }) 15 | end 16 | end 17 | 18 | if os.getenv("CI") then 19 | vim.opt.runtimepath:prepend(".") 20 | vim.cmd([[runtime! plugin/plenary.vim]]) 21 | vim.cmd([[runtime! plugin/nvim-treesitter.lua]]) 22 | require("nvim-treesitter.configs").setup({ indent = { enable = true } }) 23 | else 24 | ensure_installed("nvim-lua/plenary.nvim") 25 | ensure_installed("nvim-treesitter/nvim-treesitter") 26 | require("plenary.test_harness").test_directory("spec/filetypes") 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | local Buffer = {} 2 | 3 | --- @class Buffer 4 | --- @field lang string The language for this buffer 5 | --- @field opts table Buffer local settings 6 | --- @field setup self 7 | --- @field teardown nil 8 | 9 | --- @param lang string 10 | --- @param buf_opts table 11 | --- @return Buffer 12 | function Buffer.new(lang, buf_opts) 13 | local instance = { 14 | lang = lang, 15 | opts = vim.tbl_extend("keep", { 16 | filetype = lang, 17 | indentexpr = "nvim_treesitter#indent()", 18 | }, buf_opts or {}), 19 | } 20 | 21 | setmetatable(instance, { __index = Buffer }) 22 | 23 | return instance 24 | end 25 | 26 | --- @return self 27 | function Buffer:setup() 28 | self.handle = vim.api.nvim_create_buf(false, true) 29 | vim.treesitter.start(self.handle, self.lang) 30 | 31 | for key, value in pairs(self.opts) do 32 | vim.api.nvim_buf_set_option(self.handle, key, value) 33 | end 34 | 35 | return self 36 | end 37 | 38 | -- Fakes cursor location by just returning the node at where the cursor should be 39 | --- @param pos table 1-indexed { row, col } 40 | --- @return self 41 | function Buffer:set_cursor(pos) 42 | local row = pos[1] - 1 43 | local col = pos[2] - 1 44 | local fake_get_node = function() 45 | local node = vim.treesitter 46 | .get_parser(self.handle, self.lang) 47 | :parse()[1] 48 | :root() 49 | :named_descendant_for_range(row, col, row, col) 50 | return node, self.lang 51 | end 52 | 53 | require("ts-node-action")._get_node = fake_get_node 54 | 55 | return self 56 | end 57 | 58 | --- @param text string|table 59 | --- @return self 60 | function Buffer:write(text) 61 | if type(text) ~= "table" then 62 | text = { text } 63 | end 64 | 65 | vim.api.nvim_buf_set_lines(self.handle, 0, -1, false, text) 66 | return self 67 | end 68 | 69 | --- @return table 70 | function Buffer:read() 71 | return vim.api.nvim_buf_get_lines(self.handle, 0, -1, false) 72 | end 73 | 74 | --- @return nil 75 | function Buffer:teardown() 76 | vim.api.nvim_buf_delete(self.handle, { force = true }) 77 | end 78 | 79 | --- @return self 80 | function Buffer:run_action() 81 | vim.api.nvim_buf_call(self.handle, require("ts-node-action").node_action) 82 | return self 83 | end 84 | 85 | local SpecHelper = {} 86 | 87 | _G.SpecHelper = SpecHelper 88 | 89 | --- @class SpecHelper A general wrapper, available in the global scope, for test related helpers 90 | --- @field lang string The language for this buffer 91 | --- @field buf_opts table Buffer local settings 92 | --- @field call table 93 | 94 | --- @param lang string 95 | --- @param buf_opts table|nil 96 | --- @return SpecHelper 97 | function SpecHelper.new(lang, buf_opts) 98 | local instance = { 99 | lang = lang, 100 | buf_opts = buf_opts or {}, 101 | } 102 | 103 | setmetatable(instance, { __index = SpecHelper }) 104 | 105 | return instance 106 | end 107 | 108 | -- Runs full integration test for text 109 | -- Returns full buffer text as a table, one string per line. 110 | -- 111 | --- @param text string|table 112 | --- @param pos table|nil 1-indexed, { row, col }. Defaults to first line, first col if empty 113 | --- @return table 114 | function SpecHelper:call(text, pos) 115 | local buffer = Buffer.new(self.lang, self.buf_opts) 116 | local result = 117 | buffer:setup():set_cursor(pos or { 1, 1 }):write(text):run_action():read() 118 | 119 | buffer:teardown() 120 | 121 | return result 122 | end 123 | --------------------------------------------------------------------------------