├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── change-to-existing-textobj.yml │ ├── config.yml │ └── new-textobj.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── markdownlint.yml │ ├── nvim-type-check.yml │ ├── panvimdoc.yml │ ├── pr-title.yml │ ├── stale-bot.yml │ └── stylua.yml ├── .gitignore ├── .luarc.json ├── .markdownlint.yaml ├── .stylua.toml ├── .typos.toml ├── Justfile ├── LICENSE ├── README.md ├── doc └── nvim-various-textobjs.txt └── lua └── various-textobjs ├── charwise-core.lua ├── config ├── config.lua └── default-keymaps.lua ├── init.lua ├── textobjs ├── blockwise.lua ├── charwise.lua ├── diagnostic.lua ├── emoji.lua ├── linewise.lua └── subword.lua └── utils.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | max_line_length = 100 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 3 10 | tab_width = 3 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml,yaml,scm}] 14 | indent_style = space 15 | indent_size = 2 16 | tab_width = 2 17 | 18 | [*.py] 19 | indent_style = space 20 | indent_size = 4 21 | tab_width = 4 22 | 23 | [*.md] 24 | indent_size = 4 25 | tab_width = 4 26 | trim_trailing_whitespace = false 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/displaying-a-sponsor-button-in-your-repository 2 | 3 | custom: https://www.paypal.me/ChrisGrieser 4 | ko_fi: pseudometa 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Make sure you have done the following 10 | options: 11 | - label: I have updated to the latest version of the plugin. 12 | required: true 13 | - label: I have read the README. 14 | required: true 15 | - label: I have searched the existing issues for this plugin. 16 | required: true 17 | - type: textarea 18 | id: bug-description 19 | attributes: 20 | label: Bug Description 21 | description: A clear and concise description of the bug. 22 | validations: { required: true } 23 | - type: textarea 24 | id: reproduction-steps 25 | attributes: 26 | label: Reproduction & sample text 27 | description: > 28 | Include sample text and mark clearly how the text looks before and after the operation and 29 | what you expected the text to look like. Include the cursor positions. 30 | placeholder: | 31 | before: 32 | after: 33 | expected: 34 | *include cursor positions* 35 | validations: { required: true } 36 | - type: textarea 37 | id: version-info 38 | attributes: 39 | label: neovim version 40 | render: Text 41 | validations: { required: true } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/change-to-existing-textobj.yml: -------------------------------------------------------------------------------- 1 | name: Text Object Change 2 | description: Suggest a change to an existing text object 3 | title: "Change Textobj: " 4 | labels: ["change to existing textobj"] 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: 12 | The change would be useful to more users than just me. ([You can use the API to create 13 | custom text objects](../#advanced-usage--api).) 14 | required: true 15 | - label: 16 | This is a feature request to change an existing text object, not a bug report. (Use the 17 | bug report form for those.) 18 | required: true 19 | - label: I have searched the existing issues for this plugin. 20 | required: true 21 | - type: textarea 22 | id: textobj-change-requested 23 | attributes: 24 | label: Change requested 25 | description: Describe which text object should change in what way. 26 | validations: { required: true } 27 | - type: textarea 28 | id: sample-text 29 | attributes: 30 | label: Sample Text 31 | description: 32 | Provide sample text for the text object, including as many variations as necessary. If 33 | relevant, also provide examples of what should *not* be matched. 34 | validations: { required: true } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-textobj.yml: -------------------------------------------------------------------------------- 1 | name: New Text Object 2 | description: Suggest a new text object 3 | title: "New textobj: " 4 | labels: ["new textobj"] 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: 12 | The text object would be useful to more users than just me. ([You can use the API to 13 | create custom text objects](../#advanced-usage--api).) 14 | required: true 15 | - label: I read the documentation and checked that there is no such text object already. 16 | required: true 17 | - label: I have searched the existing issues for this plugin. 18 | required: true 19 | - type: textarea 20 | id: textobj-requested 21 | attributes: 22 | label: Text object requested 23 | description: Describe what the text object is supposed to do. 24 | validations: { required: true } 25 | - type: textarea 26 | id: sample-text 27 | attributes: 28 | label: Sample text 29 | description: 30 | Provide sample text for the text object, including as many variations as necessary. If 31 | relevant, also provide examples of what should *not* be matched. 32 | validations: { required: true } 33 | - type: textarea 34 | id: inner-outer 35 | attributes: 36 | label: Inner/outer difference 37 | description: 38 | If the text object would differentiate between inner and outer, describe the difference. 39 | - type: input 40 | id: existing-plugin 41 | attributes: 42 | label: Vimscript plugin already implementing the text object 43 | description: 44 | If there is a plugin that already implements the text object, please link to it here. 45 | - type: input 46 | id: filetypes 47 | attributes: 48 | label: Filetypes 49 | description: 50 | Filetypes the text object is usually going to be used in. Fill in "all" if the text object is 51 | agnostic to the filetype. 52 | validations: { required: true } 53 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "chore(dependabot): " 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What problem does this PR solve? 2 | 3 | ## How does the PR solve it? 4 | 5 | ## Checklist 6 | - [ ] Used only `camelCase` variable names. 7 | - [ ] If functionality is added or modified, also made respective changes to the 8 | `README.md` (the `.txt` file is auto-generated and does not need to be 9 | modified). 10 | -------------------------------------------------------------------------------- /.github/workflows/markdownlint.yml: -------------------------------------------------------------------------------- 1 | name: Markdownlint check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "**.md" 8 | - ".github/workflows/markdownlint.yml" 9 | - ".markdownlint.*" # markdownlint config files 10 | pull_request: 11 | paths: 12 | - "**.md" 13 | 14 | jobs: 15 | markdownlint: 16 | name: Markdownlint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: DavidAnson/markdownlint-cli2-action@v20 21 | with: 22 | globs: "**/*.md" 23 | -------------------------------------------------------------------------------- /.github/workflows/nvim-type-check.yml: -------------------------------------------------------------------------------- 1 | name: nvim type check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | build: 12 | name: nvim type check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: stevearc/nvim-typecheck-action@v2 17 | -------------------------------------------------------------------------------- /.github/workflows/panvimdoc.yml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - README.md 8 | - .github/workflows/panvimdoc.yml 9 | workflow_dispatch: {} # allows manual execution 10 | 11 | permissions: 12 | contents: write 13 | 14 | #─────────────────────────────────────────────────────────────────────────────── 15 | 16 | jobs: 17 | docs: 18 | runs-on: ubuntu-latest 19 | name: README.md to vimdoc 20 | steps: 21 | - uses: actions/checkout@v4 22 | - run: git pull # fix failure when multiple commits are pushed in succession 23 | - run: mkdir -p doc 24 | 25 | - name: panvimdoc 26 | uses: kdheepak/panvimdoc@main 27 | with: 28 | vimdoc: ${{ github.event.repository.name }} 29 | version: "Neovim" 30 | demojify: true 31 | treesitter: true 32 | 33 | - run: git pull 34 | - name: push changes 35 | uses: stefanzweifel/git-auto-commit-action@v5 36 | with: 37 | commit_message: "chore: auto-generate vimdocs" 38 | branch: ${{ github.head_ref }} 39 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | - ready_for_review 11 | 12 | permissions: 13 | pull-requests: read 14 | 15 | jobs: 16 | semantic-pull-request: 17 | name: Check PR title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | requireScope: false 25 | subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital 26 | types: | # add `improv` to the list of allowed types 27 | improv 28 | fix 29 | feat 30 | refactor 31 | build 32 | ci 33 | style 34 | test 35 | chore 36 | perf 37 | docs 38 | break 39 | revert 40 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: Stale bot 2 | on: 3 | schedule: 4 | - cron: "18 04 * * 3" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Close stale issues 15 | uses: actions/stale@v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | # DOCS https://github.com/actions/stale#all-options 20 | days-before-stale: 180 21 | days-before-close: 7 22 | stale-issue-label: "Stale" 23 | stale-issue-message: | 24 | This issue has been automatically marked as stale. 25 | **If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open. 26 | close-issue-message: | 27 | This issue has been closed due to inactivity, and will not be monitored. 28 | -------------------------------------------------------------------------------- /.github/workflows/stylua.yml: -------------------------------------------------------------------------------- 1 | name: Stylua check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | stylua: 12 | name: Stylua 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: JohnnyMorganz/stylua-action@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | version: latest 20 | args: --check . 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # help-tags auto-generated by lazy.nvim 2 | doc/tags 3 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime.version": "LuaJIT", 3 | "diagnostics.globals": ["vim"] 4 | } 5 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Defaults https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 2 | # DOCS https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md 3 | #─────────────────────────────────────────────────────────────────────────────── 4 | 5 | # MODIFIED SETTINGS 6 | blanks-around-headings: 7 | lines_below: 0 # space waster 8 | ul-style: { style: sublist } 9 | 10 | # not autofixable 11 | ol-prefix: { style: ordered } 12 | line-length: 13 | tables: false 14 | code_blocks: false 15 | no-inline-html: 16 | allowed_elements: [img, details, summary, kbd, a, br] 17 | 18 | #───────────────────────────────────────────────────────────────────────────── 19 | # DISABLED 20 | ul-indent: false # not compatible with using tabs 21 | no-hard-tabs: false # taken care of by editorconfig 22 | blanks-around-lists: false # space waster 23 | first-line-heading: false # e.g., ignore-comments 24 | no-emphasis-as-heading: false # sometimes useful 25 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/JohnnyMorganz/StyLua#options 2 | column_width = 100 3 | indent_type = "Tabs" 4 | indent_width = 3 5 | quote_style = "AutoPreferDouble" 6 | call_parentheses = "NoSingleTable" 7 | collapse_simple_statement = "Always" 8 | 9 | [sort_requires] 10 | enabled = true 11 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | iy = "iy" # keymap 3 | 4 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set quiet := true 2 | 3 | masonPath := "$HOME/.local/share/nvim/mason/bin/" 4 | 5 | #─────────────────────────────────────────────────────────────────────────────── 6 | 7 | stylua: 8 | #!/usr/bin/env zsh 9 | {{ masonPath }}/stylua --check --output-format=summary . && return 0 10 | {{ masonPath }}/stylua . 11 | echo "\nFiles formatted." 12 | 13 | lua_ls_check: 14 | {{ masonPath }}/lua-language-server --check . 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Christopher Grieser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # nvim-various-textobjs 🟪🔷🟡 3 | 4 | 5 | badge 6 | 7 | Bundle of more than 30 new text objects for Neovim. 8 | 9 | ## Table of contents 10 | 11 | 12 | 13 | - [List of text objects](#list-of-text-objects) 14 | - [Installation](#installation) 15 | - [Configuration](#configuration) 16 | * [Options](#options) 17 | * [Use your own keybindings](#use-your-own-keybindings) 18 | - [Advanced usage / API](#advanced-usage--api) 19 | * [Go to next occurrence of a text object](#go-to-next-occurrence-of-a-text-object) 20 | * [Dynamically switch text object settings](#dynamically-switch-text-object-settings) 21 | * [`ii` on unindented line should select entire buffer](#ii-on-unindented-line-should-select-entire-buffer) 22 | * [Smarter `gx` & `gf`](#smarter-gx--gf) 23 | * [Delete surrounding indentation](#delete-surrounding-indentation) 24 | * [Yank surrounding indentation](#yank-surrounding-indentation) 25 | * [Indent last paste](#indent-last-paste) 26 | * [Other ideas?](#other-ideas) 27 | - [Limitations & non-goals](#limitations--non-goals) 28 | - [Other text object plugins](#other-text-object-plugins) 29 | - [Credits](#credits) 30 | 31 | 32 | 33 | ## List of text objects 34 | 35 | 36 | | text object | description | inner / outer | forward-seeking | default keymaps | 37 | | :----------------------- | :-------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :-------------- | :----------------------: | 38 | | `indentation` | surrounding lines with same or higher indentation | [see overview from vim-indent-object](https://github.com/michaeljsmith/vim-indent-object) | \- | `ii`, `ai`, `aI`, (`iI`) | 39 | | `restOfIndentation` | lines downwards with same or higher indentation | \- | \- | `R` | 40 | | `greedyOuterIndentation` | outer indentation, expanded to blank lines; useful to get functions with annotations | outer includes a blank (like `ap`/`ip`) | \- | `ag`/`ig` | 41 | | `subword` | segment of a camelCase, snake_case, and kebab-case words | outer includes one trailing/leading `_` or `-` | \- | `iS`/`aS` | 42 | | `toNextClosingBracket` | from cursor to next closing `]`, `)`, or `}`, can span multiple lines | \- | small | `C` | 43 | | `toNextQuotationMark` | from cursor to next unescaped `"`, `'`, or `` ` ``, can span multiple lines | \- | small | `Q` | 44 | | `anyQuote` | between any unescaped `"`, `'`, or `` ` `` in one line | outer includes the quotation marks | small | `iq`/`aq` | 45 | | `anyBracket` | between any `()`, `[]`, or `{}` in one line | outer includes the brackets | small | `io`/`ao` | 46 | | `restOfParagraph` | like `}`, but linewise | \- | \- | `r` | 47 | | `entireBuffer` | entire buffer as one text object | \- | \- | `gG` | 48 | | `nearEoL` | from cursor position to end of line minus one character | \- | \- | `n` | 49 | | `lineCharacterwise` | current line, but characterwise | outer includes indentation & trailing spaces | small, if on blank | `i_`/`a_` | 50 | | `column` | column down until indent or shorter line; accepts `{count}` for multiple columns | \- | \- | `\|` | 51 | | `value` | value of key-value pair, or right side of assignment, excluding trailing comment (does not work for multi-line assignments) | outer includes trailing `,` or `;` | small | `iv`/`av` | 52 | | `key` | key of key-value pair, or left side of an assignment | outer includes the `=` or `:` | small | `ik`/`ak` | 53 | | `url` | `http` links or any other protocol | \- | big | `L` | 54 | | `number` | numbers, similar to `` | inner: only digits, outer: number including minus sign and decimal *point* | small | `in`/`an` | 55 | | `diagnostic` | nvim diagnostic | \- | ∞ | `!` | 56 | | `closedFold` | closed fold | outer includes one line after the last folded line | big | `iz`/`az` | 57 | | `chainMember` | section of a chain connected with `.` (or `:`) like `foo.bar` or `foo.baz(para)` | outer includes one `.` (or `:`) | small | `im`/`am` | 58 | | `visibleInWindow` | all lines visible in the current window | \- | \- | `gw` | 59 | | `restOfWindow` | from the cursorline to the last line in the window | \- | \- | `gW` | 60 | | `lastChange` | last non-deletion-change, yank, or paste (paste-manipulation plugins may interfere) | \- | \- | `g;` | 61 | | `notebookCell` | cell delimited by [double percent comment][jupytext], such as `# %%` | outer includes the top cell border | \- | `iN`/`aN` | 62 | | `emoji` | single emoji (or Nerdfont glyph) | \- | small | `.` | 63 | | `argument` | comma-separated argument (not as accurate as the treesitter-textobjects, use as fallback) | outer includes the `,` | small | `i,`/`a,` | 64 | | `filepath` | unix-filepath; supports `~` or `$HOME`, but not spaces in the filepath. | inner is only the filename | big | `iF`/`aF` | 65 | | `color` | hex; rgb or hsl in CSS format; ANSI color code | inner includes only the color value | small | `i#`/`a#` | 66 | | `doubleSquareBrackets` | text enclosed by `[[]]` | outer includes the four square brackets | small | `iD`/`aD` | 67 | 68 | [jupytext]: https://jupytext.readthedocs.io/en/latest/formats-scripts.html#the-percent-format 69 | 70 | > [!TIP] 71 | > For some text objects, you can also use `caW` or `cl` if your cursor is 72 | > standing on the object in question. However, these text objects become useful 73 | > when utilizing their forward-seeking behavior: Objects like `cL` (`url`) or `c.` 74 | > (`emoji`) will seek forward to the next occurrence and then change them in one 75 | > go. This saves you the need to navigate to them before you can use `caW` or 76 | > `cl`. 77 | 78 | | filetype-specific text objects | description | inner / outer | forward-seeking | default keymaps | filetypes (for default keymaps) | 79 | | :----------------------------- | :------------------------------------------------------------------------------------------------- | :-----------------------------------------------| :-------------- | :-----------------: | :---------------------------------- | 80 | | `mdLink` | Markdown link like `[title](url)` | inner is only the link title (between the `[]`) | small | `il`/`al` | `markdown` | 81 | | `mdEmphasis` | Markdown text enclosed by `*`, `**`, `_`, `__`, `~~`, or `==` | inner is only the emphasis content | small | `ie`/`ae` | `markdown` | 82 | | `mdFencedCodeBlock` | Markdown fenced code (enclosed by three backticks) | outer includes the enclosing backticks | big | `iC`/`aC` | `markdown` | 83 | | `cssSelector` | class in CSS such as `.my-class` | outer includes trailing comma and space | small | `ic`/`ac` | `css`, `scss` | 84 | | `htmlAttribute` | attribute in HTML/XML like `href="foobar.com"` | inner is only the value inside the quotes | small | `ix`/`ax` | `html`, `xml`, `css`, `scss`, `vue` | 85 | | `shellPipe` | segment until/after a pipe character (`\|`) | outer includes the pipe | small | `iP`/`aP` | `bash`, `zsh`, `fish`, `sh` | 86 | 87 | 88 | 89 | ## Installation 90 | **Variant 1:** Have `nvim-various-textobjs` set up all the keybindings from the 91 | table above for you. 92 | 93 | ```lua 94 | -- lazy.nvim 95 | { 96 | "chrisgrieser/nvim-various-textobjs", 97 | event = "VeryLazy", 98 | opts = { 99 | keymaps = { 100 | useDefaults = true 101 | } 102 | }, 103 | }, 104 | 105 | -- packer 106 | use { 107 | "chrisgrieser/nvim-various-textobjs", 108 | config = function () 109 | require("various-textobjs").setup({ 110 | keymaps = { 111 | useDefaults = true 112 | } 113 | }) 114 | end, 115 | } 116 | ``` 117 | 118 | **Variant 2:** Use your own keybindings. See the 119 | [Configuration](#use-your-own-keybindings) section for information on how to set 120 | your own keymaps. 121 | 122 | ```lua 123 | -- lazy.nvim 124 | { 125 | "chrisgrieser/nvim-various-textobjs", 126 | keys = { 127 | -- ... 128 | }, 129 | }, 130 | 131 | -- packer 132 | use { "chrisgrieser/nvim-various-textobjs" } 133 | ``` 134 | 135 | > [!TIP] 136 | > You can also use the `keymaps.disabledDefaults` config option to disable 137 | > only *some* default keymaps. 138 | 139 | ## Configuration 140 | 141 | ### Options 142 | The `.setup()` call is optional if you do not want to use the default keymaps. 143 | 144 | ```lua 145 | -- default config 146 | require("various-textobjs").setup { 147 | keymaps = { 148 | -- See overview table in README for the defaults. (Note that lazy-loading 149 | -- this plugin, the default keymaps cannot be set up. if you set this to 150 | -- `true`, you thus need to add `lazy = false` to your lazy.nvim config.) 151 | useDefaults = false, 152 | 153 | -- disable only some default keymaps, for example { "ai", "!" } 154 | -- (only relevant when you set `useDefaults = true`) 155 | ---@type string[] 156 | disabledDefaults = {}, 157 | }, 158 | 159 | forwardLooking = { 160 | -- Number of lines to seek forwards for a text object. See the overview 161 | -- table in the README for which text object uses which value. 162 | small = 5, 163 | big = 15, 164 | }, 165 | behavior = { 166 | -- save position in jumplist when using text objects 167 | jumplist = true, 168 | }, 169 | 170 | -- extra configuration for specific text objects 171 | textobjs = { 172 | indentation = { 173 | -- `false`: only indentation decreases delimit the text object 174 | -- `true`: indentation decreases as well as blank lines serve as delimiter 175 | blanksAreDelimiter = false, 176 | }, 177 | subword = { 178 | -- When deleting the start of a camelCased word, the result should 179 | -- still be camelCased and not PascalCased (see #113). 180 | noCamelToPascalCase = true, 181 | }, 182 | diagnostic = { 183 | wrap = true, 184 | }, 185 | url = { 186 | patterns = { 187 | [[%l%l%l+://[^%s)%]}"'`>]+]], 188 | }, 189 | }, 190 | }, 191 | 192 | notify = { 193 | icon = "󰠱", -- only used with notification plugins like `nvim-notify` 194 | whenObjectNotFound = true, 195 | }, 196 | 197 | -- show debugging messages on use of certain text objects 198 | debug = false, 199 | } 200 | ``` 201 | 202 | ### Use your own keybindings 203 | If you want to set your own keybindings, you can do so by calling the respective 204 | functions. The function names correspond to the text object names from the 205 | [overview table](#list-of-text-objects). 206 | 207 | > [!NOTE] 208 | > For dot-repeat to work, you have to call the motions as Ex-commands. Using 209 | > `function() require("various-textobjs").diagnostic() end` as third argument of 210 | > the keymap will not work. 211 | 212 | ```lua 213 | -- example: `U` for url textobj 214 | vim.keymap.set({ "o", "x" }, "U", 'lua require("various-textobjs").url()') 215 | 216 | -- example: `as` for outer subword, `is` for inner subword 217 | vim.keymap.set({ "o", "x" }, "as", 'lua require("various-textobjs").subword("outer")') 218 | vim.keymap.set({ "o", "x" }, "is", 'lua require("various-textobjs").subword("inner")') 219 | ``` 220 | 221 | For most text objects, there is only one parameter which accepts `"inner"` or 222 | `"outer"`. The exceptions are the `indentation` and `column` text objects: 223 | 224 | ```lua 225 | -- THE INDENTATION TEXTOBJ requires two parameters, the first for exclusion of 226 | -- the starting border, the second for the exclusion of ending border 227 | vim.keymap.set( 228 | { "o", "x" }, 229 | "ii", 230 | 'lua require("various-textobjs").indentation("inner", "inner")' 231 | ) 232 | vim.keymap.set( 233 | { "o", "x" }, 234 | "ai", 235 | 'lua require("various-textobjs").indentation("outer", "inner")' 236 | ) 237 | ``` 238 | 239 | ```lua 240 | -- THE COLUMN TEXTOBJ takes an optional parameter for direction: 241 | -- "down" (default), "up", "both" 242 | vim.keymap.set( 243 | { "o", "x" }, 244 | "|", 245 | 'lua require("various-textobjs").column("down")' 246 | ) 247 | vim.keymap.set( 248 | { "o", "x" }, 249 | "a|", 250 | 'lua require("various-textobjs").column("both")' 251 | ) 252 | ``` 253 | 254 | ## Advanced usage / API 255 | All text objects can also be used as an API to modify their behavior or create 256 | custom commands. Here are some examples: 257 | 258 | ### Go to next occurrence of a text object 259 | When called in normal mode, `nvim-various-textobjs` selects the next occurrence 260 | of the text object. Thus, you can easily create custom motions that go to the 261 | next occurrence of the text object: 262 | 263 | ```lua 264 | local function gotoNextInnerNumber() 265 | require("various-textobjs").number("inner") 266 | local mode = vim.fn.mode() 267 | if mode:find("[Vv]") then -- only switches to visual when textobj found 268 | vim.cmd.normal { mode, bang = true } -- leaves visual mode 269 | end 270 | end, 271 | ``` 272 | 273 | ### Dynamically switch text object settings 274 | Some text objects have specific settings allowing you to configure their 275 | behavior. In case you want two have two keymaps, one for each behavior, you can 276 | use this plugin's `setup` call before calling the respective text object. 277 | 278 | ```lua 279 | -- Example: one keymap for `http` urls only, one for `ftp` urls only 280 | vim.keymap.set({ "o", "x" }, "H", function() 281 | require("various-textobjs").setup { 282 | textobjs = { 283 | url = { 284 | patterns = { [[https?://[^%s)%]}"'`>]+]] }, 285 | }, 286 | }, 287 | } 288 | return "lua require('various-textobjs').url()" 289 | end, { expr = true, desc = "http-url textobj" }) 290 | 291 | vim.keymap.set({ "o", "x" }, "F", function() 292 | require("various-textobjs").setup { 293 | textobjs = { 294 | url = { 295 | patterns = { [[ftp://[^%s)%]}"'`>]+]] }, 296 | }, 297 | }, 298 | } 299 | return "lua require('various-textobjs').url()" 300 | end, { expr = true, desc = "ftp-url textobj" }) 301 | ``` 302 | 303 | ### `ii` on unindented line should select entire buffer 304 | Using a simple if-else-block, you can create a hybrid of the inner indentation 305 | text object and the entire-buffer text object, if you prefer that kind of 306 | behavior: 307 | 308 | ```lua 309 | -- when on unindented line, `ii` should select entire buffer 310 | vim.keymap.set("o", "ii", function() 311 | if vim.fn.indent(".") == 0 then 312 | require("various-textobjs").entireBuffer() 313 | else 314 | require("various-textobjs").indentation("inner", "inner") 315 | end 316 | end) 317 | ``` 318 | 319 | ### Smarter `gx` & `gf` 320 | The code below retrieves the next URL (within the amount of lines configured in 321 | the `setup` call), and opens it in your browser. As opposed to vim's built-in 322 | `gx`, this is **forward-seeking**, meaning your cursor does not have to stand on 323 | the URL. 324 | 325 | ```lua 326 | vim.keymap.set("n", "gx", function() 327 | require("various-textobjs").url() -- select URL 328 | 329 | local foundURL = vim.fn.mode() == "v" -- only switches to visual mode when textobj found 330 | if not foundURL then return end 331 | 332 | local url = vim.fn.getregion(vim.fn.getpos("."), vim.fn.getpos("v"), { type = "v" })[1] 333 | vim.ui.open(url) -- requires nvim 0.10 334 | vim.cmd.normal { "v", bang = true } -- leave visual mode 335 | end, { desc = "URL Opener" }) 336 | ``` 337 | 338 | Similarly, we can also create a forward-looking version of `gf`: 339 | 340 | ```lua 341 | vim.keymap.set("n", "gf", function() 342 | require("various-textobjs").filepath("outer") -- select filepath 343 | 344 | local foundPath = vim.fn.mode() == "v" -- only switches to visual mode when textobj found 345 | if not foundPath then return end 346 | 347 | local path = vim.fn.getregion(vim.fn.getpos("."), vim.fn.getpos("v"), { type = "v" })[1] 348 | 349 | local exists = vim.uv.fs_stat(vim.fs.normalize(path)) ~= nil 350 | if exists then 351 | vim.ui.open(path) 352 | else 353 | vim.notify("Path does not exist.", vim.log.levels.WARN) 354 | end 355 | end, { desc = "URL Opener" }) 356 | ``` 357 | 358 | ### Delete surrounding indentation 359 | Using the indentation text object, you can also create custom indentation-related 360 | utilities. A common operation is to remove the line before and after an 361 | indentation. Take for example this case where you are removing the `foo` 362 | condition: 363 | 364 | ```lua 365 | -- before 366 | if foo then 367 | print("bar") -- <- cursor is on this line 368 | print("baz") 369 | end 370 | 371 | -- after 372 | print("bar") 373 | print("baz") 374 | ``` 375 | 376 | The code below achieves this by dedenting the inner indentation text object 377 | (essentially running `")[1] 397 | local startBorderLn = vim.api.nvim_buf_get_mark(0, "<")[1] 398 | vim.cmd(tostring(endBorderLn) .. " delete") -- delete end first so line index is not shifted 399 | vim.cmd(tostring(startBorderLn) .. " delete") 400 | end, { desc = "Delete Surrounding Indentation" }) 401 | ``` 402 | 403 | ### Yank surrounding indentation 404 | Similarly, you can also create a `ysii` command to yank the two lines surrounding 405 | an indentation text object. (Not using `ysi`, since that blocks surround 406 | commands like `ysi)`). Using `nvim_win_[gs]et_cursor()`, you make the 407 | operation sticky, meaning the cursor is not moved. 408 | 409 | ```lua 410 | -- NOTE this function uses `vim.hl.range` requires nvim 0.11 411 | vim.keymap.set("n", "ysii", function() 412 | local startPos = vim.api.nvim_win_get_cursor(0) 413 | 414 | -- identify start- and end-border 415 | require("various-textobjs").indentation("outer", "outer") 416 | local indentationFound = vim.fn.mode():find("V") 417 | if not indentationFound then return end 418 | vim.cmd.normal { "V", bang = true } -- leave visual mode so the '< '> marks are set 419 | 420 | -- copy them into the + register 421 | local startLn = vim.api.nvim_buf_get_mark(0, "<")[1] - 1 422 | local endLn = vim.api.nvim_buf_get_mark(0, ">")[1] - 1 423 | local startLine = vim.api.nvim_buf_get_lines(0, startLn, startLn + 1, false)[1] 424 | local endLine = vim.api.nvim_buf_get_lines(0, endLn, endLn + 1, false)[1] 425 | vim.fn.setreg("+", startLine .. "\n" .. endLine .. "\n") 426 | 427 | -- highlight yanked text 428 | local dur = 1500 429 | local ns = vim.api.nvim_create_namespace("ysii") 430 | local bufnr = vim.api.nvim_get_current_buf() 431 | vim.hl.range(bufnr, ns, "IncSearch", { startLn, 0 }, { startLn, -1 }, { timeout = dur }) 432 | vim.hl.range(bufnr, ns, "IncSearch", { endLn, 0 }, { endLn, -1 }, { timeout = dur }) 433 | 434 | -- restore cursor position 435 | vim.api.nvim_win_set_cursor(0, startPos) 436 | end, { desc = "Yank surrounding indentation" }) 437 | ``` 438 | 439 | ### Indent last paste 440 | The `lastChange` text object can be used to indent the last text that was pasted. 441 | This is useful in languages such as Python where indentation is meaningful and 442 | thus formatters are not able to automatically indent everything for you. 443 | 444 | If you do not use `P` for upwards paste, "shift paste" serves as a great 445 | mnemonic. 446 | 447 | ```lua 448 | vim.keymap.set("n", "P", function() 449 | require("various-textobjs").lastChange() 450 | local changeFound = vim.fn.mode():find("v") 451 | if changeFound then vim.cmd.normal { ">", bang = true } end 452 | end 453 | ``` 454 | 455 | ### Other ideas? 456 | If you have some other useful ideas, feel free to [share them in this repo's 457 | discussion 458 | page](https://github.com/chrisgrieser/nvim-various-textobjs/discussions). 459 | 460 | ## Limitations & non-goals 461 | - This plugin uses pattern matching, so it can be inaccurate in some edge cases. 462 | - Counts are not supported for most text objects. 463 | - Most characterwise text objects do not match multi-line objects. Most notably, 464 | this affects the `value` text object. 465 | - [nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects) 466 | already does an excellent job when it comes to using Treesitter for text 467 | objects, such as function arguments or loops. This plugin's goal is therefore 468 | not to provide text objects already offered by `nvim-treesitter-textobjects`. 469 | - Some text objects (`argument`, `key`, `value`) are also offered by 470 | `nvim-treesitter-textobjects`, and usually, the treesitter version of them is 471 | more accurate, since `nvim-various-textobjs` uses pattern matching, which can 472 | only get you so far. However, `nvim-treesitter-textobjects` does not support 473 | all objects for all languages, so `nvim-various-textobjs` version exists to 474 | provide a fallback for those languages. 475 | 476 | ## Other text object plugins 477 | - [treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects) 478 | - [treesitter-textsubjects](https://github.com/RRethy/nvim-treesitter-textsubjects) 479 | - [mini.ai](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-ai.md) 480 | 481 | ## Credits 482 | **Thanks** 483 | - To the `Valuable Dev` for [their blog post on how to get started with creating 484 | custom text objects](https://thevaluable.dev/vim-create-text-objects/). 485 | - [To `@vypxl` and `@ii14` for figuring out dot-repeatability.](https://github.com/chrisgrieser/nvim-spider/pull/4) 486 | 487 | In my day job, I am a sociologist studying the social mechanisms underlying the 488 | digital economy. For my PhD project, I investigate the governance of the app 489 | economy and how software ecosystems manage the tension between innovation and 490 | compatibility. If you are interested in this subject, feel free to get in touch. 491 | 492 | - [Website](https://chris-grieser.de/) 493 | - [Mastodon](https://pkm.social/@pseudometa) 494 | - [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser) 495 | - [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/) 496 | 497 | Buy Me a Coffee at ko-fi.com 500 | -------------------------------------------------------------------------------- /doc/nvim-various-textobjs.txt: -------------------------------------------------------------------------------- 1 | *nvim-various-textobjs.txt* For Neovim Last change: 2025 May 22 2 | 3 | ============================================================================== 4 | Table of Contents *nvim-various-textobjs-table-of-contents* 5 | 6 | 1. nvim-various-textobjs |nvim-various-textobjs-nvim-various-textobjs-| 7 | - Table of contents|nvim-various-textobjs-nvim-various-textobjs--table-of-contents| 8 | - List of text objects|nvim-various-textobjs-nvim-various-textobjs--list-of-text-objects| 9 | - Installation |nvim-various-textobjs-nvim-various-textobjs--installation| 10 | - Configuration |nvim-various-textobjs-nvim-various-textobjs--configuration| 11 | - Advanced usage / API|nvim-various-textobjs-nvim-various-textobjs--advanced-usage-/-api| 12 | - Limitations & non-goals|nvim-various-textobjs-nvim-various-textobjs--limitations-&-non-goals| 13 | - Other text object plugins|nvim-various-textobjs-nvim-various-textobjs--other-text-object-plugins| 14 | - Credits |nvim-various-textobjs-nvim-various-textobjs--credits| 15 | 16 | ============================================================================== 17 | 1. nvim-various-textobjs *nvim-various-textobjs-nvim-various-textobjs-* 18 | 19 | 20 | 21 | Bundleof more than 30 new text objects for Neovim. 22 | 23 | 24 | TABLE OF CONTENTS*nvim-various-textobjs-nvim-various-textobjs--table-of-contents* 25 | 26 | - |nvim-various-textobjs-list-of-text-objects| 27 | - |nvim-various-textobjs-installation| 28 | - |nvim-various-textobjs-configuration| 29 | - |nvim-various-textobjs-options| 30 | - |nvim-various-textobjs-use-your-own-keybindings| 31 | - |nvim-various-textobjs-advanced-usage-/-api| 32 | - |nvim-various-textobjs-go-to-next-occurrence-of-a-text-object| 33 | - |nvim-various-textobjs-dynamically-switch-text-object-settings| 34 | - |nvim-various-textobjs-`ii`-on-unindented-line-should-select-entire-buffer| 35 | - |nvim-various-textobjs-smarter-`gx`-&-`gf`| 36 | - |nvim-various-textobjs-delete-surrounding-indentation| 37 | - |nvim-various-textobjs-yank-surrounding-indentation| 38 | - |nvim-various-textobjs-indent-last-paste| 39 | - |nvim-various-textobjs-other-ideas?| 40 | - |nvim-various-textobjs-limitations-&-non-goals| 41 | - |nvim-various-textobjs-other-text-object-plugins| 42 | - |nvim-various-textobjs-credits| 43 | 44 | 45 | LIST OF TEXT OBJECTS*nvim-various-textobjs-nvim-various-textobjs--list-of-text-objects* 46 | 47 | ----------------------------------------------------------------------------------------------------------- 48 | text object description inner / outer forward-seeking default 49 | keymaps 50 | ------------------------ ------------------------------- ---------------------- ----------------- --------- 51 | indentation surrounding lines with same or see overview from - ii, ai, 52 | higher indentation vim-indent-object aI, (iI) 53 | 54 | restOfIndentation lines downwards with same or - - R 55 | higher indentation 56 | 57 | greedyOuterIndentation outer indentation, expanded to outer includes a blank - ag/ig 58 | blank lines; useful to get (like ap/ip) 59 | functions with annotations 60 | 61 | subword segment of a camelCase, outer includes one - iS/aS 62 | snake_case, and kebab-case trailing/leading _ or 63 | words - 64 | 65 | toNextClosingBracket from cursor to next closing ], - small C 66 | ), or }, can span multiple 67 | lines 68 | 69 | toNextQuotationMark from cursor to next unescaped - small Q 70 | ", ', or `, can span multiple 71 | lines 72 | 73 | anyQuote between any unescaped ", ', or outer includes the small iq/aq 74 | ` in one line quotation marks 75 | 76 | anyBracket between any (), [], or {} in outer includes the small io/ao 77 | one line brackets 78 | 79 | restOfParagraph like }, but linewise - - r 80 | 81 | entireBuffer entire buffer as one text - - gG 82 | object 83 | 84 | nearEoL from cursor position to end of - - n 85 | line minus one character 86 | 87 | lineCharacterwise current line, but characterwise outer includes small, if on i_/a_ 88 | indentation & trailing blank 89 | spaces 90 | 91 | column column down until indent or - - \| 92 | shorter line; accepts {count} 93 | for multiple columns 94 | 95 | value value of key-value pair, or outer includes small iv/av 96 | right side of assignment, trailing , or ; 97 | excluding trailing comment 98 | (does not work for multi-line 99 | assignments) 100 | 101 | key key of key-value pair, or left outer includes the = small ik/ak 102 | side of an assignment or : 103 | 104 | url http links or any other - big L 105 | protocol 106 | 107 | number numbers, similar to inner: only digits, small in/an 108 | outer: number 109 | including minus sign 110 | and decimal point 111 | 112 | diagnostic nvim diagnostic - ∞ ! 113 | 114 | closedFold closed fold outer includes one big iz/az 115 | line after the last 116 | folded line 117 | 118 | chainMember section of a chain connected outer includes one . small im/am 119 | with . (or :) like foo.bar or (or :) 120 | foo.baz(para) 121 | 122 | visibleInWindow all lines visible in the - - gw 123 | current window 124 | 125 | restOfWindow from the cursorline to the last - - gW 126 | line in the window 127 | 128 | lastChange last non-deletion-change, yank, - - g; 129 | or paste (paste-manipulation 130 | plugins may interfere) 131 | 132 | notebookCell cell delimited by double outer includes the top - iN/aN 133 | percent comment, such as # %% cell border 134 | 135 | emoji single emoji (or Nerdfont - small . 136 | glyph) 137 | 138 | argument comma-separated argument (not outer includes the , small i,/a, 139 | as accurate as the 140 | treesitter-textobjects, use as 141 | fallback) 142 | 143 | filepath unix-filepath; supports ~ or inner is only the big iF/aF 144 | $HOME, but not spaces in the filename 145 | filepath. 146 | 147 | color hex; rgb or hsl in CSS format; inner includes only small i#/a# 148 | ANSI color code the color value 149 | 150 | doubleSquareBrackets text enclosed by [[]] outer includes the small iD/aD 151 | four square brackets 152 | ----------------------------------------------------------------------------------------------------------- 153 | 154 | [!TIP] For some text objects, you can also use `caW` or `cl` if your cursor is 155 | standing on the object in question. However, these text objects become useful 156 | when utilizing their forward-seeking behavior: Objects like `cL` (`url`) or 157 | `c.` (`emoji`) will seek forward to the next occurrence and then change them in 158 | one go. This saves you the need to navigate to them before you can use `caW` or 159 | `cl`. 160 | ------------------------------------------------------------------------------------------------------ 161 | filetype-specific description inner / outer forward-seeking default filetypes 162 | text objects keymaps (for 163 | default 164 | keymaps) 165 | ------------------- ---------------------------- ------------- ----------------- --------- ----------- 166 | mdLink Markdown link like inner is only small il/al markdown 167 | [title](url) the link 168 | title 169 | (between the 170 | []) 171 | 172 | mdEmphasis Markdown text enclosed by *, inner is only small ie/ae markdown 173 | **, _, __, ~~, or == the emphasis 174 | content 175 | 176 | mdFencedCodeBlock Markdown fenced code outer big iC/aC markdown 177 | (enclosed by three includes the 178 | backticks) enclosing 179 | backticks 180 | 181 | cssSelector class in CSS such as outer small ic/ac css, scss 182 | .my-class includes 183 | trailing 184 | comma and 185 | space 186 | 187 | htmlAttribute attribute in HTML/XML like inner is only small ix/ax html, xml, 188 | href="foobar.com" the value css, scss, 189 | inside the vue 190 | quotes 191 | 192 | shellPipe segment until/after a pipe outer small iP/aP bash, zsh, 193 | character (\|) includes the fish, sh 194 | pipe 195 | ------------------------------------------------------------------------------------------------------ 196 | 197 | INSTALLATION *nvim-various-textobjs-nvim-various-textobjs--installation* 198 | 199 | **Variant 1:** Have `nvim-various-textobjs` set up all the keybindings from the 200 | table above for you. 201 | 202 | >lua 203 | -- lazy.nvim 204 | { 205 | "chrisgrieser/nvim-various-textobjs", 206 | event = "VeryLazy", 207 | opts = { 208 | keymaps = { 209 | useDefaults = true 210 | } 211 | }, 212 | }, 213 | 214 | -- packer 215 | use { 216 | "chrisgrieser/nvim-various-textobjs", 217 | config = function () 218 | require("various-textobjs").setup({ 219 | keymaps = { 220 | useDefaults = true 221 | } 222 | }) 223 | end, 224 | } 225 | < 226 | 227 | **Variant 2:** Use your own keybindings. See the 228 | |nvim-various-textobjs-configuration| section for information on how to set 229 | your own keymaps. 230 | 231 | >lua 232 | -- lazy.nvim 233 | { 234 | "chrisgrieser/nvim-various-textobjs", 235 | keys = { 236 | -- ... 237 | }, 238 | }, 239 | 240 | -- packer 241 | use { "chrisgrieser/nvim-various-textobjs" } 242 | < 243 | 244 | 245 | [!TIP] You can also use the `keymaps.disabledDefaults` config option to disable 246 | only _some_ default keymaps. 247 | 248 | CONFIGURATION *nvim-various-textobjs-nvim-various-textobjs--configuration* 249 | 250 | 251 | OPTIONS ~ 252 | 253 | The `.setup()` call is optional if you do not want to use the default keymaps. 254 | 255 | >lua 256 | -- default config 257 | require("various-textobjs").setup { 258 | keymaps = { 259 | -- See overview table in README for the defaults. (Note that lazy-loading 260 | -- this plugin, the default keymaps cannot be set up. if you set this to 261 | -- `true`, you thus need to add `lazy = false` to your lazy.nvim config.) 262 | useDefaults = false, 263 | 264 | -- disable only some default keymaps, for example { "ai", "!" } 265 | -- (only relevant when you set `useDefaults = true`) 266 | ---@type string[] 267 | disabledDefaults = {}, 268 | }, 269 | 270 | forwardLooking = { 271 | -- Number of lines to seek forwards for a text object. See the overview 272 | -- table in the README for which text object uses which value. 273 | small = 5, 274 | big = 15, 275 | }, 276 | behavior = { 277 | -- save position in jumplist when using text objects 278 | jumplist = true, 279 | }, 280 | 281 | -- extra configuration for specific text objects 282 | textobjs = { 283 | indentation = { 284 | -- `false`: only indentation decreases delimit the text object 285 | -- `true`: indentation decreases as well as blank lines serve as delimiter 286 | blanksAreDelimiter = false, 287 | }, 288 | subword = { 289 | -- When deleting the start of a camelCased word, the result should 290 | -- still be camelCased and not PascalCased (see #113). 291 | noCamelToPascalCase = true, 292 | }, 293 | diagnostic = { 294 | wrap = true, 295 | }, 296 | url = { 297 | patterns = { 298 | [[%l%l%l+://[^%s)%]}"'`>]+]], 299 | }, 300 | }, 301 | }, 302 | 303 | notify = { 304 | icon = "󰠱", -- only used with notification plugins like `nvim-notify` 305 | whenObjectNotFound = true, 306 | }, 307 | 308 | -- show debugging messages on use of certain text objects 309 | debug = false, 310 | } 311 | < 312 | 313 | 314 | USE YOUR OWN KEYBINDINGS ~ 315 | 316 | If you want to set your own keybindings, you can do so by calling the 317 | respective functions. The function names correspond to the text object names 318 | from the |nvim-various-textobjs-overview-table|. 319 | 320 | 321 | [!NOTE] For dot-repeat to work, you have to call the motions as Ex-commands. 322 | Using `function() require("various-textobjs").diagnostic() end` as third 323 | argument of the keymap will not work. 324 | >lua 325 | -- example: `U` for url textobj 326 | vim.keymap.set({ "o", "x" }, "U", 'lua require("various-textobjs").url()') 327 | 328 | -- example: `as` for outer subword, `is` for inner subword 329 | vim.keymap.set({ "o", "x" }, "as", 'lua require("various-textobjs").subword("outer")') 330 | vim.keymap.set({ "o", "x" }, "is", 'lua require("various-textobjs").subword("inner")') 331 | < 332 | 333 | For most text objects, there is only one parameter which accepts `"inner"` or 334 | `"outer"`. The exceptions are the `indentation` and `column` text objects: 335 | 336 | >lua 337 | -- THE INDENTATION TEXTOBJ requires two parameters, the first for exclusion of 338 | -- the starting border, the second for the exclusion of ending border 339 | vim.keymap.set( 340 | { "o", "x" }, 341 | "ii", 342 | 'lua require("various-textobjs").indentation("inner", "inner")' 343 | ) 344 | vim.keymap.set( 345 | { "o", "x" }, 346 | "ai", 347 | 'lua require("various-textobjs").indentation("outer", "inner")' 348 | ) 349 | < 350 | 351 | >lua 352 | -- THE COLUMN TEXTOBJ takes an optional parameter for direction: 353 | -- "down" (default), "up", "both" 354 | vim.keymap.set( 355 | { "o", "x" }, 356 | "|", 357 | 'lua require("various-textobjs").column("down")' 358 | ) 359 | vim.keymap.set( 360 | { "o", "x" }, 361 | "a|", 362 | 'lua require("various-textobjs").column("both")' 363 | ) 364 | < 365 | 366 | 367 | ADVANCED USAGE / API*nvim-various-textobjs-nvim-various-textobjs--advanced-usage-/-api* 368 | 369 | All text objects can also be used as an API to modify their behavior or create 370 | custom commands. Here are some examples: 371 | 372 | 373 | GO TO NEXT OCCURRENCE OF A TEXT OBJECT ~ 374 | 375 | When called in normal mode, `nvim-various-textobjs` selects the next occurrence 376 | of the text object. Thus, you can easily create custom motions that go to the 377 | next occurrence of the text object: 378 | 379 | >lua 380 | local function gotoNextInnerNumber() 381 | require("various-textobjs").number("inner") 382 | local mode = vim.fn.mode() 383 | if mode:find("[Vv]") then -- only switches to visual when textobj found 384 | vim.cmd.normal { mode, bang = true } -- leaves visual mode 385 | end 386 | end, 387 | < 388 | 389 | 390 | DYNAMICALLY SWITCH TEXT OBJECT SETTINGS ~ 391 | 392 | Some text objects have specific settings allowing you to configure their 393 | behavior. In case you want two have two keymaps, one for each behavior, you can 394 | use this plugin’s `setup` call before calling the respective text object. 395 | 396 | >lua 397 | -- Example: one keymap for `http` urls only, one for `ftp` urls only 398 | vim.keymap.set({ "o", "x" }, "H", function() 399 | require("various-textobjs").setup { 400 | textobjs = { 401 | url = { 402 | patterns = { [[https?://[^%s)%]}"'`>]+]] }, 403 | }, 404 | }, 405 | } 406 | return "lua require('various-textobjs').url()" 407 | end, { expr = true, desc = "http-url textobj" }) 408 | 409 | vim.keymap.set({ "o", "x" }, "F", function() 410 | require("various-textobjs").setup { 411 | textobjs = { 412 | url = { 413 | patterns = { [[ftp://[^%s)%]}"'`>]+]] }, 414 | }, 415 | }, 416 | } 417 | return "lua require('various-textobjs').url()" 418 | end, { expr = true, desc = "ftp-url textobj" }) 419 | < 420 | 421 | 422 | II ON UNINDENTED LINE SHOULD SELECT ENTIRE BUFFER ~ 423 | 424 | Using a simple if-else-block, you can create a hybrid of the inner indentation 425 | text object and the entire-buffer text object, if you prefer that kind of 426 | behavior: 427 | 428 | >lua 429 | -- when on unindented line, `ii` should select entire buffer 430 | vim.keymap.set("o", "ii", function() 431 | if vim.fn.indent(".") == 0 then 432 | require("various-textobjs").entireBuffer() 433 | else 434 | require("various-textobjs").indentation("inner", "inner") 435 | end 436 | end) 437 | < 438 | 439 | 440 | SMARTER GX & GF ~ 441 | 442 | The code below retrieves the next URL (within the amount of lines configured in 443 | the `setup` call), and opens it in your browser. As opposed to vim’s built-in 444 | `gx`, this is **forward-seeking**, meaning your cursor does not have to stand 445 | on the URL. 446 | 447 | >lua 448 | vim.keymap.set("n", "gx", function() 449 | require("various-textobjs").url() -- select URL 450 | 451 | local foundURL = vim.fn.mode() == "v" -- only switches to visual mode when textobj found 452 | if not foundURL then return end 453 | 454 | local url = vim.fn.getregion(vim.fn.getpos("."), vim.fn.getpos("v"), { type = "v" })[1] 455 | vim.ui.open(url) -- requires nvim 0.10 456 | vim.cmd.normal { "v", bang = true } -- leave visual mode 457 | end, { desc = "URL Opener" }) 458 | < 459 | 460 | Similarly, we can also create a forward-looking version of `gf` 461 | 462 | >lua 463 | vim.keymap.set("n", "gf", function() 464 | require("various-textobjs").filepath("outer") -- select filepath 465 | 466 | local foundPath = vim.fn.mode() == "v" -- only switches to visual mode when textobj found 467 | if not foundPath then return end 468 | 469 | local path = vim.fn.getregion(vim.fn.getpos("."), vim.fn.getpos("v"), { type = "v" })[1] 470 | 471 | local exists = vim.uv.fs_stat(vim.fs.normalize(path)) ~= nil 472 | if exists then 473 | vim.ui.open(path) 474 | else 475 | vim.notify("Path does not exist.", vim.log.levels.WARN) 476 | end 477 | end, { desc = "URL Opener" }) 478 | < 479 | 480 | 481 | DELETESURROUNDING INDENTATION ~ 482 | 483 | Using the indentation text object, you can also create custom 484 | indentation-related utilities. A common operation is to remove the line before 485 | and after an indentation. Take for example this case where you are removing the 486 | `foo` condition: 487 | 488 | >lua 489 | -- before 490 | if foo then 491 | print("bar") -- <- cursor is on this line 492 | print("baz") 493 | end 494 | 495 | -- after 496 | print("bar") 497 | print("baz") 498 | < 499 | 500 | The code below achieves this by dedenting the inner indentation text object 501 | (essentially running ` but 504 | performed on an indentation text object. (It is also an intuitive mnemonic: 505 | Delete Surrounding Indentation.) 506 | 507 | >lua 508 | vim.keymap.set("n", "dsi", function() 509 | -- select outer indentation 510 | require("various-textobjs").indentation("outer", "outer") 511 | 512 | -- plugin only switches to visual mode when a textobj has been found 513 | local indentationFound = vim.fn.mode():find("V") 514 | if not indentationFound then return end 515 | 516 | -- dedent indentation 517 | vim.cmd.normal { "<", bang = true } 518 | 519 | -- delete surrounding lines 520 | local endBorderLn = vim.api.nvim_buf_get_mark(0, ">")[1] 521 | local startBorderLn = vim.api.nvim_buf_get_mark(0, "<")[1] 522 | vim.cmd(tostring(endBorderLn) .. " delete") -- delete end first so line index is not shifted 523 | vim.cmd(tostring(startBorderLn) .. " delete") 524 | end, { desc = "Delete Surrounding Indentation" }) 525 | < 526 | 527 | 528 | YANK SURROUNDING INDENTATION ~ 529 | 530 | Similarly, you can also create a `ysii` command to yank the two lines 531 | surrounding an indentation text object. (Not using `ysi`, since that blocks 532 | surround commands like `ysi)`). Using `nvim_win_[gs]et_cursor()`, you make the 533 | operation sticky, meaning the cursor is not moved. 534 | 535 | >lua 536 | -- NOTE this function uses `vim.hl.range` requires nvim 0.11 537 | vim.keymap.set("n", "ysii", function() 538 | local startPos = vim.api.nvim_win_get_cursor(0) 539 | 540 | -- identify start- and end-border 541 | require("various-textobjs").indentation("outer", "outer") 542 | local indentationFound = vim.fn.mode():find("V") 543 | if not indentationFound then return end 544 | vim.cmd.normal { "V", bang = true } -- leave visual mode so the '< '> marks are set 545 | 546 | -- copy them into the + register 547 | local startLn = vim.api.nvim_buf_get_mark(0, "<")[1] - 1 548 | local endLn = vim.api.nvim_buf_get_mark(0, ">")[1] - 1 549 | local startLine = vim.api.nvim_buf_get_lines(0, startLn, startLn + 1, false)[1] 550 | local endLine = vim.api.nvim_buf_get_lines(0, endLn, endLn + 1, false)[1] 551 | vim.fn.setreg("+", startLine .. "\n" .. endLine .. "\n") 552 | 553 | -- highlight yanked text 554 | local dur = 1500 555 | local ns = vim.api.nvim_create_namespace("ysii") 556 | local bufnr = vim.api.nvim_get_current_buf() 557 | vim.hl.range(bufnr, ns, "IncSearch", { startLn, 0 }, { startLn, -1 }, { timeout = dur }) 558 | vim.hl.range(bufnr, ns, "IncSearch", { endLn, 0 }, { endLn, -1 }, { timeout = dur }) 559 | 560 | -- restore cursor position 561 | vim.api.nvim_win_set_cursor(0, startPos) 562 | end, { desc = "Yank surrounding indentation" }) 563 | < 564 | 565 | 566 | INDENT LAST PASTE ~ 567 | 568 | The `lastChange` text object can be used to indent the last text that was 569 | pasted. This is useful in languages such as Python where indentation is 570 | meaningful and thus formatters are not able to automatically indent everything 571 | for you. 572 | 573 | If you do not use `P` for upwards paste, "shift paste" serves as a great 574 | mnemonic. 575 | 576 | >lua 577 | vim.keymap.set("n", "P", function() 578 | require("various-textobjs").lastChange() 579 | local changeFound = vim.fn.mode():find("v") 580 | if changeFound then vim.cmd.normal { ">", bang = true } end 581 | end 582 | < 583 | 584 | 585 | OTHER IDEAS? ~ 586 | 587 | If you have some other useful ideas, feel free to share them in this repo’s 588 | discussion page 589 | . 590 | 591 | 592 | LIMITATIONS & NON-GOALS*nvim-various-textobjs-nvim-various-textobjs--limitations-&-non-goals* 593 | 594 | - This plugin uses pattern matching, so it can be inaccurate in some edge cases. 595 | - Counts are not supported for most text objects. 596 | - Most characterwise text objects do not match multi-line objects. Most notably, 597 | this affects the `value` text object. 598 | - nvim-treesitter-textobjects 599 | already does an excellent job when it comes to using Treesitter for text 600 | objects, such as function arguments or loops. This plugin’s goal is therefore 601 | not to provide text objects already offered by `nvim-treesitter-textobjects`. 602 | - Some text objects (`argument`, `key`, `value`) are also offered by 603 | `nvim-treesitter-textobjects`, and usually, the treesitter version of them is 604 | more accurate, since `nvim-various-textobjs` uses pattern matching, which can 605 | only get you so far. However, `nvim-treesitter-textobjects` does not support 606 | all objects for all languages, so `nvim-various-textobjs` version exists to 607 | provide a fallback for those languages. 608 | 609 | 610 | OTHER TEXT OBJECT PLUGINS*nvim-various-textobjs-nvim-various-textobjs--other-text-object-plugins* 611 | 612 | - treesitter-textobjects 613 | - treesitter-textsubjects 614 | - mini.ai 615 | 616 | 617 | CREDITS *nvim-various-textobjs-nvim-various-textobjs--credits* 618 | 619 | **Thanks** - To the `Valuable Dev` for their blog post on how to get started 620 | with creating custom text objects 621 | . - To `@vypxl` and `@ii14` 622 | for figuring out dot-repeatability. 623 | 624 | 625 | In my day job, I am a sociologist studying the social mechanisms underlying the 626 | digital economy. For my PhD project, I investigate the governance of the app 627 | economy and how software ecosystems manage the tension between innovation and 628 | compatibility. If you are interested in this subject, feel free to get in 629 | touch. 630 | 631 | - Website 632 | - Mastodon 633 | - ResearchGate 634 | - LinkedIn 635 | 636 | 637 | 638 | Generated by panvimdoc 639 | 640 | vim:tw=78:ts=8:noet:ft=help:norl: 641 | -------------------------------------------------------------------------------- /lua/various-textobjs/charwise-core.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local u = require("various-textobjs.utils") 3 | -------------------------------------------------------------------------------- 4 | 5 | ---Sets the selection for the textobj (characterwise) 6 | ---@param startPos { [1]: integer, [2]: integer } 7 | ---@param endPos { [1]: integer, [2]: integer } 8 | function M.setSelection(startPos, endPos) 9 | u.saveJumpToJumplist() 10 | vim.api.nvim_win_set_cursor(0, startPos) 11 | u.normal(vim.fn.mode() == "v" and "o" or "v") 12 | vim.api.nvim_win_set_cursor(0, endPos) 13 | end 14 | 15 | ---@param endPos { [1]: integer, [2]: integer } 16 | ---@param notFoundMsg string|number 17 | function M.selectFromCursorTo(endPos, notFoundMsg) 18 | if #endPos ~= 2 then 19 | u.notFoundMsg(notFoundMsg) 20 | return 21 | end 22 | u.saveJumpToJumplist() 23 | u.normal(vim.fn.mode() == "v" and "o" or "v") 24 | vim.api.nvim_win_set_cursor(0, endPos) 25 | end 26 | 27 | ---Seek and select a characterwise textobj based on one pattern. 28 | ---CAVEAT multi-line-objects are not supported. 29 | ---@param pattern string lua pattern. REQUIRES two capture groups marking the 30 | ---two additions for the outer variant of the textobj. Use an empty capture group 31 | ---when there is no difference between inner and outer on that side. Basically, 32 | ---the two capture groups work similar to lookbehind/lookahead for the inner 33 | ---selector. 34 | ---@param scope "inner"|"outer" 35 | ---@param lookForwLines integer 36 | ---@return integer? startCol 37 | ---@return integer? endCol 38 | ---@return integer? row 39 | ---@nodiscard 40 | function M.getTextobjPos(pattern, scope, lookForwLines) 41 | -- when past the EoL in visual mode, will not find anything in that line 42 | -- anymore, thus moving back to EoL (see #108 and #109) 43 | if #vim.api.nvim_get_current_line() < vim.fn.col(".") then u.normal("h") end 44 | 45 | local cursorRow, cursorCol = unpack(vim.api.nvim_win_get_cursor(0)) 46 | local lineContent = u.getline(cursorRow) 47 | local lastLine = vim.api.nvim_buf_line_count(0) 48 | local beginCol = 0 ---@type number|nil 49 | local endCol, captureG1, captureG2, in1stLine 50 | 51 | -- first line: check if standing on or in front of textobj 52 | repeat 53 | beginCol, endCol, captureG1, captureG2 = lineContent:find(pattern, beginCol + 1) 54 | in1stLine = beginCol and (lineContent ~= "") -- check "" as .* returns non-nil then (#116) 55 | local standingOnOrInFront = endCol and endCol > cursorCol 56 | until standingOnOrInFront or not in1stLine 57 | 58 | -- subsequent lines: search full line for first occurrence 59 | local linesSearched = 0 60 | if not in1stLine then 61 | repeat 62 | linesSearched = linesSearched + 1 63 | if linesSearched > lookForwLines or cursorRow + linesSearched > lastLine then return end 64 | lineContent = u.getline(cursorRow + linesSearched) 65 | beginCol, endCol, captureG1, captureG2 = lineContent:find(pattern) 66 | until beginCol 67 | end 68 | 69 | -- capture groups determine the inner/outer difference 70 | -- `:find()` returns integers of the position if the capture group is empty 71 | if scope == "inner" then 72 | local frontOuterLen = type(captureG1) ~= "number" and #captureG1 or 0 73 | local backOuterLen = type(captureG2) ~= "number" and #captureG2 or 0 74 | beginCol = beginCol + frontOuterLen 75 | endCol = endCol - backOuterLen 76 | end 77 | 78 | beginCol = beginCol - 1 79 | endCol = endCol - 1 80 | local row = cursorRow + linesSearched 81 | return row, beginCol, endCol 82 | end 83 | 84 | -------------------------------------------------------------------------------- 85 | 86 | ---@class (exact) VariousTextobjs.PatternSpec 87 | ---@field [1] string 88 | ---@field greedy? boolean 89 | ---@field tieloser? boolean 90 | 91 | ---@alias VariousTextobjs.PatternInput string|table 92 | 93 | ---Searches for the position of one or multiple patterns and selects the closest one 94 | ---@param patterns VariousTextobjs.PatternInput lua pattern(s) for 95 | ---`getTextobjPos`; If the pattern starts with `tieloser` the textobj is always 96 | ---deprioritzed if the cursor stands on two objects. 97 | ---@param scope "inner"|"outer" 98 | ---@param lookForwLines integer 99 | ---@return integer? row -- only if found 100 | ---@return integer? startCol 101 | ---@return integer? endCol 102 | function M.selectClosestTextobj(patterns, scope, lookForwLines) 103 | local enableLogging = require("various-textobjs.config.config").config.debug 104 | local objLogging = {} 105 | 106 | -- initialized with values to always loose comparisons 107 | local closest = { row = math.huge, distance = math.huge, tieloser = true, cursorOnObj = false } 108 | 109 | -- get text object 110 | if type(patterns) == "string" then 111 | closest.row, closest.startCol, closest.endCol = 112 | M.getTextobjPos(patterns, scope, lookForwLines) 113 | elseif type(patterns) == "table" then 114 | local cursorCol = vim.api.nvim_win_get_cursor(0)[2] 115 | 116 | for patternName, patternSpec in pairs(patterns) do 117 | local cur = {} 118 | local pattern = patternSpec 119 | if type(patternSpec) ~= "string" then -- is PatternSpec instead of string 120 | pattern = patternSpec[1] ---@cast pattern string ensuring it here 121 | cur.greedy = patternSpec.greedy 122 | cur.tieloser = patternSpec.tieloser 123 | end 124 | cur.row, cur.startCol, cur.endCol = M.getTextobjPos(pattern, scope, lookForwLines) 125 | 126 | if cur.row and cur.startCol and cur.endCol then 127 | cur.distance = cur.startCol - cursorCol 128 | cur.endDistance = cursorCol - cur.endCol 129 | cur.cursorOnObj = cur.distance <= 0 and cur.endDistance <= 0 130 | cur.patternName = patternName 131 | 132 | -- INFO Here, we cannot simply use the absolute value of the distance. 133 | -- If the cursor is standing on a big textobj A, and there is a 134 | -- second textobj B which starts right after the cursor, A has a 135 | -- high negative distance, and B has a small positive distance. 136 | -- Using simply the absolute value to determine which obj is the 137 | -- closer one would then result in B being selected, even though the 138 | -- idiomatic behavior in vim is to always select an obj the cursor 139 | -- is standing on before seeking forward for a textobj. 140 | local closerInRow = cur.distance < closest.distance 141 | if cur.cursorOnObj and closest.cursorOnObj then 142 | closerInRow = cur.distance > closest.distance 143 | -- tieloser = when both objects enclose the cursor, the tieloser 144 | -- loses even when it is closer 145 | if closest.tieloser and not cur.tieloser then closerInRow = true end 146 | if not closest.tieloser and cur.tieloser then closerInRow = false end 147 | 148 | -- greedy = when both objects enclose the cursor, the greedy one 149 | -- wins if the distance is the same 150 | if cur.distance == closest.distance then 151 | if cur.greedy and not closest.greedy then closerInRow = true end 152 | if not cur.greedy and closest.greedy then closerInRow = false end 153 | end 154 | end 155 | 156 | if (cur.row < closest.row) or (cur.row == closest.row and closerInRow) then 157 | closest = cur 158 | end 159 | 160 | -- stylua: ignore 161 | objLogging[patternName] = { cur.startCol, cur.endCol, row = cur.row, distance = cur.distance, tieloser = cur.tieloser, cursorOnObj = cur.cursorOnObj } 162 | end 163 | end 164 | end 165 | 166 | if not (closest.row and closest.startCol and closest.endCol) then 167 | u.notFoundMsg(lookForwLines) 168 | return 169 | end 170 | 171 | -- set selection & log 172 | M.setSelection({ closest.row, closest.startCol }, { closest.row, closest.endCol }) 173 | if enableLogging and type(patterns) == "table" then 174 | local textobj = (debug.getinfo(3, "n") or {}).name or "unknown" 175 | objLogging._closest = closest.patternName 176 | vim.notify( 177 | vim.inspect(objLogging), 178 | vim.log.levels.DEBUG, 179 | { ft = "lua", title = scope .. " " .. textobj } 180 | ) 181 | end 182 | return closest.row, closest.startCol, closest.endCol 183 | end 184 | 185 | -------------------------------------------------------------------------------- 186 | return M 187 | -------------------------------------------------------------------------------- /lua/various-textobjs/config/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | ---@class VariousTextobjs.Config 4 | local defaultConfig = { 5 | keymaps = { 6 | -- See overview table in README for the defaults. (Note that lazy-loading 7 | -- this plugin, the default keymaps cannot be set up. if you set this to 8 | -- `true`, you thus need to add `lazy = false` to your lazy.nvim config.) 9 | useDefaults = false, 10 | 11 | -- disable only some default keymaps, for example { "ai", "!" } 12 | -- (only relevant when you set `useDefaults = true`) 13 | ---@type string[] 14 | disabledDefaults = {}, 15 | }, 16 | 17 | forwardLooking = { 18 | -- Number of lines to seek forwards for a text object. See the overview 19 | -- table in the README for which text object uses which value. 20 | small = 5, 21 | big = 15, 22 | }, 23 | behavior = { 24 | -- save position in jumplist when using text objects 25 | jumplist = true, 26 | }, 27 | 28 | -- extra configuration for specific text objects 29 | textobjs = { 30 | indentation = { 31 | -- `false`: only indentation decreases delimit the text object 32 | -- `true`: indentation decreases as well as blank lines serve as delimiter 33 | blanksAreDelimiter = false, 34 | }, 35 | subword = { 36 | -- When deleting the start of a camelCased word, the result should 37 | -- still be camelCased and not PascalCased (see #113). 38 | noCamelToPascalCase = true, 39 | }, 40 | diagnostic = { 41 | wrap = true, 42 | }, 43 | url = { 44 | patterns = { 45 | [[%l%l%l+://[^%s)%]}"'`>]+]], 46 | }, 47 | }, 48 | }, 49 | 50 | notify = { 51 | icon = "󰠱", -- only used with notification plugins like `nvim-notify` 52 | whenObjectNotFound = true, 53 | }, 54 | 55 | -- show debugging messages on use of certain text objects 56 | debug = false, 57 | } 58 | M.config = defaultConfig 59 | 60 | -------------------------------------------------------------------------------- 61 | 62 | ---@param userConfig? VariousTextobjs.Config 63 | function M.setup(userConfig) 64 | local warn = require("various-textobjs.utils").warn 65 | 66 | M.config = vim.tbl_deep_extend("force", M.config, userConfig or {}) 67 | 68 | -- DEPRECATION (2024-12-03) 69 | ---@diagnostic disable: undefined-field 70 | if M.config.lookForwardSmall then 71 | warn("The `lookForwardSmall` option is deprecated. Use `forwardLooking.small` instead.") 72 | end 73 | if M.config.lookForwardBig then 74 | warn("The `lookForwardBig` option is deprecated. Use `forwardLooking.big` instead.") 75 | end 76 | if M.config.lookForwardBig then 77 | warn("The `lookForwardBig` option is deprecated. Use `forwardLooking.big` instead.") 78 | end 79 | if M.config.notificationIcon then 80 | warn("The `notificationIcon` option is deprecated. Use `notify.icon` instead.") 81 | end 82 | if M.config.notifyNotFound ~= nil then -- not nil, since `false` is a valid value 83 | warn("The `notifyNotFound` option is deprecated. Use `notify.whenObjectNotFound` instead.") 84 | end 85 | -- DEPRECATION (2024-12-06) 86 | if M.config.useDefaultKeymaps ~= nil then 87 | warn("The `useDefaultKeymaps` option is deprecated. Use `keymaps.useDefaults` instead.") 88 | M.config.keymaps.useDefaults = M.config.useDefaultKeymaps 89 | end 90 | if M.config.disabledKeymaps then 91 | warn("The `disabledKeymaps` option is deprecated. Use `keymaps.disabledDefaults` instead.") 92 | M.config.keymaps.disabledDefaults = M.config.disabledKeymaps 93 | end 94 | ---@diagnostic enable: undefined-field 95 | 96 | if M.config.keymaps.useDefaults then 97 | require("various-textobjs..config.default-keymaps").setup(M.config.keymaps.disabledDefaults) 98 | end 99 | end 100 | 101 | -------------------------------------------------------------------------------- 102 | return M 103 | -------------------------------------------------------------------------------- /lua/various-textobjs/config/default-keymaps.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | local innerOuterMaps = { 5 | number = "n", 6 | value = "v", 7 | key = "k", 8 | subword = "S", -- lowercase taken for sentence textobj 9 | filepath = "F", -- `f` is usually used for outer/inner function textobjs 10 | notebookCell = "N", 11 | closedFold = "z", -- z is the common prefix for folds 12 | chainMember = "m", 13 | lineCharacterwise = "_", 14 | greedyOuterIndentation = "g", 15 | anyQuote = "q", 16 | anyBracket = "o", 17 | argument = ",", 18 | color = "#", 19 | doubleSquareBrackets = "D", 20 | } 21 | local oneMaps = { 22 | nearEoL = "n", -- does override the builtin "to next search match" textobj, but nobody really uses that 23 | visibleInWindow = "gw", 24 | toNextClosingBracket = "C", -- `%` has a race condition with vim's builtin matchit plugin 25 | toNextQuotationMark = "Q", 26 | restOfParagraph = "r", 27 | restOfIndentation = "R", 28 | restOfWindow = "gW", 29 | diagnostic = "!", 30 | column = "|", 31 | entireBuffer = "gG", -- `G` + `gg` 32 | url = "L", -- `gu`, `gU`, and `U` would conflict with `gugu`, `gUgU`, and `gUU`. `u` would conflict with `gcu` (undo comment) 33 | lastChange = "g;", -- consistent with `g;` movement 34 | emoji = ".", -- `:` would block the cmdline from visual mode, `e`/`E` conflicts with motions 35 | } 36 | local ftMaps = { 37 | { map = { mdLink = "l" }, fts = { "markdown" } }, 38 | { map = { mdEmphasis = "e" }, fts = { "markdown" } }, 39 | { map = { mdFencedCodeBlock = "C" }, fts = { "markdown" } }, 40 | { map = { cssSelector = "c" }, fts = { "css", "scss" } }, 41 | { map = { shellPipe = "P" }, fts = { "sh", "bash", "zsh", "fish" } }, 42 | { map = { htmlAttribute = "x" }, fts = { "html", "css", "scss", "xml", "vue" } }, 43 | } 44 | 45 | -------------------------------------------------------------------------------- 46 | 47 | function M.setup(disabledKeymaps) 48 | local function keymap(...) 49 | local args = { ... } 50 | if vim.tbl_contains(disabledKeymaps, args[2]) then return end 51 | vim.keymap.set(...) 52 | end 53 | 54 | for objName, map in pairs(oneMaps) do 55 | keymap( 56 | { "o", "x" }, 57 | map, 58 | "lua require('various-textobjs')." .. objName .. "()", 59 | { desc = objName .. " textobj" } 60 | ) 61 | end 62 | 63 | for objName, map in pairs(innerOuterMaps) do 64 | local name = " " .. objName .. " textobj" 65 | keymap( 66 | { "o", "x" }, 67 | "a" .. map, 68 | "lua require('various-textobjs')." .. objName .. "('outer')", 69 | { desc = "outer" .. name } 70 | ) 71 | keymap( 72 | { "o", "x" }, 73 | "i" .. map, 74 | "lua require('various-textobjs')." .. objName .. "('inner')", 75 | { desc = "inner" .. name } 76 | ) 77 | end 78 | -- stylua: ignore start 79 | keymap( { "o", "x" }, "ii" , "lua require('various-textobjs').indentation('inner', 'inner')", { desc = "inner-inner indentation textobj" }) 80 | keymap( { "o", "x" }, "ai" , "lua require('various-textobjs').indentation('outer', 'inner')", { desc = "outer-inner indentation textobj" }) 81 | keymap( { "o", "x" }, "iI" , "lua require('various-textobjs').indentation('inner', 'inner')", { desc = "inner-inner indentation textobj" }) 82 | keymap( { "o", "x" }, "aI" , "lua require('various-textobjs').indentation('outer', 'outer')", { desc = "outer-outer indentation textobj" }) 83 | -- stylua: ignore end 84 | 85 | local group = vim.api.nvim_create_augroup("VariousTextobjs", {}) 86 | for _, textobj in pairs(ftMaps) do 87 | vim.api.nvim_create_autocmd("FileType", { 88 | group = group, 89 | pattern = textobj.fts, 90 | callback = function() 91 | for objName, map in pairs(textobj.map) do 92 | local name = " " .. objName .. " textobj" 93 | -- stylua: ignore start 94 | keymap( { "o", "x" }, "a" .. map, ("lua require('various-textobjs').%s('%s')"):format(objName, "outer"), { desc = "outer" .. name, buffer = true }) 95 | keymap( { "o", "x" }, "i" .. map, ("lua require('various-textobjs').%s('%s')"):format(objName, "inner"), { desc = "inner" .. name, buffer = true }) 96 | -- stylua: ignore end 97 | end 98 | end, 99 | }) 100 | end 101 | end 102 | 103 | -------------------------------------------------------------------------------- 104 | return M 105 | -------------------------------------------------------------------------------- /lua/various-textobjs/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | ---@param userConfig? VariousTextobjs.Config 5 | function M.setup(userConfig) require("various-textobjs.config.config").setup(userConfig) end 6 | 7 | -- redirect calls to this module to the respective submodules 8 | setmetatable(M, { 9 | __index = function(_, key) 10 | return function(...) 11 | local warn = require("various-textobjs.utils").warn 12 | 13 | -- DEPRECATION (2025-04-24) 14 | if key == "pyTripleQuotes" then 15 | local msg = "The `pyTripleQuotes` textobj is deprecated. " 16 | .. "Please use `nvim-treesitter-teextobjects`, create a file " 17 | .. "`./queries/python/textobjects.scm` in your config dir with " 18 | .. "the following content:\n\n" 19 | .. "```\n" 20 | .. "; extends\n" 21 | .. "(expression_statement (string (string_content) @docstring.inner) @docstring.outer)\n" 22 | .. "```\n" 23 | .. "Call the textobject via `:TSTextobjectSelect @docstring.outer`" 24 | warn(msg) 25 | return function() end -- empty function to prevent error 26 | end 27 | 28 | local linewiseObjs = vim.tbl_keys(require("various-textobjs.textobjs.linewise")) 29 | local charwiseObjs = vim.tbl_keys(require("various-textobjs.textobjs.charwise")) 30 | 31 | local module 32 | if vim.tbl_contains(linewiseObjs, key) then module = "linewise" end 33 | if vim.tbl_contains(charwiseObjs, key) then module = "charwise" end 34 | if key == "column" then module = "blockwise" end 35 | if key == "diagnostic" then module = "diagnostic" end 36 | if key == "subword" then module = "subword" end 37 | if key == "emoji" then module = "emoji" end 38 | 39 | if module then 40 | require("various-textobjs.textobjs." .. module)[key](...) 41 | else 42 | local msg = ("There is no text object called `%s`.\n\n"):format(key) 43 | .. "Make sure it exists in the list of text objects, and that you haven't misspelled it." 44 | warn(msg) 45 | end 46 | end 47 | end, 48 | }) 49 | 50 | -------------------------------------------------------------------------------- 51 | return M 52 | -------------------------------------------------------------------------------- /lua/various-textobjs/textobjs/blockwise.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local u = require("various-textobjs.utils") 3 | -------------------------------------------------------------------------------- 4 | 5 | ---Column Textobj (blockwise up and/or down until indent or shorter line) 6 | ---@param direction string "down" (default), "up", "both" 7 | function M.column(direction) 8 | if direction == "both" then 9 | M.column("up") 10 | u.normal("oO") 11 | M.column("down") 12 | return 13 | end 14 | 15 | local step, key = 1, "j" 16 | if direction == "up" then 17 | step, key = -1, "k" 18 | end 19 | 20 | local lastLnum = vim.api.nvim_buf_line_count(0) 21 | local startRow = vim.api.nvim_win_get_cursor(0)[1] 22 | local trueCursorCol = vim.fn.virtcol(".") -- virtcol accurately accounts for tabs as indentation 23 | local extraColumns = vim.v.count1 - 1 -- before running other :normal commands, since they change v:count 24 | 25 | local nextLnum = startRow 26 | 27 | repeat 28 | nextLnum = nextLnum + step 29 | if nextLnum > lastLnum or nextLnum < 0 then break end 30 | local trueLineLength = #u.getline(nextLnum):gsub("\t", string.rep(" ", vim.bo.tabstop)) 31 | local shorterLine = trueLineLength <= trueCursorCol 32 | local hitsIndent = trueCursorCol <= vim.fn.indent(nextLnum) 33 | until hitsIndent or shorterLine 34 | local linesToMove = step * (nextLnum - startRow) - 1 35 | 36 | -- SET POSITION 37 | u.saveJumpToJumplist() 38 | 39 | -- start visual block mode ( requires special character `^V`) 40 | if not (vim.fn.mode() == "") then vim.cmd.execute([["normal! \"]]) end 41 | 42 | -- not using `setCursor`, since its column-positions are messed up by tab indentation 43 | -- not using `G` to go down lines, since affected by `opt.startofline` 44 | if linesToMove > 0 then u.normal(tostring(linesToMove) .. key) end 45 | if extraColumns > 0 then u.normal(tostring(extraColumns) .. "l") end 46 | end 47 | 48 | -------------------------------------------------------------------------------- 49 | return M 50 | -------------------------------------------------------------------------------- /lua/various-textobjs/textobjs/charwise.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local core = require("various-textobjs.charwise-core") 3 | local u = require("various-textobjs.utils") 4 | -------------------------------------------------------------------------------- 5 | 6 | ---@return integer 7 | ---@nodiscard 8 | local function smallForward() 9 | return require("various-textobjs.config.config").config.forwardLooking.small 10 | end 11 | 12 | ---@return integer 13 | ---@nodiscard 14 | local function bigForward() 15 | return require("various-textobjs.config.config").config.forwardLooking.big 16 | end 17 | 18 | -------------------------------------------------------------------------------- 19 | 20 | function M.toNextClosingBracket() 21 | local pattern = "().([]})])" 22 | local row, _, endCol = core.getTextobjPos(pattern, "inner", smallForward()) 23 | core.selectFromCursorTo({ row, endCol }, smallForward()) 24 | end 25 | 26 | function M.toNextQuotationMark() 27 | local pattern = [[()[^\](["'`])]] 28 | local row, _, endCol = core.getTextobjPos(pattern, "inner", smallForward()) 29 | core.selectFromCursorTo({ row, endCol }, smallForward()) 30 | end 31 | 32 | ---@param scope "inner"|"outer" 33 | function M.anyQuote(scope) 34 | -- INFO 35 | -- `%f[\"]` is the lua frontier pattern, and effectively used as a negative 36 | -- lookbehind, that is ensuring that the previous character may not be a `\` 37 | local patterns = { 38 | ['""'] = [[(%f[\"]").-(%f[\"]")]], 39 | ["''"] = [[(%f[\']').-(%f[\']')]], 40 | ["``"] = [[(%f[\`]`).-(%f[\`]`)]], 41 | } 42 | core.selectClosestTextobj(patterns, scope, smallForward()) 43 | end 44 | 45 | ---@param scope "inner"|"outer" 46 | function M.anyBracket(scope) 47 | local patterns = { 48 | ["()"] = "(%().-(%))", 49 | ["[]"] = "(%[).-(%])", 50 | ["{}"] = "({).-(})", 51 | } 52 | core.selectClosestTextobj(patterns, scope, smallForward()) 53 | end 54 | 55 | ---near end of the line, ignoring trailing whitespace 56 | ---(relevant for markdown, where you normally add a -space after the `.` ending a sentence.) 57 | function M.nearEoL() 58 | local pattern = "().(%S%s*)$" 59 | local row, _, endCol = core.getTextobjPos(pattern, "inner", 0) 60 | core.selectFromCursorTo({ row, endCol }, smallForward()) 61 | end 62 | 63 | ---current line, but characterwise 64 | ---@param scope "inner"|"outer" outer includes indentation and trailing spaces 65 | function M.lineCharacterwise(scope) 66 | local pattern = "^(%s*).-(%s*)$" -- use `.-` so inner obj does not match trailing spaces 67 | core.selectClosestTextobj(pattern, scope, smallForward()) 68 | end 69 | 70 | ---@param scope "inner"|"outer" inner value excludes trailing commas or semicolons, outer includes them. Both exclude trailing comments. 71 | function M.value(scope) 72 | -- captures value till the end of the line 73 | -- negative sets and frontier pattern ensure that equality comparators ==, != 74 | -- or css pseudo-elements :: are not matched 75 | local pattern = "(%s*%f[!<>~=:][=:]%s*)[^=:].*()" 76 | 77 | local row, startCol, _ = core.getTextobjPos(pattern, scope, smallForward()) 78 | if not (row and startCol) then 79 | u.notFoundMsg(smallForward()) 80 | return 81 | end 82 | 83 | -- if value found, remove trailing comment from it 84 | local lineContent = u.getline(row) 85 | if vim.bo.commentstring ~= "" then -- JSON has empty commentstring 86 | local commentPat = vim.bo.commentstring:gsub(" ?%%s.*", "") -- remove placeholder and backside of commentstring 87 | commentPat = vim.pesc(commentPat) -- escape lua pattern 88 | commentPat = " *" .. commentPat .. ".*" -- to match till end of line 89 | lineContent = lineContent:gsub(commentPat, "") -- remove commentstring 90 | end 91 | local valueEndCol = #lineContent - 1 92 | 93 | -- inner value = exclude trailing comma/semicolon 94 | if scope == "inner" and lineContent:find("[,;]$") then valueEndCol = valueEndCol - 1 end 95 | 96 | -- set selection 97 | core.setSelection({ row, startCol }, { row, valueEndCol }) 98 | end 99 | 100 | ---@param scope "inner"|"outer" outer key includes the `:` or `=` after the key 101 | function M.key(scope) 102 | local pattern = "()%S.-( ?[:=] ?)" 103 | core.selectClosestTextobj(pattern, scope, smallForward()) 104 | end 105 | 106 | ---@param scope "inner"|"outer" inner number consists purely of digits, outer number factors in decimal points and includes minus sign 107 | function M.number(scope) 108 | -- Here two different patterns make more sense, so the inner number can match 109 | -- before and after the decimal dot. enforcing digital after dot so outer 110 | -- excludes enumrations. 111 | local pattern = "%d+" ---@type VariousTextobjs.PatternInput 112 | if scope == "outer" then 113 | pattern = { 114 | -- The outer pattern considers `.` as decimal separators, `_` as 115 | -- thousand separator, and a potential leading `-` for negative numbers. 116 | underscoreAsThousandSep = "%-?%d[%d_]*%d%.?%d*", 117 | noThousandSep = { "%-?%d+%.?%d*", tieloser = true }, 118 | } 119 | end 120 | core.selectClosestTextobj(pattern, "outer", smallForward()) 121 | end 122 | 123 | function M.url() 124 | local urlPatterns = require("various-textobjs.config.config").config.textobjs.url.patterns 125 | core.selectClosestTextobj(urlPatterns, "outer", bigForward()) 126 | end 127 | 128 | ---@param scope "inner"|"outer" inner is only the filename 129 | function M.filepath(scope) 130 | local pattern = { 131 | unixPath = "([.~]?/?[%w_%-.$/]+/)[%w_%-.]+()", 132 | } 133 | core.selectClosestTextobj(pattern, scope, bigForward()) 134 | end 135 | 136 | ---@param scope "inner"|"outer" inner excludes the leading dot 137 | function M.chainMember(scope) 138 | -- make with-call greedy, so the call of a chainmember is always included 139 | local patterns = { 140 | leadingWithoutCall = "()[%w_][%w_]*([:.])", 141 | leadingWithCall = { "()[%w_][%w_]*%b()([:.])", greedy = true }, 142 | followingWithoutCall = "([:.])[%w_][%w_]*()", 143 | followingWithCall = { "([:.])[%w_][%w_]*%b()()", greedy = true }, 144 | } 145 | core.selectClosestTextobj(patterns, scope, smallForward()) 146 | end 147 | 148 | ---@param scope "inner"|"outer" outer includes the comma 149 | function M.argument(scope) 150 | local patterns = { 151 | -- CAVEAT patterns will not work with arguments that contain a `()`, to 152 | -- get those accurately, you will need treeesitter 153 | leadingComma = [[(,)[%w_."'%]%[]+()]], 154 | followingComma = [[()[%w_."'%]%[]+(,)]], 155 | } 156 | core.selectClosestTextobj(patterns, scope, smallForward()) 157 | end 158 | 159 | function M.lastChange() 160 | local changeStartPos = vim.api.nvim_buf_get_mark(0, "[") 161 | local changeEndPos = vim.api.nvim_buf_get_mark(0, "]") 162 | 163 | if changeStartPos[1] == changeEndPos[1] and changeStartPos[2] == changeEndPos[2] then 164 | u.warn("Last change was a deletion operation, aborting.") 165 | return 166 | end 167 | 168 | core.setSelection(changeStartPos, changeEndPos) 169 | end 170 | 171 | -------------------------------------------------------------------------------- 172 | -- FILETYPE SPECIFIC TEXTOBJS 173 | 174 | ---@param scope "inner"|"outer" inner link only includes the link title, outer link includes link, url, and the four brackets. 175 | function M.mdLink(scope) 176 | local pattern = "(%[)[^%]]-(%]%b())" 177 | core.selectClosestTextobj(pattern, scope, smallForward()) 178 | end 179 | 180 | -- DEPRECATION (2024-12-04), changed for consistency with other objects 181 | function M.mdlink() u.warn("`.mdlink()` is deprecated. Use `.mdLink()` instead (uses capital L).") end 182 | 183 | ---@param scope "inner"|"outer" inner selector only includes the content, outer selector includes the type. 184 | function M.mdEmphasis(scope) 185 | -- CAVEAT this still has a few edge cases with escaped markup, will need a 186 | -- treesitter object to reliably account for that. 187 | local patterns = { 188 | ["**?"] = "([^\\]%*%*?).-[^\\](%*%*?)", 189 | ["__?"] = "([^\\]__?).-[^\\](__?)", 190 | ["=="] = "([^\\]==).-[^\\](==)", 191 | ["~~"] = "([^\\]~~).-[^\\](~~)", 192 | ["**? (start)"] = "(^%*%*?).-[^\\](%*%*?)", 193 | ["__? (start)"] = "(^__?).-[^\\](__?)", 194 | ["== (start)"] = "(^==).-[^\\](==)", 195 | ["~~ (start)"] = "(^~~).-[^\\](~~)", 196 | } 197 | core.selectClosestTextobj(patterns, scope, smallForward()) 198 | 199 | -- pattern accounts for escape char, so move to right to account for that 200 | local isAtStart = vim.api.nvim_win_get_cursor(0)[2] == 1 201 | if scope == "outer" and not isAtStart then u.normal("ol") end 202 | end 203 | 204 | ---@param scope "inner"|"outer" inner selector excludes the brackets themselves 205 | function M.doubleSquareBrackets(scope) 206 | local pattern = "(%[%[).-(%]%])" 207 | core.selectClosestTextobj(pattern, scope, smallForward()) 208 | end 209 | 210 | ---@param scope "inner"|"outer" outer selector includes trailing comma and whitespace 211 | function M.cssSelector(scope) 212 | local pattern = "()[#.][%w-_]+(,? ?)" 213 | core.selectClosestTextobj(pattern, scope, smallForward()) 214 | end 215 | 216 | ---@param scope "inner"|"outer" inner selector is only the value of the attribute inside the quotation marks. 217 | function M.htmlAttribute(scope) 218 | local pattern = { 219 | ['""'] = '([%w-]+=").-(")', 220 | ["''"] = "([%w-]+=').-(')", 221 | } 222 | core.selectClosestTextobj(pattern, scope, smallForward()) 223 | end 224 | 225 | ---@param scope "inner"|"outer" outer selector includes the pipe 226 | function M.shellPipe(scope) 227 | local patterns = { 228 | trailingPipe = "()[^|%s][^|]-( ?| ?)", -- 1st char non-space to exclude indentation 229 | leadingPipe = "( ?| ?)[^|]*()", 230 | } 231 | core.selectClosestTextobj(patterns, scope, smallForward()) 232 | end 233 | 234 | ---@param scope "inner"|"outer" inner selector only affects the color value 235 | function M.color(scope) 236 | local pattern = { 237 | ["#123456"] = "(#)" .. ("%x"):rep(6) .. "()", 238 | ["hsl(123, 23%, 23%)"] = "(hsla?%()[%%%d,./deg ]-(%))", -- optionally with `deg`/`%` 239 | ["rgb(123, 23, 23)"] = "(rgba?%()[%d,./ ]-(%))", -- optionally with `%` 240 | ["ansi-color-e"] = "\\e%[[%d;]+m", -- \e[1;32m or \e[48;5;123m 241 | ["ansi-color-033"] = "\\033%[[%d;]+m", 242 | ["ansi-color-x1b"] = "\\x1b%[[%d;]+m", 243 | } 244 | core.selectClosestTextobj(pattern, scope, smallForward()) 245 | end 246 | 247 | ---@deprecated 248 | function M.cssColor(...) 249 | u.warn("`.cssColor` is deprecated, use `.color`. (Now also supports ansi color codes)") 250 | M.color(...) 251 | end 252 | 253 | -------------------------------------------------------------------------------- 254 | return M 255 | -------------------------------------------------------------------------------- /lua/various-textobjs/textobjs/diagnostic.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local core = require("various-textobjs.charwise-core") 3 | local u = require("various-textobjs.utils") 4 | -------------------------------------------------------------------------------- 5 | 6 | function M.diagnostic(oldWrapSetting) 7 | -- DEPRECATION (2024-12-03) 8 | if oldWrapSetting ~= nil then 9 | local msg = 10 | '`.diagnostic()` does not use a "wrap" argument anymore. Use the config `textobjs.diagnostic.wrap` instead.' 11 | u.warn(msg) 12 | end 13 | 14 | local wrap = require("various-textobjs.config.config").config.textobjs.diagnostic.wrap 15 | 16 | -- HACK if cursor is standing on a diagnostic, get_prev() will return that 17 | -- diagnostic *BUT* only if the cursor is not on the first character of the 18 | -- diagnostic, since the columns checked seem to be off-by-one as well m( 19 | -- Therefore counteracted by temporarily moving the cursor 20 | u.normal("l") 21 | local prevD = vim.diagnostic.get_prev { wrap = false } 22 | u.normal("h") 23 | 24 | local nextD = vim.diagnostic.get_next { wrap = wrap } 25 | local curStandingOnPrevD = false -- however, if prev diag is covered by or before the cursor has yet to be determined 26 | local curRow, curCol = unpack(vim.api.nvim_win_get_cursor(0)) 27 | 28 | if prevD then 29 | local curAfterPrevDstart = (curRow == prevD.lnum + 1 and curCol >= prevD.col) 30 | or (curRow > prevD.lnum + 1) 31 | local curBeforePrevDend = (curRow == prevD.end_lnum + 1 and curCol <= prevD.end_col - 1) 32 | or (curRow < prevD.end_lnum) 33 | curStandingOnPrevD = curAfterPrevDstart and curBeforePrevDend 34 | end 35 | 36 | local target = curStandingOnPrevD and prevD or nextD 37 | if target then 38 | core.setSelection( 39 | { target.lnum + 1, target.col }, 40 | { target.end_lnum + 1, target.end_col - 1 } 41 | ) 42 | else 43 | u.notFoundMsg("No diagnostic found.") 44 | end 45 | end 46 | 47 | -------------------------------------------------------------------------------- 48 | return M 49 | -------------------------------------------------------------------------------- /lua/various-textobjs/textobjs/emoji.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local core = require("various-textobjs.charwise-core") 3 | local u = require("various-textobjs.utils") 4 | -------------------------------------------------------------------------------- 5 | 6 | ---Decode one UTF-8 codepoint starting from position `i` 7 | ---@param str string UTF-8 encoded string 8 | ---@param i number starting byte index 9 | ---@return number? cp decoded codepoint 10 | ---@return number? nextI The next byte index 11 | local function utf8Decode(str, i) 12 | local c = str:byte(i) 13 | if not c then return end 14 | 15 | if c < 0x80 then 16 | return c, i + 1 17 | elseif c < 0xE0 then 18 | local c2 = str:byte(i + 1) 19 | return ((c % 0x20) * 0x40 + (c2 % 0x40)), i + 2 20 | elseif c < 0xF0 then 21 | local c2, c3 = str:byte(i + 1, i + 2) 22 | return ((c % 0x10) * 0x1000 + (c2 % 0x40) * 0x40 + (c3 % 0x40)), i + 3 23 | elseif c < 0xF8 then 24 | local c2, c3, c4 = str:byte(i + 1, i + 3) 25 | return ((c % 0x08) * 0x40000 + (c2 % 0x40) * 0x1000 + (c3 % 0x40) * 0x40 + (c4 % 0x40)), i + 4 26 | end 27 | end 28 | 29 | ---Check if a codepoint is likely an emoji or NerdFont glyph. 30 | ---@param cp number Unicode codepoint 31 | ---@return boolean 32 | local function isEmoji(cp) 33 | return ( 34 | (cp >= 0x1F600 and cp <= 0x1F64F) -- Emoticons 35 | or (cp >= 0x1F300 and cp <= 0x1F5FF) -- Misc Symbols and Pictographs 36 | or (cp >= 0x1F680 and cp <= 0x1F6FF) -- Transport and Map 37 | or (cp >= 0x1F900 and cp <= 0x1F9FF) -- Supplemental Symbols and Pictographs 38 | or (cp >= 0x1FA70 and cp <= 0x1FAFF) -- Extended-A 39 | or (cp >= 0x2600 and cp <= 0x26FF) -- Misc symbols 40 | or (cp >= 0x2700 and cp <= 0x27BF) -- Dingbats 41 | or (cp >= 0xE000 and cp <= 0xF8FF) -- Private Use Area (PUA, where NerdFonts map glyphs) 42 | or (cp >= 0xF0000 and cp <= 0xFFFFD) -- Supplementary Private Use Area-A 43 | or (cp >= 0x100000 and cp <= 0x10FFFD) -- Supplementary Private Use Area-B 44 | ) 45 | end 46 | 47 | ---@param input string 48 | ---@param offset number? Optional starting byte index (defaults to 1) 49 | ---@return number? startPos The byte index of the start of the emoji 50 | ---@return number? endPos The byte index of the end of the emoji 51 | local function findEmoji(input, offset) 52 | local i = offset or 1 53 | while i <= #input do 54 | local cp, nextI = utf8Decode(input, i) 55 | if not (cp and nextI) then return end 56 | if isEmoji(cp) then return i, nextI - 1 end 57 | i = nextI 58 | end 59 | end 60 | 61 | local function getLine(lnum) return vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1] end 62 | 63 | -------------------------------------------------------------------------------- 64 | 65 | function M.emoji() 66 | local lookForw = require("various-textobjs.config.config").config.forwardLooking.small 67 | local row, col = unpack(vim.api.nvim_win_get_cursor(0)) 68 | local stopRow = math.min(row + lookForw, vim.api.nvim_buf_line_count(0)) 69 | 70 | local startPos, endPos 71 | while true do 72 | startPos, endPos = findEmoji(getLine(row), col) 73 | if startPos and endPos then break end 74 | col = 1 -- for lines after the one with the cursor, search from the start 75 | row = row + 1 76 | if row > stopRow then 77 | u.notFoundMsg(lookForw) 78 | return 79 | end 80 | end 81 | 82 | startPos, endPos = startPos - 1, endPos - 1 -- lua indexing 83 | core.setSelection({ row, startPos }, { row, endPos }) 84 | end 85 | 86 | -------------------------------------------------------------------------------- 87 | return M 88 | -------------------------------------------------------------------------------- /lua/various-textobjs/textobjs/linewise.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local u = require("various-textobjs.utils") 3 | -------------------------------------------------------------------------------- 4 | 5 | ---sets the selection for the textobj (linewise) 6 | ---@param startline integer 7 | ---@param endline integer 8 | local function setLinewiseSelection(startline, endline) 9 | u.saveJumpToJumplist() 10 | vim.api.nvim_win_set_cursor(0, { startline, 0 }) 11 | if vim.fn.mode() ~= "V" then u.normal("V") end 12 | u.normal("o") 13 | vim.api.nvim_win_set_cursor(0, { endline, 0 }) 14 | end 15 | 16 | ---@param lineNr number 17 | ---@return boolean|nil -- nil when lineNr is out of bounds 18 | local function isBlankLine(lineNr) 19 | local lastLine = vim.api.nvim_buf_line_count(0) 20 | if lineNr > lastLine or lineNr < 1 then return nil end 21 | local lineContent = u.getline(lineNr) 22 | return lineContent:find("^%s*$") ~= nil 23 | end 24 | 25 | -------------------------------------------------------------------------------- 26 | 27 | ---@param scope "inner"|"outer" outer adds one line after the fold 28 | function M.closedFold(scope) 29 | local startLnum = vim.api.nvim_win_get_cursor(0)[1] 30 | local lastLine = vim.api.nvim_buf_line_count(0) 31 | local startedOnFold = vim.fn.foldclosed(startLnum) > 0 32 | local foldStart, foldEnd 33 | local bigForward = require("various-textobjs.config.config").config.forwardLooking.big 34 | 35 | if startedOnFold then 36 | foldStart = vim.fn.foldclosed(startLnum) 37 | foldEnd = vim.fn.foldclosedend(startLnum) 38 | else 39 | foldStart = startLnum 40 | repeat 41 | if foldStart >= lastLine or foldStart > (bigForward + startLnum) then 42 | u.notFoundMsg(bigForward) 43 | return 44 | end 45 | foldStart = foldStart + 1 46 | local reachedClosedFold = vim.fn.foldclosed(foldStart) > 0 47 | until reachedClosedFold 48 | foldEnd = vim.fn.foldclosedend(foldStart) 49 | end 50 | if scope == "outer" and (foldEnd + 1 <= lastLine) then foldEnd = foldEnd + 1 end 51 | 52 | -- fold has to be opened for so line can be correctly selected 53 | vim.cmd(("%d,%d foldopen"):format(foldStart, foldEnd)) 54 | setLinewiseSelection(foldStart, foldEnd) 55 | 56 | -- if yanking, close the fold afterwards again. 57 | -- (For the other operators, opening the fold does not matter (d) or is desirable (gu).) 58 | if vim.v.operator == "y" then vim.cmd(("%d,%d foldclose"):format(foldStart, foldEnd)) end 59 | end 60 | 61 | function M.entireBuffer() 62 | -- FIX folds at the first or last line cause lines being left out 63 | local foldWasEnabled = vim.opt_local.foldenable:get() ---@diagnostic disable-line: undefined-field 64 | if foldWasEnabled then vim.opt_local.foldenable = false end 65 | 66 | local lastLine = vim.api.nvim_buf_line_count(0) 67 | setLinewiseSelection(1, lastLine) 68 | 69 | if foldWasEnabled then vim.opt_local.foldenable = true end 70 | end 71 | 72 | ---rest of paragraph (linewise) 73 | function M.restOfParagraph() 74 | if vim.fn.mode() ~= "V" then u.normal("V") end 75 | u.normal("}") 76 | 77 | -- one up, except on last line 78 | local curLnum = vim.api.nvim_win_get_cursor(0)[1] 79 | local lastLine = vim.api.nvim_buf_line_count(0) 80 | if curLnum ~= lastLine then u.normal("k") end 81 | end 82 | 83 | ---@param scope "inner"|"outer" inner excludes the backticks 84 | function M.mdFencedCodeBlock(scope) 85 | -- 1. allow indented codeblocks after various md syntax (#78) 86 | -- 2. allow anything as codeblock label (#127) 87 | local codeBlockPattern = "^[%d%s.)>-*+]*```[^`]*$" 88 | 89 | local cursorLnum = vim.api.nvim_win_get_cursor(0)[1] 90 | local bigForward = require("various-textobjs.config.config").config.forwardLooking.big 91 | 92 | -- scan buffer for all code blocks, add beginnings & endings to a table each 93 | local cbBegin = {} 94 | local cbEnd = {} 95 | local allLines = vim.api.nvim_buf_get_lines(0, 0, -1, true) 96 | local i = 1 97 | for _, line in pairs(allLines) do 98 | if line:find(codeBlockPattern) then 99 | if #cbBegin == #cbEnd then 100 | table.insert(cbBegin, i) 101 | else 102 | table.insert(cbEnd, i) 103 | end 104 | end 105 | i = i + 1 106 | end 107 | 108 | if #cbBegin > #cbEnd then table.remove(cbBegin) end -- incomplete codeblock 109 | 110 | -- determine cursor location in a codeblock 111 | local j = 0 112 | repeat 113 | j = j + 1 114 | if j > #cbBegin then 115 | u.notFoundMsg(bigForward) 116 | return 117 | end 118 | local cursorInBetween = (cbBegin[j] <= cursorLnum) and (cbEnd[j] >= cursorLnum) 119 | -- seek forward for a codeblock 120 | local cursorInFront = (cbBegin[j] > cursorLnum) and (cbBegin[j] <= cursorLnum + bigForward) 121 | until cursorInBetween or cursorInFront 122 | 123 | local start = cbBegin[j] 124 | local ending = cbEnd[j] 125 | if scope == "inner" then 126 | start = start + 1 127 | ending = ending - 1 128 | end 129 | 130 | setLinewiseSelection(start, ending) 131 | end 132 | 133 | function M.visibleInWindow() 134 | local start = vim.fn.line("w0") 135 | local ending = vim.fn.line("w$") 136 | setLinewiseSelection(start, ending) 137 | end 138 | 139 | function M.restOfWindow() 140 | local start = vim.fn.line(".") 141 | local ending = vim.fn.line("w$") 142 | setLinewiseSelection(start, ending) 143 | end 144 | 145 | -------------------------------------------------------------------------------- 146 | 147 | ---@param startBorder "inner"|"outer" 148 | ---@param endBorder "inner"|"outer" 149 | ---@return boolean success 150 | function M.indentation(startBorder, endBorder, oldBlankSetting) 151 | -- DEPRECATION (2024-12-06) 152 | if oldBlankSetting ~= nil then 153 | local msg = 154 | "`.indentation()` does not use a 3rd argument anymore. Use the config `textobjs.indent.blanksAreDelimiter` instead." 155 | u.warn(msg) 156 | end 157 | local blanksDelimit = 158 | require("various-textobjs.config.config").config.textobjs.indentation.blanksAreDelimiter 159 | 160 | -- when on blank line seek for next non-blank line to start 161 | local curLnum = vim.api.nvim_win_get_cursor(0)[1] 162 | while isBlankLine(curLnum) do 163 | curLnum = curLnum + 1 164 | end 165 | local startIndent = vim.fn.indent(curLnum) -- `-1` for out of bounds 166 | if startIndent < 1 then 167 | u.warn("Current line is not indented.") 168 | return false 169 | end 170 | local prevLn = curLnum - 1 171 | local nextLn = curLnum + 1 172 | local lastLine = vim.api.nvim_buf_line_count(0) 173 | 174 | -- seek backwards/forwards until meeting line with higher indentation, blank 175 | -- (if used as delimiter), or start/end of file 176 | while (isBlankLine(prevLn) and not blanksDelimit) or vim.fn.indent(prevLn) >= startIndent do 177 | prevLn = prevLn - 1 178 | if prevLn == 0 then break end 179 | end 180 | while (isBlankLine(nextLn) and not blanksDelimit) or vim.fn.indent(nextLn) >= startIndent do 181 | nextLn = nextLn + 1 182 | if nextLn > lastLine then break end 183 | end 184 | 185 | -- at start/end of file, abort when with `outer` or go back a step for `inner` 186 | if prevLn == 0 and startBorder == "outer" then 187 | u.notFoundMsg("No top border found.") 188 | return false 189 | elseif nextLn > lastLine and endBorder == "outer" then 190 | u.notFoundMsg("No bottom border found.") 191 | return false 192 | end 193 | if startBorder == "inner" then prevLn = prevLn + 1 end 194 | if endBorder == "inner" then nextLn = nextLn - 1 end 195 | 196 | -- keep blanks in case of missing bottom border (e.g. for python) 197 | while isBlankLine(nextLn) do 198 | nextLn = nextLn - 1 199 | end 200 | 201 | setLinewiseSelection(prevLn, nextLn) 202 | return true 203 | end 204 | 205 | ---outer indentation, expanded until the next blank lines in both directions 206 | ---@param scope "inner"|"outer" outer adds a blank, like ip/ap textobjs 207 | function M.greedyOuterIndentation(scope) 208 | local success = M.indentation("outer", "outer") 209 | if not success then return end 210 | 211 | u.normal("o{j") -- to next blank line above 212 | u.normal("o}") -- to next blank line down 213 | if scope == "inner" then u.normal("k") end -- exclude blank below if inner 214 | end 215 | 216 | ---from cursor position down all lines with same or higher indentation; 217 | ---essentially `ii` downwards 218 | function M.restOfIndentation() 219 | local startLnum = vim.api.nvim_win_get_cursor(0)[1] 220 | local lastLine = vim.api.nvim_buf_line_count(0) 221 | local curLnum = startLnum 222 | while isBlankLine(curLnum) do -- when on blank line, use next line 223 | if lastLine == curLnum then return end 224 | curLnum = curLnum + 1 225 | end 226 | 227 | local indentOfStart = vim.fn.indent(curLnum) 228 | if indentOfStart == 0 then 229 | u.warn("Current line is not indented.") 230 | return 231 | end 232 | 233 | local nextLnum = curLnum + 1 234 | 235 | while isBlankLine(nextLnum) or vim.fn.indent(nextLnum) >= indentOfStart do 236 | if nextLnum > lastLine then break end 237 | nextLnum = nextLnum + 1 238 | end 239 | 240 | setLinewiseSelection(startLnum, nextLnum - 1) 241 | end 242 | 243 | -------------------------------------------------------------------------------- 244 | 245 | ---@param scope "inner"|"outer" outer includes bottom cell border 246 | function M.notebookCell(scope) 247 | local function isCellBorder(lnum) 248 | local cellMarker = vim.bo.commentstring:format("%%") 249 | local line = u.getline(lnum) 250 | return vim.startswith(vim.trim(line), cellMarker) 251 | end 252 | 253 | if vim.bo.commentstring == "" then 254 | u.warn("Buffer has no commentstring set.") 255 | return 256 | end 257 | 258 | local curLnum = vim.api.nvim_win_get_cursor(0)[1] 259 | local lastLine = vim.api.nvim_buf_line_count(0) 260 | local prevLnum = curLnum 261 | local nextLnum = isCellBorder(curLnum) and curLnum + 1 or curLnum 262 | 263 | while prevLnum > 0 and not isCellBorder(prevLnum) do 264 | prevLnum = prevLnum - 1 265 | end 266 | while nextLnum <= lastLine and not isCellBorder(nextLnum) do 267 | nextLnum = nextLnum + 1 268 | end 269 | 270 | -- outer includes *top* cell border (see #124) 271 | if scope == "outer" and prevLnum > 1 then prevLnum = prevLnum - 1 end 272 | 273 | setLinewiseSelection(prevLnum + 1, nextLnum - 1) 274 | end 275 | 276 | -------------------------------------------------------------------------------- 277 | return M 278 | -------------------------------------------------------------------------------- /lua/various-textobjs/textobjs/subword.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local core = require("various-textobjs.charwise-core") 3 | local u = require("various-textobjs.utils") 4 | -------------------------------------------------------------------------------- 5 | 6 | ---@param scope "inner"|"outer" 7 | function M.subword(scope) 8 | -- needs to be saved, since using a textobj always results in visual mode 9 | local initialMode = vim.fn.mode() 10 | 11 | local patterns = { 12 | camelOrLowercase = "()%a[%l%d]+([_-]?)", 13 | UPPER_CASE = "()%u[%u%d]+([_-]?)", 14 | number = "()%d+([_-]?)", 15 | 16 | -- e.g., "x" in "xSide" or "sideX" (see #75) 17 | singleChar = { "()%a([_-]?)", tieloser = true }, 18 | } 19 | local row, startCol, endCol = core.selectClosestTextobj(patterns, scope, 0) 20 | if not (row and startCol and endCol) then return end 21 | 22 | ----------------------------------------------------------------------------- 23 | -- EXTRA ADJUSTMENTS 24 | local line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1] 25 | startCol, endCol = startCol + 1, endCol + 1 -- adjust for lua indexing for `:sub` 26 | local charBefore = line:sub(startCol - 1, startCol - 1) 27 | local charAfter = line:sub(endCol + 1, endCol + 1) 28 | local firstChar = line:sub(startCol, startCol) 29 | local lastChar = line:sub(endCol, endCol) 30 | 31 | -- LEADING `-_` ON LAST PART OF SUBWORD 32 | -- 1. The outer pattern checks for subwords that with potentially trailing 33 | -- `_-`, however, if the subword is the last segment of a word, there is 34 | -- potentially also a leading `_-` which should be included (see #83). 35 | -- 2. Checking for those with patterns is not possible, since subwords 36 | -- without any trailing/leading chars are always considered the closest (and 37 | -- thus prioritized by `selectClosestTextobj`), even though the usage 38 | -- expectation is that `subword` should be more greedy. 39 | -- 3. Thus, we check if we are on the last part of a snake_cased word, and if 40 | -- so, add the leading `_-` to the selection. 41 | local onLastSnakeCasePart = charBefore:find("[_-]") and not lastChar:find("[_-]") 42 | if scope == "outer" and onLastSnakeCasePart then 43 | -- `o`: to start of selection, `h`: select char before `o`: back to end 44 | u.normal("oho") 45 | end 46 | 47 | -- CAMEL/PASCAL CASE DEALING 48 | -- When deleting the start of a camelCased word, the result should still be 49 | -- camelCased and not PascalCased (see #113). 50 | local noCamelToPascalCase = 51 | require("various-textobjs.config.config").config.textobjs.subword.noCamelToPascalCase 52 | if noCamelToPascalCase then 53 | local isCamel = vim.fn.expand(""):find("%l%u") 54 | local notPascal = not firstChar:find("%u") -- see https://github.com/chrisgrieser/nvim-various-textobjs/issues/113#issuecomment-2752632884 55 | local nextIsPascal = charAfter:find("%u") 56 | local isWordStart = charBefore:find("%W") or charBefore == "" 57 | local isDeletion = vim.v.operator == "d" 58 | local notVisual = not initialMode:find("[Vv]") -- see #121 59 | if isCamel and notPascal and nextIsPascal and isWordStart and isDeletion and notVisual then 60 | -- lowercase the following subword 61 | local updatedLine = line:sub(1, endCol) .. charAfter:lower() .. line:sub(endCol + 2) 62 | vim.api.nvim_buf_set_lines(0, row - 1, row, false, { updatedLine }) 63 | end 64 | end 65 | end 66 | 67 | -------------------------------------------------------------------------------- 68 | return M 69 | -------------------------------------------------------------------------------- /lua/various-textobjs/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | ---runs `:normal` with bang 5 | ---@param cmdStr string 6 | function M.normal(cmdStr) 7 | local is08orHigher = vim.version().major > 0 or vim.version().minor > 7 8 | if is08orHigher then 9 | vim.cmd.normal { cmdStr, bang = true } 10 | else 11 | vim.cmd("normal! " .. cmdStr) 12 | end 13 | end 14 | 15 | ---equivalent to fn.getline(), but using more efficient nvim api 16 | ---@param lnum integer 17 | ---@return string 18 | ---@nodiscard 19 | function M.getline(lnum) return vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1] end 20 | 21 | ---@param msg string 22 | function M.warn(msg) 23 | local icon = require("various-textobjs.config.config").config.notify.icon 24 | vim.notify(msg, vim.log.levels.WARN, { title = "various-textobjs", icon = icon }) 25 | end 26 | 27 | ---notification when no textobj could be found 28 | ---@param msg integer|string lines tried to look forward, or custom message 29 | function M.notFoundMsg(msg) 30 | if not require("various-textobjs.config.config").config.notify.whenObjectNotFound then return end 31 | local notifyText 32 | if type(msg) == "number" then 33 | local lookForwLines = msg 34 | notifyText = ("Textobject not found within the next %d lines."):format(lookForwLines) 35 | if lookForwLines == 1 then notifyText = notifyText:gsub("s%.$", ".") end 36 | if lookForwLines == 0 then notifyText = "Textobject not found within the line." end 37 | elseif type(msg) == "string" then 38 | notifyText = msg 39 | end 40 | local icon = require("various-textobjs.config.config").config.notify.icon 41 | vim.notify(notifyText, vim.log.levels.INFO, { title = "various-textobjs", icon = icon }) 42 | end 43 | 44 | function M.saveJumpToJumplist() 45 | local jumplist = require("various-textobjs.config.config").config.behavior.jumplist 46 | if jumplist then M.normal("m`") end 47 | end 48 | 49 | -------------------------------------------------------------------------------- 50 | return M 51 | --------------------------------------------------------------------------------