Use the w, e, b motions like a spider. Move by subwords and skip insignificant punctuation.
The w, e, b (and ge) motions work the same as the default ones by vim, except for two differences:
The motions are based on subwords, meaning they stop at the segments of a camelCase, SNAKE_CASE, or kebab-case variable.
-- positions vim's `w` will move to local myVariableName = FOO_BAR_BAZ -- ^ ^ ^ -- positions spider's `w` will move to local myVariableName = FOO_BAR_BAZ -- ^ ^ ^ ^ ^ ^ ^A sequence of one or more punctuation characters is considered significant if it is surrounded by whitespace and does not include any non-punctuation characters:
foo == bar .. "baz" -- ^ ^ significant punctuation foo:find("a") -- ^ ^ ^ insignificant punctuationThis speeds up the movement across the line by reducing the number of mostly unnecessary stops:
-- positions vim's `w` will move to if foo:find("%d") and foo == bar then print("[foo] has" .. bar) end -- ^ ^^ ^ ^^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ -> 21 -- positions spider's `w` will move to if foo:find("%d") and foo == bar then print("[foo] has" .. bar) end -- ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ -> 14If you prefer to use this plugin only for subword motions, you can disable this feature by setting skipInsignificantPunctuation = false in the .setup() call.
Note
This plugin ignores vim's iskeyword option.
-- packer use { "chrisgrieser/nvim-spider" } -- lazy.nvim { "chrisgrieser/nvim-spider", lazy = true }, -- vim-plug Plug("chrisgrieser/nvim-spider")No keybindings are created by default. Below are the mappings to replace the default w, e, b, and ge motions with this plugin's version of them.
vim.keymap.set({ "n", "o", "x" }, "w", "<cmd>lua require('spider').motion('w')<CR>") vim.keymap.set({ "n", "o", "x" }, "e", "<cmd>lua require('spider').motion('e')<CR>") vim.keymap.set({ "n", "o", "x" }, "b", "<cmd>lua require('spider').motion('b')<CR>") vim.keymap.set({ "n", "o", "x" }, "ge", "<cmd>lua require('spider').motion('ge')<CR>") -- OR: lazy-load on keystroke (lazy.nvim) { "chrisgrieser/nvim-spider", keys = { { "w", "<cmd>lua require('spider').motion('w')<CR>", mode = { "n", "o", "x" } }, { "e", "<cmd>lua require('spider').motion('e')<CR>", mode = { "n", "o", "x" } }, { "b", "<cmd>lua require('spider').motion('b')<CR>", mode = { "n", "o", "x" } }, { "ge", "<cmd>lua require('spider').motion('ge')<CR>", mode = { "n", "o", "x" } }, }, },Note
For dot-repeat to work, you have to call the motions as Ex-commands. Dot-repeat will not work when using function() require("spider").motion("w") end as third argument.
The .setup() call is optional.
-- default values require("spider").setup { skipInsignificantPunctuation = true, subwordMovement = true, consistentOperatorPending = false, -- see the README for details customPatterns = {}, -- see the README for details }You can also pass the configuration table to the .motion() function:
require("spider").motion("w", { skipInsignificantPunctuation = false })Any options passed to .motion() take precedence over the options set in .setup().
You can use the customPatterns table to define custom movement patterns.
- They must be lua patterns.
- If multiple patterns are given, the motion searches for all of them and stops at the closest one. When there is no match, the search continues in the next line.
- The
customPatternsoption overridesnvim-spider's default behavior, meaning no subword movement and skipping of punctuation. Pass apatterntable and setoverrideDefault = falseto extendnvim-spider's default behavior with a new pattern. - You can use
customPatternsin the.motioncall to create new motions, while still having accessnvim-spider's default behavior. - They must be symmetrical (work the same backwards and forwards) to work for the backwards and forwards motions. If your patterns are not symmetric, you must define them for each direction via
.motion.
A few examples:
-- The motion stops only at numbers. require("spider").motion("w", { customPatterns = { "%d+" }, }) -- The motion stops at any occurrence of the letters "A" or "C", in addition -- to spider's default behavior. require("spider").motion("w", { customPatterns = { patterns = { "A", "C" }, overrideDefault = false, }, }) -- The motion stops at the next declaration of a javascript variable. -- (The `e` motion combined with the `.` matching any character in -- lua patterns ensures that you stop at beginning of the variable name.) require("spider").motion("e", { customPatterns = { "const .", "let .", "var ." }, })Support for special characters requires utf-8 support via the luautf8 rock.
-- lazy.nvim return { { "chrisgrieser/nvim-spider", lazy = true }, { "vhyrro/luarocks.nvim", priority = 1000, -- high priority required, luarocks.nvim should run as the first plugin in your config lazy = false, opts = { rocks = { "luautf8" } -- specifies a list of rocks to install }, }, }-- packer { "chrisgrieser/nvim-spider", rocks = "luautf8" }For troubleshooting issues with utf-8 support, refer to the alternative solutions described in the following issues:
Note
CJK characters still have some issues.
This plugin supports w, e, and b in operator-pending mode, but does not include a subword variant of iw. For a version of iw that considers camelCase, check out the subword text object of nvim-various-textobjs.
In operator pending mode, vim's web motions are actually a bit inconsistent. For instance, cw will change to the end of a word instead of the start of the next word, like dw does. This is probably done for convenience in vi's early days before there were text objects. In my view, this is quite problematic since it makes people habitualize inconsistent motion behavior. In addition, such a behavior can create unexpected results when used in subwords or near punctuation.
Therefore, nvim-spider deliberately does not implement that cw behavior. If you nevertheless prefer cw to behave that way, you can achieve that by mapping cw to ce:
vim.keymap.set("o", "w", "<cmd>lua require('spider').motion('w')<CR>") vim.keymap.set("n", "cw", "ce", { remap = true }) -- OR in one mapping vim.keymap.set("n", "cw", "c<cmd>lua require('spider').motion('e')<CR>")Vim has more inconsistencies related to how the motion range is interpreted (see :h exclusive). For example, if the end of the motion is at the beginning of a line, the endpoint is moved to the last character of the previous line.
foo bar -- ^ bazTyping dw deletes only bar. baz stays on the next line.
Similarly, if the start of the motion is before or at the first non-blank character in a line, and the end is at the beginning of a line, the motion is changed to linewise.
foo -- ^ barTyping yw yanks foo\r, that is, the indentation before the cursor is included, and the register type is set to linewise.
Setting consistentOperatorPending = true removes these special cases. In the first example, bar\r would be deleted charwise. In the second example, foo\r would be yanked charwise.
Caveats
- Last visual selection marks (
`[and`]) are updated and point to the endpoints of the motion. This was not always the case before. - Forced blockwise motion may be canceled if it cannot be correctly represented with the current
selectionoption.
Simply wrap the normal mode motions in <Esc>l and i. (Drop the l on backwards motions.)
vim.keymap.set("i", "<C-f>", "<Esc>l<cmd>lua require('spider').motion('w')<CR>i") vim.keymap.set("i", "<C-b>", "<Esc><cmd>lua require('spider').motion('b')<CR>i")You can use precognition.nvim with nvim-spider to get hints for the w, e, and b motions. (precognition does not support hints for multi-character motions, thus ge is not supported.)
nvim-spider automatically registers the precognition adapter on calling require("spider").setup().
-- lazy.nvim return { { "tris203/precognition.nvim", dependencies = { "chrisgrieser/nvim-spider" }, opts = {}, }, { "chrisgrieser/nvim-spider", keys = { { "w", "<cmd>lua require('spider').motion('w')<CR>", mode = { "n", "o", "x" } }, { "e", "<cmd>lua require('spider').motion('e')<CR>", mode = { "n", "o", "x" } }, { "b", "<cmd>lua require('spider').motion('b')<CR>", mode = { "n", "o", "x" } }, }, opts = {}, -- calls `setup()`, which registers the precognition adapter }, }Thanks
@vypxland@ii14for figuring out dot-repeatability of textobjects.@vanaigrfor a large contribution regarding operator-pending mode.
About the developer
In my day job, I am a sociologist studying the social mechanisms underlying the digital economy. For my PhD project, I investigate the governance of the app economy and how software ecosystems manage the tension between innovation and compatibility. If you are interested in this subject, feel free to get in touch.
If you find this project helpful, you can support me via 🩷 GitHub Sponsors.