├── .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 |
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 |
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 |
--------------------------------------------------------------------------------