commit 1cde71233fcf972c5a4ddd155278af299527bb9f
parent 6d2330f50d07dd2d3c4c01ee37f918b5d320d998
Author: Sean Dewar <6256228+seandewar@users.noreply.github.com>
Date: Sat, 20 Dec 2025 14:00:10 +0000
fix(autocmd): skip empty comma-separated patterns properly
Problem: empty comma-separated patterns in an aupat aren't skipped correctly.
Solution: skip consecutive commas in an aupat.
Also simplify the logic.
Diffstat:
3 files changed, 71 insertions(+), 48 deletions(-)
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c
@@ -823,12 +823,10 @@ static Array get_patterns_from_pattern_or_buf(Object pattern, bool has_buffer, B
if (pattern.type != kObjectTypeNil) {
if (pattern.type == kObjectTypeString) {
const char *pat = pattern.data.string.data;
- size_t patlen = aucmd_pattern_length(pat);
+ size_t patlen = aucmd_span_pattern(pat, &pat);
while (patlen) {
kvi_push(patterns, CBUF_TO_ARENA_OBJ(arena, pat, patlen));
-
- pat = aucmd_next_pattern(pat, patlen);
- patlen = aucmd_pattern_length(pat);
+ patlen = aucmd_span_pattern(pat + patlen, &pat);
}
} else if (pattern.type == kObjectTypeArray) {
if (!check_string_array(pattern.data.array, "pattern", true, err)) {
@@ -838,12 +836,10 @@ static Array get_patterns_from_pattern_or_buf(Object pattern, bool has_buffer, B
Array array = pattern.data.array;
FOREACH_ITEM(array, entry, {
const char *pat = entry.data.string.data;
- size_t patlen = aucmd_pattern_length(pat);
+ size_t patlen = aucmd_span_pattern(pat, &pat);
while (patlen) {
kvi_push(patterns, CBUF_TO_ARENA_OBJ(arena, pat, patlen));
-
- pat = aucmd_next_pattern(pat, patlen);
- patlen = aucmd_pattern_length(pat);
+ patlen = aucmd_span_pattern(pat + patlen, &pat);
}
})
} else {
diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c
@@ -165,7 +165,7 @@ static void au_show_for_event(int group, event_T event, const char *pat)
// Empty pattern shows all autocommands for this event
int patlen = 0;
if (*pat != NUL) {
- patlen = (int)aucmd_pattern_length(pat);
+ patlen = (int)aucmd_span_pattern(pat, &pat);
if (patlen == 0) { // Don't show if it contains only commas
return;
}
@@ -288,8 +288,7 @@ static void au_show_for_event(int group, event_T event, const char *pat)
}
}
- pat = aucmd_next_pattern(endpat, 0);
- patlen = (int)aucmd_pattern_length(pat);
+ patlen = (int)aucmd_span_pattern(endpat, &pat);
} while (patlen);
}
@@ -920,7 +919,7 @@ int do_autocmd_event(event_T event, const char *pat, bool once, int nested, cons
}
// Loop through all the specified patterns.
- int patlen = (int)aucmd_pattern_length(pat);
+ int patlen = (int)aucmd_span_pattern(pat, &pat);
while (patlen) {
const char *endpat = pat + patlen;
@@ -966,8 +965,7 @@ int do_autocmd_event(event_T event, const char *pat, bool once, int nested, cons
autocmd_register(0, event, pat, patlen, group, once, nested, NULL, cmd, &handler_fn);
}
- pat = aucmd_next_pattern(endpat, 0);
- patlen = (int)aucmd_pattern_length(pat);
+ patlen = (int)aucmd_span_pattern(endpat, &pat);
}
au_cleanup(); // may really delete removed patterns/commands now
@@ -1105,46 +1103,28 @@ int autocmd_register(int64_t id, event_T event, const char *pat, int patlen, int
return OK;
}
-size_t aucmd_pattern_length(const char *pat)
- FUNC_ATTR_PURE
+size_t aucmd_span_pattern(const char *pat, const char **start)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
- if (*pat == NUL) {
- return 0;
+ // Skip leading commas.
+ while (*pat == ',') {
+ pat++;
}
- const char *endpat;
-
- for (; *pat; pat = endpat + 1) {
- // Find end of the pattern.
- // Watch out for a comma in braces, like "*.\{obj,o\}".
- endpat = pat;
- // ignore single comma
- if (*endpat == ',') {
- continue;
+ // Find end of the pattern.
+ // Watch out for a comma in braces, like "*.\{obj,o\}".
+ const char *p = pat;
+ int brace_level = 0;
+ for (; *p && (*p != ',' || brace_level || (p > pat && p[-1] == '\\')); p++) {
+ if (*p == '{') {
+ brace_level++;
+ } else if (*p == '}') {
+ brace_level--;
}
- int brace_level = 0;
- for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\'); endpat++) {
- if (*endpat == '{') {
- brace_level++;
- } else if (*endpat == '}') {
- brace_level--;
- }
- }
-
- return (size_t)(endpat - pat);
}
- return strlen(pat);
-}
-
-const char *aucmd_next_pattern(const char *pat, size_t patlen)
- FUNC_ATTR_PURE
-{
- pat = pat + patlen;
- if (*pat == ',') {
- pat = pat + 1;
- }
- return pat;
+ *start = pat;
+ return (size_t)(p - pat);
}
/// Implementation of ":doautocmd [group] event [fname]".
diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua
@@ -800,4 +800,51 @@ describe('autocmd', function()
fn.execute('autocmd BufEnter <buffer=1>,bar,bar,foo,foo,<buffer>,<buffer=6>,<buffer>')
)
end)
+
+ it('parses empty comma-delimited patterns correctly', function()
+ exec [[
+ autocmd User , "
+ autocmd User ,, "
+ autocmd User ,,according,to,,all,known,,,laws,, "
+ ]]
+ api.nvim_create_autocmd('User', { pattern = ',,of,,,aviation,,,,there,,', command = '' })
+ api.nvim_create_autocmd('User', {
+ pattern = { ',,,,is,,no', ',,way,,,', 'a,,bee{,should be, able to,},fly' },
+ command = '',
+ })
+ eq(
+ {
+ 'according',
+ 'to',
+ 'all',
+ 'known',
+ 'laws',
+ 'of',
+ 'aviation',
+ 'there',
+ 'is',
+ 'no',
+ 'way',
+ 'a',
+ 'bee{,should be, able to,}',
+ 'fly',
+ },
+ exec_lua(function()
+ return vim.tbl_map(function(v)
+ return v.pattern
+ end, vim.api.nvim_get_autocmds({ event = 'User' }))
+ end)
+ )
+ eq(
+ dedent([[
+
+ --- Autocommands ---
+ User
+ there
+ is
+ a
+ fly]]),
+ fn.execute('autocmd User ,,,there,is,,a,fly,,')
+ )
+ end)
end)