Skip to content

Commit dfbf959

Browse files
authored
fix(repeatable_move): using ; or , in operator-pending mode misses one character. ci: fix broken test (#833)
Fixes #699 properly. Also fixes broken tests. I personally found it very hard to test the CI, because it's hard to test locally due to the fact that all parsers are installed into `~/.local/share/nvim`. Thus, I think it's good to sandbox the data into `.test-deps` in `minimal_init.lua`. Also I believe that lint-docs should be a separate CI that opens a PR of a new updated docs.
1 parent 63c4dce commit dfbf959

File tree

7 files changed

+370
-285
lines changed

7 files changed

+370
-285
lines changed

.github/workflows/test.yml

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ name: Tests
33
on:
44
push:
55
branches:
6-
- "main"
6+
- 'main'
77
pull_request:
88
branches:
9-
- "main"
9+
- 'main'
1010
workflow_dispatch:
1111

1212
concurrency:
@@ -47,6 +47,7 @@ jobs:
4747
ref: main
4848
path: ${{ env.NVIM_TS_DIR }}
4949

50+
# FIXME: cache not functioning because of wrong key.
5051
- name: Setup Parsers Cache
5152
id: parsers-cache
5253
uses: actions/cache@v4
@@ -62,10 +63,55 @@ jobs:
6263
- name: Lint docs
6364
run: |
6465
make docs
65-
git diff --exit-code
66+
67+
if ! git diff --quiet; then
68+
echo "::group::Docs diff"
69+
git status --short
70+
git diff
71+
echo "::endgroup::"
72+
73+
changed_files=$(git diff --name-only)
74+
75+
{
76+
echo "## Documentation needs to be updated"
77+
echo
78+
echo "CI regenerated documentation and found differences with what is committed."
79+
echo
80+
echo "You do **not** need to run \`make docs\` locally."
81+
echo
82+
echo "### How to fix this"
83+
echo "1. Go to the **Summary** tab in this workflow run."
84+
echo "2. Copy the updated content."
85+
echo "3. Paste it into the corresponding file in your branch."
86+
echo "4. Commit and push the changes."
87+
echo
88+
echo "### Generated content"
89+
for f in $changed_files; do
90+
echo
91+
echo "#### \`$f\`"
92+
echo
93+
echo '```'
94+
cat "$f"
95+
echo '```'
96+
done
97+
echo
98+
echo "### Git diff (patch-style)"
99+
echo
100+
echo "You can also apply these changes using a patch:"
101+
echo
102+
echo '```diff'
103+
git diff
104+
echo '```'
105+
} >> "$GITHUB_STEP_SUMMARY"
106+
107+
echo "::error::Documentation is out of date. Copy the generated docs from the job summary into your branch and commit them."
108+
exit 1
109+
fi
66110
67111
- name: Check queries
112+
if: ${{ always() }}
68113
run: make checkquery
69114

70-
# - name: Run tests
71-
# run: make tests
115+
- name: Run tests
116+
if: ${{ always() }}
117+
run: make tests

BUILTIN_TEXTOBJECTS.md

Lines changed: 191 additions & 0 deletions
Large diffs are not rendered by default.

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ checkquery: $(TSQUERYLS)
121121

122122
.PHONY: docs
123123
docs: $(NVIM) $(NVIM_TS)
124-
NVIM_TS=$(NVIM_TS) $(NVIM_BIN) -l scripts/update-readme.lua
124+
NVIM_TS=$(NVIM_TS) $(NVIM_BIN) -l scripts/update-builtin-textobjects.lua
125125

126126
.PHONY: tests
127-
tests: $(NVIM) $(PLENARY)
128-
PLENARY=$(PLENARY) $(NVIM_BIN) --headless --clean -u scripts/minimal_init.lua \
129-
-c "PlenaryBustedDirectory tests/$(TESTS) { minimal_init = './scripts/minimal_init.lua' }"
127+
tests: $(NVIM) $(PLENARY) $(NVIM_TS)
128+
NVIM_TS=$(NVIM_TS) PLENARY=$(PLENARY) $(NVIM_BIN) --headless --clean -u scripts/minimal_init.lua \
129+
-c "PlenaryBustedDirectory tests { minimal_init = './scripts/minimal_init.lua' }"
130130

131131
.PHONY: all
132132
all: lua query docs tests

README.md

Lines changed: 1 addition & 187 deletions
Large diffs are not rendered by default.

lua/nvim-treesitter-textobjects/repeatable_move.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,31 @@ M.make_repeatable_move = function(move_fn)
2626
end
2727
end
2828

29+
--- Enter visual mode (nov) if operator-pending (no) mode (fixes #699)
30+
--- Why? According to https://learnvimscriptthehardway.stevelosh.com/chapters/15.html
31+
--- If your operator-pending mapping ends with some text visually selected, Vim will operate on that text.
32+
--- Otherwise, Vim will operate on the text between the original cursor position and the new position.
33+
local function force_operator_pending_visual_mode()
34+
local mode = vim.api.nvim_get_mode()
35+
if mode.mode == 'no' then
36+
vim.cmd.normal({ 'v', bang = true })
37+
end
38+
end
39+
2940
---@param opts_extend TSTextObjects.MoveOpts?
3041
M.repeat_last_move = function(opts_extend)
3142
if not M.last_move then
3243
return
3344
end
3445
local opts = vim.tbl_deep_extend('force', M.last_move.opts, opts_extend or {})
3546
if M.last_move.func == 'f' or M.last_move.func == 't' then
47+
force_operator_pending_visual_mode()
3648
vim.cmd([[normal! ]] .. vim.v.count1 .. (opts.forward and ';' or ','))
3749
elseif M.last_move.func == 'F' or M.last_move.func == 'T' then
50+
force_operator_pending_visual_mode()
3851
vim.cmd([[normal! ]] .. vim.v.count1 .. (opts.forward and ',' or ';'))
3952
else
53+
-- we assume other textobjects (move) already handle operator-pending mode correctly
4054
M.last_move.func(opts, unpack(M.last_move.additional_args))
4155
end
4256
end

scripts/minimal_init.lua

Lines changed: 99 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
1-
vim.opt.runtimepath:append(os.getenv('PLENARY'))
1+
--- Usage:
2+
--- 1. Put plenary.nvim and nvim-treesitter in {repo_root}/.test-deps/
3+
--- 2. in repo root, run `nvim -u scripts/minimal_init.lua`
4+
--- or, just run `make tests`
5+
local test_root = '.test-deps'
6+
for _, name in ipairs({ 'config', 'data', 'state', 'cache' }) do
7+
vim.env[('XDG_%s_HOME'):format(name:upper())] = test_root .. '/' .. name
8+
end
9+
vim.opt.runtimepath:append(os.getenv('PLENARY') or (test_root .. '/plenary.nvim'))
10+
vim.opt.runtimepath:append(os.getenv('NVIM_TS') or (test_root .. '/nvim-treesitter'))
211
vim.opt.runtimepath:append('.')
3-
vim.cmd.runtime({ 'plugin/query_predicates.lua', bang = true })
12+
13+
require('nvim-treesitter').install({ 'python' }):wait(300000) -- wait max. 5 minutes
14+
15+
local tsstart = vim.api.nvim_create_augroup('tsstart', { clear = true })
16+
vim.api.nvim_create_autocmd('FileType', {
17+
pattern = '*',
18+
callback = function(args)
19+
-- Try to start Tree-sitter, but don’t error if there’s no parser
20+
vim.treesitter.start(args.buf)
21+
-- Important: ensure the parser is actually parsed.
22+
-- without this, the textobject may not do anything in headless mode
23+
vim.treesitter.get_parser(0, vim.bo[args.buf].filetype):parse()
24+
end,
25+
group = tsstart,
26+
})
427

528
require('nvim-treesitter-textobjects').setup({
629
select = {
@@ -17,77 +40,76 @@ require('nvim-treesitter-textobjects').setup({
1740

1841
local select = require('nvim-treesitter-textobjects.select')
1942

20-
for _, mode in ipairs({ 'x', 'o' }) do
21-
vim.keymap.set(mode, 'am', function()
22-
select.select_textobject('@function.outer', 'textobjects')
23-
end)
24-
vim.keymap.set(mode, 'im', function()
25-
select.select_textobject('@function.inner', 'textobjects')
26-
end)
27-
vim.keymap.set(mode, 'al', function()
28-
select.select_textobject('@class.outer', 'textobjects')
29-
end)
30-
vim.keymap.set(mode, 'il', function()
31-
select.select_textobject('@class.inner', 'textobjects')
32-
end)
33-
vim.keymap.set(mode, 'ab', function()
34-
select.select_textobject('@block.outer', 'textobjects')
35-
end)
36-
vim.keymap.set(mode, 'ib', function()
37-
select.select_textobject('@block.inner', 'textobjects')
38-
end)
39-
vim.keymap.set(mode, 'ad', function()
40-
select.select_textobject('@conditional.outer', 'textobjects')
41-
end)
42-
vim.keymap.set(mode, 'id', function()
43-
select.select_textobject('@conditional.inner', 'textobjects')
44-
end)
45-
vim.keymap.set(mode, 'ao', function()
46-
select.select_textobject('@loop.outer', 'textobjects')
47-
end)
48-
vim.keymap.set(mode, 'io', function()
49-
select.select_textobject('@loop.inner', 'textobjects')
50-
end)
51-
vim.keymap.set(mode, 'aa', function()
52-
select.select_textobject('@parameter.outer', 'textobjects')
53-
end)
54-
vim.keymap.set(mode, 'ia', function()
55-
select.select_textobject('@parameter.inner', 'textobjects')
56-
end)
57-
vim.keymap.set(mode, 'af', function()
58-
select.select_textobject('@call.outer', 'textobjects')
59-
end)
60-
vim.keymap.set(mode, 'if', function()
61-
select.select_textobject('@call.inner', 'textobjects')
62-
end)
63-
vim.keymap.set(mode, 'ac', function()
64-
select.select_textobject('@comment.outer', 'textobjects')
65-
end)
66-
vim.keymap.set(mode, 'ar', function()
67-
select.select_textobject('@frame.outer', 'textobjects')
68-
end)
69-
vim.keymap.set(mode, 'ir', function()
70-
select.select_textobject('@frame.inner', 'textobjects')
71-
end)
72-
vim.keymap.set(mode, 'at', function()
73-
select.select_textobject('@attribute.outer', 'textobjects')
74-
end)
75-
vim.keymap.set(mode, 'it', function()
76-
select.select_textobject('@attribute.inner', 'textobjects')
77-
end)
78-
vim.keymap.set(mode, 'ae', function()
79-
select.select_textobject('@scopename.inner', 'textobjects')
80-
end)
81-
vim.keymap.set(mode, 'ie', function()
82-
select.select_textobject('@scopename.inner', 'textobjects')
83-
end)
84-
vim.keymap.set(mode, 'as', function()
85-
select.select_textobject('@statement.outer', 'textobjects')
86-
end)
87-
vim.keymap.set(mode, 'is', function()
88-
select.select_textobject('@statement.outer', 'textobjects')
89-
end)
90-
end
43+
local mode = { 'x', 'o' }
44+
vim.keymap.set(mode, 'am', function()
45+
select.select_textobject('@function.outer', 'textobjects')
46+
end)
47+
vim.keymap.set(mode, 'im', function()
48+
select.select_textobject('@function.inner', 'textobjects')
49+
end)
50+
vim.keymap.set(mode, 'al', function()
51+
select.select_textobject('@class.outer', 'textobjects')
52+
end)
53+
vim.keymap.set(mode, 'il', function()
54+
select.select_textobject('@class.inner', 'textobjects')
55+
end)
56+
vim.keymap.set(mode, 'ab', function()
57+
select.select_textobject('@block.outer', 'textobjects')
58+
end)
59+
vim.keymap.set(mode, 'ib', function()
60+
select.select_textobject('@block.inner', 'textobjects')
61+
end)
62+
vim.keymap.set(mode, 'ad', function()
63+
select.select_textobject('@conditional.outer', 'textobjects')
64+
end)
65+
vim.keymap.set(mode, 'id', function()
66+
select.select_textobject('@conditional.inner', 'textobjects')
67+
end)
68+
vim.keymap.set(mode, 'ao', function()
69+
select.select_textobject('@loop.outer', 'textobjects')
70+
end)
71+
vim.keymap.set(mode, 'io', function()
72+
select.select_textobject('@loop.inner', 'textobjects')
73+
end)
74+
vim.keymap.set(mode, 'aa', function()
75+
select.select_textobject('@parameter.outer', 'textobjects')
76+
end)
77+
vim.keymap.set(mode, 'ia', function()
78+
select.select_textobject('@parameter.inner', 'textobjects')
79+
end)
80+
vim.keymap.set(mode, 'af', function()
81+
select.select_textobject('@call.outer', 'textobjects')
82+
end)
83+
vim.keymap.set(mode, 'if', function()
84+
select.select_textobject('@call.inner', 'textobjects')
85+
end)
86+
vim.keymap.set(mode, 'ac', function()
87+
select.select_textobject('@comment.outer', 'textobjects')
88+
end)
89+
vim.keymap.set(mode, 'ar', function()
90+
select.select_textobject('@frame.outer', 'textobjects')
91+
end)
92+
vim.keymap.set(mode, 'ir', function()
93+
select.select_textobject('@frame.inner', 'textobjects')
94+
end)
95+
vim.keymap.set(mode, 'at', function()
96+
select.select_textobject('@attribute.outer', 'textobjects')
97+
end)
98+
vim.keymap.set(mode, 'it', function()
99+
select.select_textobject('@attribute.inner', 'textobjects')
100+
end)
101+
vim.keymap.set(mode, 'ae', function()
102+
select.select_textobject('@scopename.inner', 'textobjects')
103+
end)
104+
vim.keymap.set(mode, 'ie', function()
105+
select.select_textobject('@scopename.inner', 'textobjects')
106+
end)
107+
vim.keymap.set(mode, 'as', function()
108+
select.select_textobject('@statement.outer', 'textobjects')
109+
end)
110+
vim.keymap.set(mode, 'is', function()
111+
select.select_textobject('@statement.outer', 'textobjects')
112+
end)
91113

92114
-- swap
93115
local swap = require('nvim-treesitter-textobjects.swap')
@@ -400,12 +422,12 @@ local ts_repeat_move = require('nvim-treesitter-textobjects.repeatable_move')
400422

401423
-- Repeat movement with ; and ,
402424
-- ensure ; goes forward and , goes backward regardless of the last direction
403-
vim.keymap.set({ 'n', 'x', 'o' }, ';', ts_repeat_move.repeat_last_move_next)
404-
vim.keymap.set({ 'n', 'x', 'o' }, ',', ts_repeat_move.repeat_last_move_previous)
425+
-- vim.keymap.set({ 'n', 'x', 'o' }, ';', ts_repeat_move.repeat_last_move_next)
426+
-- vim.keymap.set({ 'n', 'x', 'o' }, ',', ts_repeat_move.repeat_last_move_previous)
405427

406428
-- vim way: ; goes to the direction you were moving.
407-
-- vim.keymap.set({ 'n', 'x', 'o' }, ';', ts_repeat_move.repeat_last_move)
408-
-- vim.keymap.set({ 'n', 'x', 'o' }, ',', ts_repeat_move.repeat_last_move_opposite)
429+
vim.keymap.set({ 'n', 'x', 'o' }, ';', ts_repeat_move.repeat_last_move)
430+
vim.keymap.set({ 'n', 'x', 'o' }, ',', ts_repeat_move.repeat_last_move_opposite)
409431

410432
-- Optionally, make builtin f, F, t, T also repeatable with ; and ,
411433
vim.keymap.set({ 'n', 'x', 'o' }, 'f', ts_repeat_move.builtin_f_expr, { expr = true })
Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
-- Execute as `nvim --headless -c "luafile ./scripts/update-readme.lua"`
2-
vim.opt.runtimepath:append(os.getenv('NVIM_TS'))
2+
local test_root = '.test-deps'
3+
for _, name in ipairs({ 'config', 'data', 'state', 'cache' }) do
4+
vim.env[('XDG_%s_HOME'):format(name:upper())] = test_root .. '/' .. name
5+
end
6+
vim.opt.runtimepath:append(os.getenv('NVIM_TS') or (test_root .. '/nvim-treesitter'))
37
vim.opt.runtimepath:append('.')
48

59
local parsers = require('nvim-treesitter.parsers')
@@ -67,17 +71,11 @@ for _, v in ipairs(sorted_parsers) do
6771
end
6872
generated_text = generated_text .. '</table>\n'
6973

70-
local readme_text = table.concat(vim.fn.readfile('README.md'), '\n')
71-
72-
local new_readme_text = string.gsub(
73-
readme_text,
74-
'<!%-%-textobjectinfo%-%->.*<!%-%-textobjectinfo%-%->',
75-
'<!--textobjectinfo-->\n' .. generated_text .. '<!--textobjectinfo-->'
76-
)
77-
vim.fn.writefile(vim.fn.split(new_readme_text, '\n'), 'README.md')
74+
local prev_builtin_textobjects_text = table.concat(vim.fn.readfile('BUILTIN_TEXTOBJECTS.md'), '\n')
75+
vim.fn.writefile(vim.fn.split(generated_text, '\n'), 'BUILTIN_TEXTOBJECTS.md')
7876

79-
if string.find(readme_text, generated_text, 1, true) then
80-
print('README.md is up-to-date\n')
77+
if string.find(prev_builtin_textobjects_text, generated_text, 1, true) then
78+
print('BUILTIN_TEXTOBJECTS.md is up-to-date\n')
8179
else
82-
print('New README.md was written\n')
80+
print('New BUILTIN_TEXTOBJECTS.md was written\n')
8381
end

0 commit comments

Comments
 (0)