Repository: neovim/neovim
Pull request #33605: feat(glob): New Glob Implementation Based on Peglob
Author: login: brynne8
Labels: name: lua
description: stdlib
name: lsp
Created: 2025-04-24T09:05:42Z | Merged: 2025-05-22T07:24:50Z

This is the glob implementation mentioned in [Discussion #33570](https://github.com/neovim/neovim/discussions/33570). It’s intended to replace `vim.glob.to_lpeg`, while still using LPeg internally.

The implementation aims to adhere closely to the LSP 3.17.0 specification, providing a stricter and more intuitive interpretation where the spec allows for it. This enhances consistency, correctness, and performance, without adversely affecting common usage patterns.

## Key Behavior Interpretations and Clarifications (LSP 3.17):

The following points detail how this implementation interprets specific glob patterns, addressing previous test case discrepancies. These interpretations aim for greater predictability and robustness, remaining within or clarifying the scope of the LSP 3.17 spec:

- [x] **`match('*', '')` results in `true`**
    *   **LSP 3.17 Spec:** States `*` matches "one or more characters in a path segment."
    *   **Resolution & Clarification:** While the literal spec text says "one or more," this has been identified as a discrepancy with common glob behavior and VS Code's own implementation. This implementation aligns with the clarified understanding (see [LSP PR #2132](https://github.com/microsoft/language-server-protocol/pull/2132)) that `*` matches **zero or more characters**. The test has been updated accordingly.

- [x] **`match('a**', 'abc')` results in `false`**
    *   **LSP 3.17 Spec:** States `**` matches "any number of path segments, including none."
    *   **Interpretation:** This implementation adopts a stricter interpretation focusing on `**`'s role in matching *complete path segments*. Patterns like `a**` (where `**` is not clearly delineating path segments, i.e., not separated by slashes and adjacent to other characters within the same potential segment) are considered an ill-formed use of `**` by this parser. While the spec allows `**` to match "none" (zero segments), this implementation prioritizes well-defined segment boundaries for `**` usage to maintain a clear distinction from `*` (in-segment wildcard) and avoid ambiguity.

- [x] **`match('**/', '/a/b/c')` results in `false`**
    *   **LSP 3.17 Spec:** Defines `**` to match "any number of path segments, including none." It does not explicitly detail the interaction of a trailing `/` in the pattern with inputs not ending in `/`.
    *   **Interpretation:** The pattern `**/` is interpreted to require the input to also end with a path separator for a match, as the pattern explicitly includes it. Since the input `/a/b/c` ends with a character (`c`), it does not match. This behavior aligns with a direct reading of the pattern and is consistent with @stevearc's comment: "I agree that if we look purely at the spec, I would _not_ expect these to match."

- [x] **`match('{}', '')` and `match('{a}', 'a')` are not treated as valid glob alternations**
    *   **LSP 3.17 Spec:** States `{}` is "to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)." The example provided uses a comma. The spec does not explicitly define behavior for `{}` or `{a}` (without commas) as alternation mechanisms.
    *   **Interpretation:** For braces `{}` to signify alternation, this implementation requires them to contain **at least one comma** (e.g., `{a,b}`, `{,a}`). This aligns with common brace expansion definitions (e.g., Rosetta Code) and ensures the "group conditions" feature for alternation is unambiguous. Patterns like `{}` or `{a}` (without a comma) are not considered valid glob alternation syntax by this parser. (@stevearc acknowledged the spec doesn't enforce contents for `{}`, viewing it as an edge case). This stricter approach enhances predictability.

- [x] **`match('{a,{b,c}}', 'c')` results in `true`**
    *   **LSP 3.17 Spec:** States `{}` is "to group conditions." It does not explicitly mention or prohibit nesting of these groups.
    *   **Interpretation:** Nested braces are supported as a natural extension of "grouping conditions." This is a common and intuitive behavior for brace expansion. As @stevearc commented, "The spec doesn't explicitly mention nesting, but I agree with the interpretation that this pattern _should_ match." Supporting nesting enhances the utility of brace grouping.

---

These interpretations aim to make glob patterns more robust and easier to reason about, while staying consistent with the LSP 3.17 specification's intent and addressing areas where it is underspecified or has known discrepancies. The tests previously commented out have been updated to reflect these behaviors.

This new implementation also demonstrates significant improvements in correctness and performance compared to the previous `vim.glob.to_lpeg`, as shown by extensive testing (see discussion for detailed test results).

PS: The heatmaps for correctness of glob matching proves that Peglob is great at accuracy and precision against glob test suite from Bun.js. (fig 5 in paper)
![img](https://github.com/user-attachments/assets/d0263d49-1378-49e1-ab33-b9d396dff24f)

Relevant documentation, code formatting, and test adjustments have been made based on review feedback.


Diff:
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index 926295b2267977..26b591d077feb1 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -3236,30 +3236,51 @@ vim.fs.root({source}, {marker})                                *vim.fs.root()*
 ==============================================================================
 Lua module: vim.glob                                                *vim.glob*
 
-vim.glob.to_lpeg({pattern})                               *vim.glob.to_lpeg()*
-    Parses a raw glob into an |lua-lpeg| pattern.
+Glob-to-LPeg Converter (Peglob) This module converts glob patterns to LPeg
+patterns according to the LSP 3.17 specification:
+https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
+
+Glob grammar overview:
+• `*` to match zero or more characters in a path segment
+• `?` to match on one character in a path segment
+• `**` to match any number of path segments, including none
+• `{}` to group conditions (e.g. `*.{ts,js}` matches TypeScript and JavaScript
+  files)
+• `[]` to declare a range of characters to match in a path segment (e.g.,
+  `example.[0-9]` to match on `example.0`, `example.1`, …)
+• `[!...]` to negate a range of characters to match in a path segment (e.g.,
+  `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+
+Additional constraints:
+• A Glob pattern must match an entire path, with partial matches considered
+  failures.
+• The pattern only determines success or failure, without specifying which
+  parts correspond to which characters.
+• A path segment is the portion of a path between two adjacent path separators
+  (`/`), or between the start/end of the path and the nearest separator.
+• The `**` (globstar) pattern matches zero or more path segments, including
+  intervening separators (`/`). Within pattern strings, `**` must be delimited
+  by path separators (`/`) or pattern boundaries and cannot be adjacent to any
+  characters other than `/`. If `**` is not the final element, it must be
+  followed by `/`.
+• `{}` (braced conditions) contains valid Glob patterns as branches, separated
+  by commas. Commas are exclusively used for separating branches and cannot
+  appear within a branch for any other purpose. Nested `{}` structures are
+  allowed, but `{}` must contain at least two branches—zero or one branch is
+  not permitted.
+• In `[]` or `[!...]`, a character range consists of character intervals
+  (e.g., `a-z`) or individual characters (e.g., `w`). A range including `/`
+  won’t match that character.
 
-    This uses glob semantics from LSP 3.17.0:
-    https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
 
-    Glob patterns can have the following syntax:
-    • `*` to match one or more characters in a path segment
-    • `?` to match on one character in a path segment
-    • `**` to match any number of path segments, including none
-    • `{}` to group conditions (e.g. `*.{ts,js}` matches TypeScript and
-      JavaScript files)
-    • `[]` to declare a range of characters to match in a path segment (e.g.,
-      `example.[0-9]` to match on `example.0`, `example.1`, …)
-    • `[!...]` to negate a range of characters to match in a path segment
-      (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not
-      `example.0`)
+vim.glob.to_lpeg({pattern})                               *vim.glob.to_lpeg()*
+    Parses a raw glob into an |lua-lpeg| pattern.
 
     Parameters: ~
       • {pattern}  (`string`) The raw glob pattern
 
     Return: ~
-        (`vim.lpeg.Pattern`) pattern An |lua-lpeg| representation of the
-        pattern
+        (`vim.lpeg.Pattern`) An |lua-lpeg| representation of the pattern
 
 
 ==============================================================================
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 3cda63dd5845ee..17bf29f93c43ea 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -175,7 +175,11 @@ OPTIONS
 
 PERFORMANCE
 
-• todo
+• |vim.glob.to_lpeg()| uses a new LPeg-based implementation (Peglob) that
+  provides ~50% speedup for complex patterns. The implementation restores
+  support for nested braces and follows LSP 3.17 specification with
+  additional constraints for improved correctness and resistance to
+  backtracking edge cases.
 
 PLUGINS
 
diff --git a/runtime/lua/vim/glob.lua b/runtime/lua/vim/glob.lua
index 242c70d4b23bfc..740cf7a7d1efba 100644
--- a/runtime/lua/vim/glob.lua
+++ b/runtime/lua/vim/glob.lua
@@ -1,93 +1,375 @@
-local lpeg = vim.lpeg
-local P, S, V, R, B = lpeg.P, lpeg.S, lpeg.V, lpeg.R, lpeg.B
-local C, Cc, Ct, Cf, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
-
-local M = {}
-
-local pathsep = P('/')
-
---- Parses a raw glob into an |lua-lpeg| pattern.
+--- @brief Glob-to-LPeg Converter (Peglob)
+--- This module converts glob patterns to LPeg patterns according to the LSP 3.17 specification:
+--- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
 ---
---- This uses glob semantics from LSP 3.17.0: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
----
---- Glob patterns can have the following syntax:
---- - `*` to match one or more characters in a path segment
+--- Glob grammar overview:
+--- - `*` to match zero or more characters in a path segment
 --- - `?` to match on one character in a path segment
 --- - `**` to match any number of path segments, including none
 --- - `{}` to group conditions (e.g. `*.{ts,js}` matches TypeScript and JavaScript files)
---- - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
---- - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+--- - `[]` to declare a range of characters to match in a path segment
+---   (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+--- - `[!...]` to negate a range of characters to match in a path segment
+---   (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
 ---
----@param pattern string The raw glob pattern
----@return vim.lpeg.Pattern pattern An |lua-lpeg| representation of the pattern
-function M.to_lpeg(pattern)
-  local function class(inv, ranges)
-    local patt = R(unpack(vim.tbl_map(table.concat, ranges)))
-    if inv == '!' then
-      patt = P(1) - patt
+--- Additional constraints:
+--- - A Glob pattern must match an entire path, with partial matches
+---   considered failures.
+--- - The pattern only determines success or failure, without specifying
+---   which parts correspond to which characters.
+--- - A *path segment* is the portion of a path between two adjacent path
+---   separators (`/`), or between the start/end of the path and the nearest
+---   separator.
+--- - The `**` (*globstar*) pattern matches zero or more path segments,
+---   including intervening separators (`/`). Within pattern strings, `**`
+---   must be delimited by path separators (`/`) or pattern boundaries and
+---   cannot be adjacent to any characters other than `/`. If `**` is not
+---   the final element, it must be followed by `/`.
+--- - `{}` (*braced conditions*) contains valid Glob patterns as branches,
+---   separated by commas. Commas are exclusively used for separating
+---   branches and cannot appear within a branch for any other purpose.
+---   Nested `{}` structures are allowed, but `{}` must contain at least two
+---   branches—zero or one branch is not permitted.
+--- - In `[]` or `[!...]`, a *character range* consists of character
+---   intervals (e.g., `a-z`) or individual characters (e.g., `w`). A range
+---   including `/` won’t match that character.
+
+--- @diagnostic disable: missing-fields
+
+local m = vim.lpeg
+local mt = getmetatable(m.P(0))
+local re = vim.re
+local bit = require('bit')
+
+local M = {}
+
+-- Basic patterns for matching glob components
+local letter = m.P(1) - m.S(',*?[]{}/\\') -- Any character except special glob characters
+local slash = m.P '/' * m.Cc(m.P '/') -- Path separator with capture
+local notslash = m.P(1) - m.P '/' -- Any character except path separator
+local notcomma = m.P(1) - m.S(',\\') -- Any character except comma and backslash
+
+--- Handle EOF, considering whether we're in a segment or not
+--- @type vim.lpeg.Pattern
+local eof = -1
+  * m.Cb('inseg')
+  / function(flag)
+    if flag then
+      return #m.P '/'
+    else
+      return m.P(-1)
     end
-    return patt
   end
 
-  local function condlist(conds, after)
-    return vim.iter(conds):fold(P(false), function(acc, cond)
-      return acc + cond * after
-    end)
+---@alias pat_table { F: string?, [1]: string, [2]: vim.lpeg.Pattern }
+---@alias seg_part { [string]: any, [integer]: pat_table }
+
+--- @param p pat_table Initial segment pattern data
+--- @return seg_part Segment structure with start pattern
+local function start_seg(p)
+  return { s = p[2], e = true, n = 0 }
+end
+
+--- @param t seg_part Segment structure
+--- @param p pat_table Pattern to look for
+--- @return table Updated segment structure
+local function lookfor(t, p)
+  t.n = t.n + 1
+  t[t.n] = p
+  return t
+end
+
+--- @param t seg_part Segment structure
+--- @return table Segment structure with end pattern
+local function to_seg_end(t)
+  t.e = notslash ^ 0
+  return t
+end
+
+--- Constructs a segment matching pattern from collected components
+---
+--- @param t seg_part Segment structure with patterns
+--- @return vim.lpeg.Pattern Complete segment match pattern
+local function end_seg(t)
+  --- @type table<any,any>
+  local seg_grammar = { 's' }
+  if t.n > 0 then
+    seg_grammar.s = t.s
+    for i = 1, t.n do
+      local rname = t[i][1]
+      if not seg_grammar[rname] then
+        -- Optimize search when deterministic first character is available
+        if t[i].F then
+          seg_grammar[rname] = t[i][2] + notslash * (notslash - m.P(t[i].F)) ^ 0 * m.V(rname)
+        else
+          seg_grammar[rname] = t[i][2] + notslash * m.V(rname)
+        end
+      end
+      seg_grammar.s = seg_grammar.s * m.V(rname)
+    end
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
+    end
+    return m.P(seg_grammar)
+  else
+    seg_grammar.s = t.s
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
+    end
+    return seg_grammar.s
   end
+end
 
-  local function mul(acc, m)
-    return acc * m
+--- @param p vim.lpeg.Pattern Pattern directly after `**/`
+--- @return vim.lpeg.Pattern LPeg pattern for `**/p`
+local function dseg(p)
+  return m.P { p + notslash ^ 0 * m.P '/' * m.V(1) }
+end
+
+--- @type (vim.lpeg.Pattern|table)
+local g = nil
+
+--- Multiplies conditions for braced expansion (Cartesian product)
+---
+--- @param a string|string[] First part
+--- @param b string|string[] Second part
+--- @return string|string[] Cartesian product of values
+local function mul_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return a .. b
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        b[i] = a .. b[i]
+      end
+      return b
+    else
+      return a
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      for i = 1, #a do
+        a[i] = a[i] .. b
+      end
+      return a
+    elseif type(b) == 'table' then
+      --- @type string[]
+      local res = {}
+      local idx = 0
+      for i = 1, #a do
+        for j = 1, #b do
+          idx = idx + 1
+          res[idx] = a[i] .. b[j]
+        end
+      end
+      return res
+    else
+      return a
+    end
+  else
+    return b
   end
+end
 
-  local function star(stars, after)
-    return (-after * (P(1) - pathsep)) ^ #stars * after
+--- Combines alternatives in braced patterns
+---
+--- @param a string|table First part
+--- @param b string|table Second part
+--- @return table #Combined alternatives
+local function add_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return { a, b }
+    elseif type(b) == 'table' then
+      table.insert(b, 1, a)
+      return b
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      table.insert(a, b)
+      return a
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        table.insert(a, b[i])
+      end
+      return a
+    end
+    --- @diagnostic disable-next-line: missing-return
   end
+end
 
-  local function dstar(after)
-    return (-after * P(1)) ^ 0 * after
+--- Expands patterns handling segment boundaries
+--- `#` prefix is added for sub-grammar to detect in-segment flag
+---
+---@param a (any[]|vim.lpeg.Pattern[]) Array of patterns
+---@param b string Tail string
+---@param inseg boolean Whether inside a path segment
+---@return vim.lpeg.Pattern #Expanded pattern
+local function expand(a, b, inseg)
+  for i = 1, #a do
+    if inseg then
+      a[i] = '#' .. a[i]
+    end
+    a[i] = g:match(a[i] .. b)
+  end
+  local res = a[1]
+  for i = 2, #a do
+    res = res + a[i]
   end
+  return res
+end
+
+--- Converts a UTF-8 character to its Unicode codepoint
+---
+--- @param utf8_str string UTF-8 character
+--- @return number #Codepoint value
+local function to_codepoint(utf8_str)
+  local codepoint = 0
+  local byte_count = 0
+
+  for i = 1, #utf8_str do
+    local byte = utf8_str:byte(i)
 
-  -- luacheck: push ignore s
-  local function cut(_s, idx, match)
-    return idx, match
+    if byte_count ~= 0 then
+      codepoint = bit.bor(bit.lshift(codepoint, 6), bit.band(byte, 0x3F))
+      byte_count = byte_count - 1
+    else
+      if byte < 0x80 then
+        codepoint = byte
+      elseif byte < 0xE0 then
+        byte_count = 1
+        codepoint = bit.band(byte, 0x1F)
+      elseif byte < 0xF0 then
+        byte_count = 2
+        codepoint = bit.band(byte, 0x0F)
+      else
+        byte_count = 3
+        codepoint = bit.band(byte, 0x07)
+      end
+    end
+
+    if byte_count == 0 then
+      break
+    end
   end
-  -- luacheck: pop
-
-  --- @diagnostic disable-next-line: missing-fields
-  local p = P({
-    'Pattern',
-    Pattern = V('Elem') ^ -1 * V('End'),
-    Elem = Cmt(
-      Cf(
-        (V('DStar') + V('Star') + V('Ques') + V('Class') + V('CondList') + V('Literal'))
-          * (V('Elem') + V('End')),
-        mul
-      ),
-      cut
-    ),
-    DStar = (B(pathsep) + -B(P(1)))
-      * P('**')
-      * (pathsep * (V('Elem') + V('End')) + V('End'))
-      / dstar,
-    Star = C(P('*') ^ 1) * (V('Elem') + V('End')) / star,
-    Ques = P('?') * Cc(P(1) - pathsep),
-    Class = P('[')
-      * C(P('!') ^ -1)
-      * Ct(Ct(C(P(1)) * P('-') * C(P(1) - P(']'))) ^ 1 * P(']'))
-      / class,
-    CondList = P('{') * Ct(V('Cond') * (P(',') * V('Cond')) ^ 0) * P('}') * V('Pattern') / condlist,
-    -- TODO: '*' inside a {} condition is interpreted literally but should probably have the same
-    -- wildcard semantics it usually has.
-    -- Fixing this is non-trivial because '*' should match non-greedily up to "the rest of the
-    -- pattern" which in all other cases is the entire succeeding part of the pattern, but at the end of a {}
-    -- condition means "everything after the {}" where several other options separated by ',' may
-    -- exist in between that should not be matched by '*'.
-    Cond = Cmt(Cf((V('Ques') + V('Class') + V('Literal') - S(',}')) ^ 1, mul), cut) + Cc(P(0)),
-    Literal = P(1) / P,
-    End = P(-1) * Cc(P(-1)),
-  })
-
-  local lpeg_pattern = p:match(pattern) --[[@as vim.lpeg.Pattern?]]
+
+  return codepoint
+end
+
+--- Pattern for matching UTF-8 characters
+local cont = m.R('\128\191')
+local any_utf8 = m.R('\0\127')
+  + m.R('\194\223') * cont
+  + m.R('\224\239') * cont * cont
+  + m.R('\240\244') * cont * cont * cont
+
+--- Creates a character class pattern for glob ranges
+--- @param inv string Inversion flag ('!' or '')
+--- @param ranges (string|string[])[] Character ranges
+--- @return vim.lpeg.Pattern #Character class pattern
+local function class(inv, ranges)
+  local patt = m.P(false)
+  if #ranges == 0 then
+    if inv == '!' then
+      return m.P '[!]'
+    else
+      return m.P '[]'
+    end
+  end
+  for _, v in ipairs(ranges) do
+    patt = patt + (type(v) == 'table' and m.utfR(to_codepoint(v[1]), to_codepoint(v[2])) or m.P(v))
+  end
+  if inv == '!' then
+    patt = m.P(1) - patt --[[@as vim.lpeg.Pattern]]
+  end
+  return patt - m.P '/'
+end
+
+-- Parse constraints for optimizing braced conditions
+local noopt_condlist = re.compile [[
+  s <- '/' / '**' / . [^/*]* s
+]]
+
+local opt_tail = re.compile [[
+  s <- (!'**' [^{/])* &'/'
+]]
+
+-- stylua: ignore start
+--- @nodoc
+--- @diagnostic disable
+--- Main grammar for glob pattern matching
+g = {
+  'Glob',
+  Glob     = (m.P'#' * m.Cg(m.Cc(true), 'inseg') + m.Cg(m.Cc(false), 'inseg')) *
+             m.Cf(m.V'Element'^-1 * (slash * m.V'Element')^0 * (slash^-1 * eof), mt.__mul),
+  -- Elements handle segments, globstar patterns
+  Element  = m.V'DSeg' + m.V'DSEnd' + m.Cf(m.V'Segment' * (slash * m.V'Segment')^0 * (slash * eof + eof^-1), mt.__mul),
+  -- Globstar patterns
+  DSeg     = m.P'**/' * ((m.V'Element' + eof) / dseg),
+  DSEnd    = m.P'**' * -1 * m.Cc(m.P(1)^0),
+  -- Segment handling with word and star patterns
+  Segment  = (m.V'Word' / start_seg + m.Cc({ '', true }) / start_seg * (m.V'Star' * m.V'Word' % lookfor)) *
+              (m.V'Star' * m.V'Word' % lookfor)^0 * (m.V'Star' * m.V'CheckBnd' % to_seg_end)^-1 / end_seg
+             + m.V'Star' * m.V'CheckBnd' * m.Cc(notslash^0),
+  CheckBnd = #m.P'/' + -1,  -- Boundary constraint
+
+  -- Word patterns for fixed-length matching
+  Word     = -m.P'*' * m.Ct( m.V('FIRST')^-1 * m.C(m.V'WordAux') ),
+  WordAux  = m.V'Branch' + m.Cf(m.V'Simple'^1 * m.V'Branch'^-1, mt.__mul),
+  Simple   = m.Cg( m.V'Token' * (m.V'Token' % mt.__mul)^0 * (m.V'Boundary' % mt.__mul)^-1),
+  Boundary = #m.P'/' * m.Cc(#m.P'/') + eof,
+  Token    = m.V'Ques' + m.V'Class' + m.V'Escape' + m.V'Literal',
+  Star     = m.P'*',
+  Ques     = m.P'?' * m.Cc(notslash),
+  Escape   = m.P'\\' * m.C(1) / m.P,
+  Literal  = m.C(letter^1) / m.P,
+
+  -- Branch handling for braced conditions
+  Branch   = m.Cmt(m.C(m.V'CondList'), function(s, i, p1, p2)
+                                         -- Optimize brace expansion when possible
+                                         -- p1: string form of condition list, p2: transformed lua table
+                                         if noopt_condlist:match(p1) then
+                                           -- Cannot optimize, match till the end
+                                           return #s + 1, p2, s:sub(i)
+                                         end
+                                         -- Find point to cut for optimization
+                                         local cut = opt_tail:match(s, i)
+                                         if cut then
+                                           -- Can optimize: match till cut point
+                                           -- true flag tells expand to transform EOF matches to &'/' predicates
+                                           return cut, p2, s:sub(i, cut - 1), true
+                                         else
+                                           -- Cannot optimize
+                                           return #s + 1, p2, s:sub(i)
+                                         end
+                                       end) / expand,
+  -- Brace expansion handling
+  CondList = m.Cf(m.P'{' * m.V'Cond' * (m.P',' * m.V'Cond')^1 * m.P'}', add_cond),
+  Cond     = m.Cf((m.C((notcomma + m.P'\\' * 1 - m.S'{}')^1) + m.V'CondList')^1, mul_cond) + m.C(true),
+
+  -- Character class handling
+  Class    = m.P'[' * m.C(m.P'!'^-1) * m.Ct(
+              (m.Ct(m.C(any_utf8) * m.P'-' * m.C(any_utf8 - m.P']')) + m.C(any_utf8 - m.P']'))^0
+            ) * m.P']' / class,
+
+  -- Deterministic first character extraction for optimization
+  FIRST    = m.Cg(m.P(function(s, i)
+                        if letter:match(s, i) then return true, s:sub(i, i)
+                        else return false end
+                      end), 'F')
+}
+-- stylua: ignore end
+--- @diagnostic enable
+
+--- @nodoc
+g = m.P(g)
+
+--- Parses a raw glob into an |lua-lpeg| pattern.
+---
+---@param pattern string The raw glob pattern
+---@return vim.lpeg.Pattern #An |lua-lpeg| representation of the pattern
+function M.to_lpeg(pattern)
+  local lpeg_pattern = g:match(pattern) --[[@as vim.lpeg.Pattern?]]
   assert(lpeg_pattern, 'Invalid glob')
   return lpeg_pattern
 end
diff --git a/test/functional/lua/glob_spec.lua b/test/functional/lua/glob_spec.lua
index 8302c7334d8807..3a105747fe247a 100644
--- a/test/functional/lua/glob_spec.lua
+++ b/test/functional/lua/glob_spec.lua
@@ -18,6 +18,7 @@ describe('glob', function()
       eq(true, match('', ''))
       eq(false, match('', 'a'))
       eq(true, match('a', 'a'))
+      eq(true, match('.', '.'))
       eq(true, match('/', '/'))
       eq(true, match('abc', 'abc'))
       eq(false, match('abc', 'abcdef'))
@@ -35,7 +36,8 @@ describe('glob', function()
     end)
 
     it('should match * wildcards', function()
-      eq(false, match('*', ''))
+      eq(true, match('*', ''))
+      eq(true, match('*', '   '))
       eq(true, match('*', 'a'))
       eq(false, match('*', '/'))
       eq(false, match('*', '/a'))
@@ -43,6 +45,7 @@ describe('glob', function()
       eq(true, match('*', 'aaa'))
       eq(true, match('*a', 'aa'))
       eq(true, match('*a', 'abca'))
+      eq(true, match('*.ts', '.ts'))
       eq(true, match('*.txt', 'file.txt'))
       eq(false, match('*.txt', 'file.txtxt'))
       eq(false, match('*.txt', 'dir/file.txt'))
@@ -62,18 +65,13 @@ describe('glob', function()
       eq(false, match('dir/*/file.txt', 'dir/file.txt'))
       eq(true, match('dir/*/file.txt', 'dir/subdir/file.txt'))
       eq(false, match('dir/*/file.txt', 'dir/subdir/subdir/file.txt'))
-
-      -- The spec does not describe this, but VSCode only interprets ** when it's by
-      -- itself in a path segment, and otherwise interprets ** as consecutive * directives.
-      -- see: https://github.com/microsoft/vscode/blob/eef30e7165e19b33daa1e15e92fa34ff4a5df0d3/src/vs/base/common/glob.ts#L112
-      eq(true, match('a**', 'abc')) -- '**' should parse as two '*'s when not by itself in a path segment
-      eq(true, match('**c', 'abc'))
-      eq(false, match('a**', 'ab')) -- each '*' should still represent at least one character
-      eq(false, match('**c', 'bc'))
-      eq(true, match('a**', 'abcd'))
-      eq(true, match('**d', 'abcd'))
-      eq(false, match('a**', 'abc/d'))
-      eq(false, match('**d', 'abc/d'))
+      eq(true, match('a*b*c*d*e*', 'axbxcxdxe'))
+      eq(true, match('a*b*c*d*e*', 'axbxcxdxexxx'))
+      eq(true, match('a*b?c*x', 'abxbbxdbxebxczzx'))
+      eq(false, match('a*b?c*x', 'abxbbxdbxebxczzy'))
+      eq(true, match('a*b*[cy]*d*e*', 'axbxcxdxexxx'))
+      eq(true, match('a*b*[cy]*d*e*', 'axbxyxdxexxx'))
+      eq(true, match('a*b*[cy]*d*e*', 'axbxxxyxdxexxx'))
     end)
 
     it('should match ? wildcards', function()
@@ -84,6 +82,11 @@ describe('glob', function()
       eq(true, match('??', 'ab'))
       eq(true, match('a?c', 'abc'))
       eq(false, match('a?c', 'a/c'))
+      eq(false, match('a/', 'a/.b'))
+      eq(true, match('?/?', 'a/b'))
+      eq(true, match('/??', '/ab'))
+      eq(true, match('/?b', '/ab'))
+      eq(false, match('foo?bar', 'foo/bar'))
     end)
 
     it('should match ** wildcards', function()
@@ -99,7 +102,7 @@ describe('glob', function()
       eq(true, match('/**', '/'))
       eq(true, match('/**', '/a/b/c'))
       eq(true, match('**/', '')) -- **/ absorbs trailing /
-      eq(true, match('**/', '/a/b/c'))
+      eq(false, match('**/', '/a/b/c'))
       eq(true, match('**/**', ''))
       eq(true, match('**/**', 'a'))
       eq(false, match('a/**', ''))
@@ -134,20 +137,9 @@ describe('glob', function()
     end)
 
     it('should match {} groups', function()
-      eq(true, match('{}', ''))
-      eq(false, match('{}', 'a'))
-      eq(true, match('a{}', 'a'))
-      eq(true, match('{}a', 'a'))
       eq(true, match('{,}', ''))
       eq(true, match('{a,}', ''))
       eq(true, match('{a,}', 'a'))
-      eq(true, match('{a}', 'a'))
-      eq(false, match('{a}', 'aa'))
-      eq(false, match('{a}', 'ab'))
-      eq(true, match('{a?c}', 'abc'))
-      eq(false, match('{ab}', 'a'))
-      eq(false, match('{ab}', 'b'))
-      eq(true, match('{ab}', 'ab'))
       eq(true, match('{a,b}', 'a'))
       eq(true, match('{a,b}', 'b'))
       eq(false, match('{a,b}', 'ab'))
@@ -155,7 +147,22 @@ describe('glob', function()
       eq(false, match('{ab,cd}', 'a'))
       eq(true, match('{ab,cd}', 'cd'))
       eq(true, match('{a,b,c}', 'c'))
-      eq(false, match('{a,{b,c}}', 'c')) -- {} cannot nest
+      eq(true, match('{a,{b,c}}', 'c'))
+      eq(true, match('a{,/}*.txt', 'a.txt'))
+      eq(true, match('a{,/}*.txt', 'ab.txt'))
+      eq(true, match('a{,/}*.txt', 'a/b.txt'))
+      eq(true, match('a{,/}*.txt', 'a/ab.txt'))
+      eq(true, match('a/{a{a,b},b}', 'a/aa'))
+      eq(true, match('a/{a{a,b},b}', 'a/ab'))
+      eq(false, match('a/{a{a,b},b}', 'a/ac'))
+      eq(true, match('a/{a{a,b},b}', 'a/b'))
+      eq(false, match('a/{a{a,b},b}', 'a/c'))
+      eq(true, match('foo{bar,b*z}', 'foobar'))
+      eq(true, match('foo{bar,b*z}', 'foobuzz'))
+      eq(true, match('foo{bar,b*z}', 'foobarz'))
+      eq(true, match('{a,b}/c/{d,e}/**/*est.ts', 'a/c/d/one/two/three.test.ts'))
+      eq(true, match('{a,{d,e}b}/c', 'a/c'))
+      eq(true, match('{**/a,**/b}', 'b'))
     end)
 
     it('should match [] groups', function()
@@ -181,6 +188,13 @@ describe('glob', function()
       eq(true, match('[a-zA-Z0-9]', 'Z'))
       eq(true, match('[a-zA-Z0-9]', '9'))
       eq(false, match('[a-zA-Z0-9]', '&'))
+      eq(true, match('[?]', '?'))
+      eq(false, match('[?]', 'a'))
+      eq(true, match('[*]', '*'))
+      eq(false, match('[*]', 'a'))
+      eq(true, match('[\\!]', '!'))
+      eq(true, match('a\\*b', 'a*b'))
+      eq(false, match('a\\*b', 'axb'))
     end)
 
     it('should match [!...] groups', function()
@@ -202,8 +216,7 @@ describe('glob', function()
     it('should handle long patterns', function()
       -- lpeg has a recursion limit of 200 by default, make sure the grammar does trigger it on
       -- strings longer than that
-      local fill_200 =
-        'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+      local fill_200 = ('a'):rep(200)
       eq(200, fill_200:len())
       local long_lit = fill_200 .. 'a'
       eq(false, match(long_lit, 'b'))
@@ -212,6 +225,21 @@ describe('glob', function()
       eq(true, match(long_pat, fill_200 .. 'a/b/c/d.c'))
     end)
 
+    -- New test for unicode patterns from assets
+    it('should match unicode patterns', function()
+      eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.ts'))
+      eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.tsx'))
+      eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.js'))
+      eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.jsx'))
+      eq(false, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.jsxxxxxxxx'))
+      eq(true, match('*é*', 'café noir'))
+      eq(true, match('caf*noir', 'café noir'))
+      eq(true, match('caf*noir', 'cafeenoir'))
+      eq(true, match('F[ë£a]', 'Fë'))
+      eq(true, match('F[ë£a]', 'F£'))
+      eq(true, match('F[ë£a]', 'Fa'))
+    end)
+
     it('should match complex patterns', function()
       eq(false, match('**/*.{c,h}', ''))
       eq(false, match('**/*.{c,h}', 'c'))


Reviews:
user: login: clason
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T09:29:57Z
user: login: clason
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T12:05:24Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T12:20:09Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T12:21:11Z
user: login: brynne8
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:21:42Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T12:22:39Z
user: login: brynne8
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:23:45Z
user: login: brynne8
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:26:23Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T12:37:38Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T09:33:25Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T09:38:32Z
user: login: justinmk
body: Please add a news.txt description, either under PERFORMANCE or perhaps LUA. If this improves performance, give a hint about the most notable cases: `vim.glob performance for common patterns like "foo" improved by 5x"`
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-21T09:48:40Z
user: login: brynne8
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T03:49:21Z

Review comments:
diff_hunk: @@ -1,93 +1,363 @@
-local lpeg = vim.lpeg
-local P, S, V, R, B = lpeg.P, lpeg.S, lpeg.V, lpeg.R, lpeg.B
-local C, Cc, Ct, Cf, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
+--[[
+Glob-to-LPeg Converter (Peglob)
+This module converts glob patterns to LPeg patterns according to the LSP 3.17 specification:
+https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
+
+Glob grammar overview:
+- `*` to match one or more characters in a path segment
+- `?` to match on one character in a path segment
+- `**` to match any number of path segments, including none
+- `{}` to group conditions (e.g. `*.{ts,js}` matches TypeScript and JavaScript files)
+- `[]` to declare a range of characters to match in a path segment
+  (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+- `[!...]` to negate a range of characters to match in a path segment
+  (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+
+Additional constraints:
+- A Glob pattern must match an entire path, with partial matches
+  considered failures.
+- The pattern only determines success or failure, without specifying
+  which parts correspond to which characters.
+- A **path segment** is the portion of a path between two adjacent path
+  separators (`/`), or between the start/end of the path and the nearest
+  separator.
+- The `**` (**globstar**) pattern matches zero or more path segments,
+  including intervening separators (`/`). Within pattern strings, `**`
+  must be delimited by path separators (`/`) or pattern boundaries and
+  cannot be adjacent to any characters other than `/`. If `**` is not
+  the final element, it must be followed by `/`.
+- `{}` (**braced conditions**) contains valid Glob patterns as branches,
+  separated by commas. Commas are exclusively used for separating
+  branches and cannot appear within a branch for any other purpose.
+  Nested `{}` structures are allowed, but `{}` must contain at least two
+  branches—zero or one branch is not permitted.
+- In `[]` or `[!...]`, a **character range** consists of character
+  intervals (e.g., `a-z`) or individual characters (e.g., `w`). A range
+  including `/` won’t match that character.
+]]
+
+local m = vim.lpeg
+local mt = getmetatable(m.P(0))
+local re = vim.re
 
 local M = {}
 
-local pathsep = P('/')
+-- Basic patterns for matching glob components
+local letter = m.P(1) - m.S(',*?[]{}/\\') -- Any character except special glob characters
+local slash = m.P '/' * m.Cc(m.P '/') -- Path separator with capture
+local notslash = m.P(1) - m.P '/' -- Any character except path separator
+local notcomma = m.P(1) - m.S(',\\') -- Any character except comma and backslash
+-- Handle EOF, considering whether we're in a segment or not
+local eof = -1
+  * m.Cb('inseg')
+  / function(flag)
+    if flag then
+      return #m.P '/'
+    else
+      return m.P(-1)
+    end
+  end
 
---- Parses a raw glob into an |lua-lpeg| pattern.
----
---- This uses glob semantics from LSP 3.17.0: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
----
---- Glob patterns can have the following syntax:
---- - `*` to match one or more characters in a path segment
---- - `?` to match on one character in a path segment
---- - `**` to match any number of path segments, including none
---- - `{}` to group conditions (e.g. `*.{ts,js}` matches TypeScript and JavaScript files)
---- - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
---- - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+--- @param p table Initial segment pattern data
+--- @return table Segment structure with start pattern
+local function start_seg(p)
+  return { s = p[2], e = true, n = 0 }
+end
+
+--- @param t table Segment structure
+--- @param p any Pattern to look for
+--- @return table Updated segment structure
+local function lookfor(t, p)
+  t.n = t.n + 1
+  t[t.n] = p
+  return t
+end
+
+--- @param t table Segment structure
+--- @return table Segment structure with end pattern
+local function to_seg_end(t)
+  t.e = notslash ^ 0
+  return t
+end
+
+--- Constructs a segment matching pattern from collected components
 ---
----@param pattern string The raw glob pattern
----@return vim.lpeg.Pattern pattern An |lua-lpeg| representation of the pattern
-function M.to_lpeg(pattern)
-  local function class(inv, ranges)
-    local patt = R(unpack(vim.tbl_map(table.concat, ranges)))
-    if inv == '!' then
-      patt = P(1) - patt
+--- @param t table Segment structure with patterns
+--- @return vim.lpeg.Pattern Complete segment match pattern
+local function end_seg(t)
+  local seg_grammar = { 's' }
+  if t.n > 0 then
+    seg_grammar.s = t.s
+    for i = 1, t.n do
+      local rname = t[i][1]
+      if not seg_grammar[rname] then
+        -- Optimize search when deterministic first character is available
+        if t[i].F then
+          seg_grammar[rname] = t[i][2] + notslash * (notslash - m.P(t[i].F)) ^ 0 * m.V(rname)
+        else
+          seg_grammar[rname] = t[i][2] + notslash * m.V(rname)
+        end
+      end
+      seg_grammar.s = seg_grammar.s * m.V(rname)
+    end
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
     end
-    return patt
+    return m.P(seg_grammar)
+  else
+    seg_grammar.s = t.s
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
+    end
+    return seg_grammar.s
   end
+end
 
-  local function condlist(conds, after)
-    return vim.iter(conds):fold(P(false), function(acc, cond)
-      return acc + cond * after
-    end)
+---@param p vim.lpeg.Pattern Pattern directly after `**/`
+---@return vim.lpeg.Pattern LPeg pattern for `**/p`
+local function dseg(p)
+  return m.P { p + notslash ^ 0 * m.P '/' * m.V(1) }
+end
+
+local g = nil
+
+--- Multiplies conditions for braced expansion (Cartesian product)
+---
+--- @param a string|table First part
+--- @param b string|table Second part
+--- @return string|table Cartesian product of values
+local function mul_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return a .. b
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        b[i] = a .. b[i]
+      end
+      return b
+    else
+      return a
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      for i = 1, #a do
+        a[i] = a[i] .. b
+      end
+      return a
+    elseif type(b) == 'table' then
+      local res = {}
+      local idx = 0
+      for i = 1, #a do
+        for j = 1, #b do
+          idx = idx + 1
+          res[idx] = a[i] .. b[j]
+        end
+      end
+      return res
+    else
+      return a
+    end
+  else
+    return b
   end
+end
 
-  local function mul(acc, m)
-    return acc * m
+--- Combines alternatives in braced patterns
+---
+--- @param a string|table First part
+--- @param b string|table Second part
+--- @return table Combined alternatives
+local function add_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return { a, b }
+    elseif type(b) == 'table' then
+      table.insert(b, 1, a)
+      return b
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      table.insert(a, b)
+      return a
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        table.insert(a, b[i])
+        return a
+      end
+    end
   end
+end
 
-  local function star(stars, after)
-    return (-after * (P(1) - pathsep)) ^ #stars * after
+--- Expands patterns handling segment boundaries
+--- `#` prefix is added for sub-grammar to detect in-segment flag
+---
+---@param a table Array of patterns
+---@param b string Tail string
+---@param inseg boolean Whether inside a path segment
+---@return vim.lpeg.Pattern Expanded pattern
+local function expand(a, b, inseg)
+  for i = 1, #a do
+    if inseg then
+      a[i] = '#' .. a[i]
+    end
+    a[i] = g:match(a[i] .. b)
   end
+  local res = a[1]
+  for i = 2, #a do
+    res = res + a[i]
+  end
+  return res
+end
+
+--- Converts a UTF-8 character to its Unicode codepoint
+---
+---@param utf8_str string UTF-8 character
+---@return number Codepoint value
+local function codepoint(utf8_str)
+  local codepoint = 0
+  local byte_count = 0
+
+  for i = 1, #utf8_str do
+    local byte = utf8_str:byte(i)
+
+    if byte_count ~= 0 then
+      codepoint = bit.bor(bit.lshift(codepoint, 6), bit.band(byte, 0x3F))
path: runtime/lua/vim/glob.lua
user: login: clason
body: Note that we target PUC Lua, not LuaJIT, so `bit` is not always available. Luckily, _we_ bundle `bit` so you can -- and should -- put `local bit = require('bit')` at the top of this file.
created_at: 2025-04-25T09:29:57Z
author_association: MEMBER
diff_hunk: @@ -1,93 +1,363 @@
-local lpeg = vim.lpeg
-local P, S, V, R, B = lpeg.P, lpeg.S, lpeg.V, lpeg.R, lpeg.B
-local C, Cc, Ct, Cf, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
+--[[
path: runtime/lua/vim/glob.lua
user: login: clason
body: `--- @brief`

(And the text needs to be commented linewise for the docgen to work, I believe.)
created_at: 2025-04-25T12:05:24Z
author_association: MEMBER
diff_hunk: @@ -83,3 +83,4 @@ tags
 /contrib/result-*
 
 CMakeUserPresets.json
+Xtest[_-]*
path: .gitignore
user: login: justinmk
body: this shouldn't be necessary. tests should cleanup after themselves. tests can use `finally()` (see existing examples) or `after()` / `after_each()`.
created_at: 2025-04-25T12:20:09Z
author_association: MEMBER
diff_hunk: @@ -1,93 +1,363 @@
-local lpeg = vim.lpeg
-local P, S, V, R, B = lpeg.P, lpeg.S, lpeg.V, lpeg.R, lpeg.B
-local C, Cc, Ct, Cf, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
+--[[
+Glob-to-LPeg Converter (Peglob)
+This module converts glob patterns to LPeg patterns according to the LSP 3.17 specification:
+https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
path: runtime/lua/vim/glob.lua
user: login: justinmk
body: ```suggestion
--- Glob-to-LPeg Converter (Peglob)
--- This module converts glob patterns to LPeg patterns according to the LSP 3.17 specification:
--- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
```

and so on
created_at: 2025-04-25T12:21:11Z
author_association: MEMBER
diff_hunk: @@ -1,93 +1,363 @@
-local lpeg = vim.lpeg
-local P, S, V, R, B = lpeg.P, lpeg.S, lpeg.V, lpeg.R, lpeg.B
-local C, Cc, Ct, Cf, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
+--[[
path: runtime/lua/vim/glob.lua
user: login: brynne8
body: I'm about to work on it. The `@brief` annotation seems not a LuaCATS defined annotation. It's better this be documented. 
created_at: 2025-04-25T12:21:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,28 +134,28 @@ describe('glob', function()
     end)
 
     it('should match {} groups', function()
-      eq(true, match('{}', ''))
-      eq(false, match('{}', 'a'))
-      eq(true, match('a{}', 'a'))
-      eq(true, match('{}a', 'a'))
+      -- eq(true, match('{}', ''))
path: test/functional/lua/glob_spec.lua
user: login: justinmk
body: why comment these instead of changing the assertion?
created_at: 2025-04-25T12:22:39Z
author_association: MEMBER
diff_hunk: @@ -83,3 +83,4 @@ tags
 /contrib/result-*
 
 CMakeUserPresets.json
+Xtest[_-]*
path: .gitignore
user: login: brynne8
body: Is that because I only ran the functional test? When I tried to commit, I saw Xtest files, so I added them to gitignore.
created_at: 2025-04-25T12:23:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,28 +134,28 @@ describe('glob', function()
     end)
 
     it('should match {} groups', function()
-      eq(true, match('{}', ''))
-      eq(false, match('{}', 'a'))
-      eq(true, match('a{}', 'a'))
-      eq(true, match('{}a', 'a'))
+      -- eq(true, match('{}', ''))
path: test/functional/lua/glob_spec.lua
user: login: brynne8
body: Actually I was commenting to let you check what tests failed, and the behaviors could be discussed. When finalized, All these should be changed to correct results.
created_at: 2025-04-25T12:26:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,3 +83,4 @@ tags
 /contrib/result-*
 
 CMakeUserPresets.json
+Xtest[_-]*
path: .gitignore
user: login: justinmk
body: that could mean that some test didn't cleanup properly. that test should be fixed. this would just hide that problem.
created_at: 2025-04-25T12:37:38Z
author_association: MEMBER
diff_hunk: @@ -83,3 +83,4 @@ tags
 /contrib/result-*
 
 CMakeUserPresets.json
+Xtest[_-]*
path: .gitignore
user: login: justinmk
body: ^
created_at: 2025-05-21T09:33:25Z
author_association: MEMBER
diff_hunk: @@ -1,93 +1,375 @@
-local lpeg = vim.lpeg
-local P, S, V, R, B = lpeg.P, lpeg.S, lpeg.V, lpeg.R, lpeg.B
-local C, Cc, Ct, Cf, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
-
-local M = {}
-
-local pathsep = P('/')
-
---- Parses a raw glob into an |lua-lpeg| pattern.
+--- @brief Glob-to-LPeg Converter (Peglob)
+--- This module converts glob patterns to LPeg patterns according to the LSP 3.17 specification:
+--- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
 ---
---- This uses glob semantics from LSP 3.17.0: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
----
---- Glob patterns can have the following syntax:
+--- Glob grammar overview:
 --- - `*` to match one or more characters in a path segment
 --- - `?` to match on one character in a path segment
 --- - `**` to match any number of path segments, including none
 --- - `{}` to group conditions (e.g. `*.{ts,js}` matches TypeScript and JavaScript files)
---- - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
---- - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+--- - `[]` to declare a range of characters to match in a path segment
+---   (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+--- - `[!...]` to negate a range of characters to match in a path segment
+---   (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
 ---
----@param pattern string The raw glob pattern
----@return vim.lpeg.Pattern pattern An |lua-lpeg| representation of the pattern
-function M.to_lpeg(pattern)
-  local function class(inv, ranges)
-    local patt = R(unpack(vim.tbl_map(table.concat, ranges)))
-    if inv == '!' then
-      patt = P(1) - patt
+--- Additional constraints:
+--- - A Glob pattern must match an entire path, with partial matches
+---   considered failures.
+--- - The pattern only determines success or failure, without specifying
+---   which parts correspond to which characters.
+--- - A *path segment* is the portion of a path between two adjacent path
+---   separators (`/`), or between the start/end of the path and the nearest
+---   separator.
+--- - The `**` (*globstar*) pattern matches zero or more path segments,
+---   including intervening separators (`/`). Within pattern strings, `**`
+---   must be delimited by path separators (`/`) or pattern boundaries and
+---   cannot be adjacent to any characters other than `/`. If `**` is not
+---   the final element, it must be followed by `/`.
+--- - `{}` (*braced conditions*) contains valid Glob patterns as branches,
+---   separated by commas. Commas are exclusively used for separating
+---   branches and cannot appear within a branch for any other purpose.
+---   Nested `{}` structures are allowed, but `{}` must contain at least two
+---   branches—zero or one branch is not permitted.
+--- - In `[]` or `[!...]`, a *character range* consists of character
+---   intervals (e.g., `a-z`) or individual characters (e.g., `w`). A range
+---   including `/` won’t match that character.
+
+--- @diagnostic disable: missing-fields
+
+local m = vim.lpeg
+local mt = getmetatable(m.P(0))
+local re = vim.re
+local bit = require('bit')
+
+local M = {}
+
+-- Basic patterns for matching glob components
+local letter = m.P(1) - m.S(',*?[]{}/\\') -- Any character except special glob characters
+local slash = m.P '/' * m.Cc(m.P '/') -- Path separator with capture
+local notslash = m.P(1) - m.P '/' -- Any character except path separator
+local notcomma = m.P(1) - m.S(',\\') -- Any character except comma and backslash
+
+--- Handle EOF, considering whether we're in a segment or not
+--- @type vim.lpeg.Pattern
+local eof = -1
+  * m.Cb('inseg')
+  / function(flag)
+    if flag then
+      return #m.P '/'
+    else
+      return m.P(-1)
     end
-    return patt
   end
 
-  local function condlist(conds, after)
-    return vim.iter(conds):fold(P(false), function(acc, cond)
-      return acc + cond * after
-    end)
+---@alias pat_table { F: string?, [1]: string, [2]: vim.lpeg.Pattern }
+---@alias seg_part { [string]: any, [integer]: pat_table }
+
+--- @param p pat_table Initial segment pattern data
+--- @return seg_part Segment structure with start pattern
+local function start_seg(p)
+  return { s = p[2], e = true, n = 0 }
+end
+
+--- @param t seg_part Segment structure
+--- @param p pat_table Pattern to look for
+--- @return table Updated segment structure
+local function lookfor(t, p)
+  t.n = t.n + 1
+  t[t.n] = p
+  return t
+end
+
+--- @param t seg_part Segment structure
+--- @return table Segment structure with end pattern
+local function to_seg_end(t)
+  t.e = notslash ^ 0
+  return t
+end
+
+--- Constructs a segment matching pattern from collected components
+---
+--- @param t seg_part Segment structure with patterns
+--- @return vim.lpeg.Pattern Complete segment match pattern
+local function end_seg(t)
+  --- @type table<any,any>
+  local seg_grammar = { 's' }
+  if t.n > 0 then
+    seg_grammar.s = t.s
+    for i = 1, t.n do
+      local rname = t[i][1]
+      if not seg_grammar[rname] then
+        -- Optimize search when deterministic first character is available
+        if t[i].F then
+          seg_grammar[rname] = t[i][2] + notslash * (notslash - m.P(t[i].F)) ^ 0 * m.V(rname)
+        else
+          seg_grammar[rname] = t[i][2] + notslash * m.V(rname)
+        end
+      end
+      seg_grammar.s = seg_grammar.s * m.V(rname)
+    end
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
+    end
+    return m.P(seg_grammar)
+  else
+    seg_grammar.s = t.s
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
+    end
+    return seg_grammar.s
   end
+end
 
-  local function mul(acc, m)
-    return acc * m
+--- @param p vim.lpeg.Pattern Pattern directly after `**/`
+--- @return vim.lpeg.Pattern LPeg pattern for `**/p`
+local function dseg(p)
+  return m.P { p + notslash ^ 0 * m.P '/' * m.V(1) }
+end
+
+--- @type (vim.lpeg.Pattern|table)
+local g = nil
+
+--- Multiplies conditions for braced expansion (Cartesian product)
+---
+--- @param a string|string[] First part
+--- @param b string|string[] Second part
+--- @return string|string[] Cartesian product of values
+local function mul_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return a .. b
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        b[i] = a .. b[i]
+      end
+      return b
+    else
+      return a
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      for i = 1, #a do
+        a[i] = a[i] .. b
+      end
+      return a
+    elseif type(b) == 'table' then
+      --- @type string[]
+      local res = {}
+      local idx = 0
+      for i = 1, #a do
+        for j = 1, #b do
+          idx = idx + 1
+          res[idx] = a[i] .. b[j]
+        end
+      end
+      return res
+    else
+      return a
+    end
+  else
+    return b
   end
+end
 
-  local function star(stars, after)
-    return (-after * (P(1) - pathsep)) ^ #stars * after
+--- Combines alternatives in braced patterns
+---
+--- @param a string|table First part
+--- @param b string|table Second part
+--- @return table #Combined alternatives
+local function add_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return { a, b }
+    elseif type(b) == 'table' then
+      table.insert(b, 1, a)
+      return b
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      table.insert(a, b)
+      return a
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        table.insert(a, b[i])
+      end
+      return a
+    end
+    --- @diagnostic disable-next-line: missing-return
   end
+end
 
-  local function dstar(after)
-    return (-after * P(1)) ^ 0 * after
+--- Expands patterns handling segment boundaries
+--- `#` prefix is added for sub-grammar to detect in-segment flag
+---
+---@param a (any[]|vim.lpeg.Pattern[]) Array of patterns
+---@param b string Tail string
+---@param inseg boolean Whether inside a path segment
+---@return vim.lpeg.Pattern #Expanded pattern
+local function expand(a, b, inseg)
+  for i = 1, #a do
+    if inseg then
+      a[i] = '#' .. a[i]
+    end
+    a[i] = g:match(a[i] .. b)
+  end
+  local res = a[1]
+  for i = 2, #a do
+    res = res + a[i]
   end
+  return res
+end
+
+--- Converts a UTF-8 character to its Unicode codepoint
+---
+--- @param utf8_str string UTF-8 character
+--- @return number #Codepoint value
+local function to_codepoint(utf8_str)
+  local codepoint = 0
+  local byte_count = 0
+
+  for i = 1, #utf8_str do
+    local byte = utf8_str:byte(i)
 
-  -- luacheck: push ignore s
-  local function cut(_s, idx, match)
-    return idx, match
+    if byte_count ~= 0 then
+      codepoint = bit.bor(bit.lshift(codepoint, 6), bit.band(byte, 0x3F))
+      byte_count = byte_count - 1
+    else
+      if byte < 0x80 then
+        codepoint = byte
+      elseif byte < 0xE0 then
+        byte_count = 1
+        codepoint = bit.band(byte, 0x1F)
+      elseif byte < 0xF0 then
+        byte_count = 2
+        codepoint = bit.band(byte, 0x0F)
+      else
+        byte_count = 3
+        codepoint = bit.band(byte, 0x07)
+      end
+    end
+
+    if byte_count == 0 then
+      break
+    end
   end
-  -- luacheck: pop
-
-  --- @diagnostic disable-next-line: missing-fields
-  local p = P({
-    'Pattern',
-    Pattern = V('Elem') ^ -1 * V('End'),
-    Elem = Cmt(
-      Cf(
-        (V('DStar') + V('Star') + V('Ques') + V('Class') + V('CondList') + V('Literal'))
-          * (V('Elem') + V('End')),
-        mul
-      ),
-      cut
-    ),
-    DStar = (B(pathsep) + -B(P(1)))
-      * P('**')
-      * (pathsep * (V('Elem') + V('End')) + V('End'))
-      / dstar,
-    Star = C(P('*') ^ 1) * (V('Elem') + V('End')) / star,
-    Ques = P('?') * Cc(P(1) - pathsep),
-    Class = P('[')
-      * C(P('!') ^ -1)
-      * Ct(Ct(C(P(1)) * P('-') * C(P(1) - P(']'))) ^ 1 * P(']'))
-      / class,
-    CondList = P('{') * Ct(V('Cond') * (P(',') * V('Cond')) ^ 0) * P('}') * V('Pattern') / condlist,
-    -- TODO: '*' inside a {} condition is interpreted literally but should probably have the same
-    -- wildcard semantics it usually has.
-    -- Fixing this is non-trivial because '*' should match non-greedily up to "the rest of the
-    -- pattern" which in all other cases is the entire succeeding part of the pattern, but at the end of a {}
-    -- condition means "everything after the {}" where several other options separated by ',' may
-    -- exist in between that should not be matched by '*'.
-    Cond = Cmt(Cf((V('Ques') + V('Class') + V('Literal') - S(',}')) ^ 1, mul), cut) + Cc(P(0)),
-    Literal = P(1) / P,
-    End = P(-1) * Cc(P(-1)),
-  })
-
-  local lpeg_pattern = p:match(pattern) --[[@as vim.lpeg.Pattern?]]
+
+  return codepoint
+end
+
+--- Pattern for matching UTF-8 characters
+local cont = m.R('\128\191')
+local any_utf8 = m.R('\0\127')
+  + m.R('\194\223') * cont
+  + m.R('\224\239') * cont * cont
+  + m.R('\240\244') * cont * cont * cont
+
+--- Creates a character class pattern for glob ranges
+--- @param inv string Inversion flag ('!' or '')
+--- @param ranges (string|string[])[] Character ranges
+--- @return vim.lpeg.Pattern #Character class pattern
+local function class(inv, ranges)
+  local patt = m.P(false)
+  if #ranges == 0 then
+    if inv == '!' then
+      return m.P '[!]'
+    else
+      return m.P '[]'
+    end
+  end
+  for _, v in ipairs(ranges) do
+    patt = patt + (type(v) == 'table' and m.utfR(to_codepoint(v[1]), to_codepoint(v[2])) or m.P(v))
+  end
+  if inv == '!' then
+    patt = m.P(1) - patt --[[@as vim.lpeg.Pattern]]
+  end
+  return patt - m.P '/'
+end
+
+-- Parse constraints for optimizing braced conditions
+local noopt_condlist = re.compile [[
+  s <- '/' / '**' / . [^/*]* s
+]]
+
+local opt_tail = re.compile [[
+  s <- (!'**' [^{/])* &'/'
+]]
+
+-- stylua: ignore start
+--- @nodoc
+--- @diagnostic disable
+--- Main grammar for glob pattern matching
+g = {
+  'Glob',
+  Glob     = (m.P'#' * m.Cg(m.Cc(true), 'inseg') + m.Cg(m.Cc(false), 'inseg')) *
+             m.Cf(m.V'Element'^-1 * (slash * m.V'Element')^0 * (slash^-1 * eof), mt.__mul),
+  -- Elements handle segments, globstar patterns
+  Element  = m.V'DSeg' + m.V'DSEnd' + m.Cf(m.V'Segment' * (slash * m.V'Segment')^0 * (slash * eof + eof^-1), mt.__mul),
path: runtime/lua/vim/glob.lua
user: login: justinmk
body: was this renamed from `Elem` in the old impl? will that break any consumers? 

same for any other cases. avoiding renaming stuff, even if it's not exactly the same behavior.
created_at: 2025-05-21T09:38:32Z
author_association: MEMBER
diff_hunk: @@ -1,93 +1,375 @@
-local lpeg = vim.lpeg
-local P, S, V, R, B = lpeg.P, lpeg.S, lpeg.V, lpeg.R, lpeg.B
-local C, Cc, Ct, Cf, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
-
-local M = {}
-
-local pathsep = P('/')
-
---- Parses a raw glob into an |lua-lpeg| pattern.
+--- @brief Glob-to-LPeg Converter (Peglob)
+--- This module converts glob patterns to LPeg patterns according to the LSP 3.17 specification:
+--- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
 ---
---- This uses glob semantics from LSP 3.17.0: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
----
---- Glob patterns can have the following syntax:
+--- Glob grammar overview:
 --- - `*` to match one or more characters in a path segment
 --- - `?` to match on one character in a path segment
 --- - `**` to match any number of path segments, including none
 --- - `{}` to group conditions (e.g. `*.{ts,js}` matches TypeScript and JavaScript files)
---- - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
---- - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+--- - `[]` to declare a range of characters to match in a path segment
+---   (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+--- - `[!...]` to negate a range of characters to match in a path segment
+---   (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
 ---
----@param pattern string The raw glob pattern
----@return vim.lpeg.Pattern pattern An |lua-lpeg| representation of the pattern
-function M.to_lpeg(pattern)
-  local function class(inv, ranges)
-    local patt = R(unpack(vim.tbl_map(table.concat, ranges)))
-    if inv == '!' then
-      patt = P(1) - patt
+--- Additional constraints:
+--- - A Glob pattern must match an entire path, with partial matches
+---   considered failures.
+--- - The pattern only determines success or failure, without specifying
+---   which parts correspond to which characters.
+--- - A *path segment* is the portion of a path between two adjacent path
+---   separators (`/`), or between the start/end of the path and the nearest
+---   separator.
+--- - The `**` (*globstar*) pattern matches zero or more path segments,
+---   including intervening separators (`/`). Within pattern strings, `**`
+---   must be delimited by path separators (`/`) or pattern boundaries and
+---   cannot be adjacent to any characters other than `/`. If `**` is not
+---   the final element, it must be followed by `/`.
+--- - `{}` (*braced conditions*) contains valid Glob patterns as branches,
+---   separated by commas. Commas are exclusively used for separating
+---   branches and cannot appear within a branch for any other purpose.
+---   Nested `{}` structures are allowed, but `{}` must contain at least two
+---   branches—zero or one branch is not permitted.
+--- - In `[]` or `[!...]`, a *character range* consists of character
+---   intervals (e.g., `a-z`) or individual characters (e.g., `w`). A range
+---   including `/` won’t match that character.
+
+--- @diagnostic disable: missing-fields
+
+local m = vim.lpeg
+local mt = getmetatable(m.P(0))
+local re = vim.re
+local bit = require('bit')
+
+local M = {}
+
+-- Basic patterns for matching glob components
+local letter = m.P(1) - m.S(',*?[]{}/\\') -- Any character except special glob characters
+local slash = m.P '/' * m.Cc(m.P '/') -- Path separator with capture
+local notslash = m.P(1) - m.P '/' -- Any character except path separator
+local notcomma = m.P(1) - m.S(',\\') -- Any character except comma and backslash
+
+--- Handle EOF, considering whether we're in a segment or not
+--- @type vim.lpeg.Pattern
+local eof = -1
+  * m.Cb('inseg')
+  / function(flag)
+    if flag then
+      return #m.P '/'
+    else
+      return m.P(-1)
     end
-    return patt
   end
 
-  local function condlist(conds, after)
-    return vim.iter(conds):fold(P(false), function(acc, cond)
-      return acc + cond * after
-    end)
+---@alias pat_table { F: string?, [1]: string, [2]: vim.lpeg.Pattern }
+---@alias seg_part { [string]: any, [integer]: pat_table }
+
+--- @param p pat_table Initial segment pattern data
+--- @return seg_part Segment structure with start pattern
+local function start_seg(p)
+  return { s = p[2], e = true, n = 0 }
+end
+
+--- @param t seg_part Segment structure
+--- @param p pat_table Pattern to look for
+--- @return table Updated segment structure
+local function lookfor(t, p)
+  t.n = t.n + 1
+  t[t.n] = p
+  return t
+end
+
+--- @param t seg_part Segment structure
+--- @return table Segment structure with end pattern
+local function to_seg_end(t)
+  t.e = notslash ^ 0
+  return t
+end
+
+--- Constructs a segment matching pattern from collected components
+---
+--- @param t seg_part Segment structure with patterns
+--- @return vim.lpeg.Pattern Complete segment match pattern
+local function end_seg(t)
+  --- @type table<any,any>
+  local seg_grammar = { 's' }
+  if t.n > 0 then
+    seg_grammar.s = t.s
+    for i = 1, t.n do
+      local rname = t[i][1]
+      if not seg_grammar[rname] then
+        -- Optimize search when deterministic first character is available
+        if t[i].F then
+          seg_grammar[rname] = t[i][2] + notslash * (notslash - m.P(t[i].F)) ^ 0 * m.V(rname)
+        else
+          seg_grammar[rname] = t[i][2] + notslash * m.V(rname)
+        end
+      end
+      seg_grammar.s = seg_grammar.s * m.V(rname)
+    end
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
+    end
+    return m.P(seg_grammar)
+  else
+    seg_grammar.s = t.s
+    if t.e then
+      seg_grammar.s = seg_grammar.s * t.e
+    end
+    return seg_grammar.s
   end
+end
 
-  local function mul(acc, m)
-    return acc * m
+--- @param p vim.lpeg.Pattern Pattern directly after `**/`
+--- @return vim.lpeg.Pattern LPeg pattern for `**/p`
+local function dseg(p)
+  return m.P { p + notslash ^ 0 * m.P '/' * m.V(1) }
+end
+
+--- @type (vim.lpeg.Pattern|table)
+local g = nil
+
+--- Multiplies conditions for braced expansion (Cartesian product)
+---
+--- @param a string|string[] First part
+--- @param b string|string[] Second part
+--- @return string|string[] Cartesian product of values
+local function mul_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return a .. b
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        b[i] = a .. b[i]
+      end
+      return b
+    else
+      return a
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      for i = 1, #a do
+        a[i] = a[i] .. b
+      end
+      return a
+    elseif type(b) == 'table' then
+      --- @type string[]
+      local res = {}
+      local idx = 0
+      for i = 1, #a do
+        for j = 1, #b do
+          idx = idx + 1
+          res[idx] = a[i] .. b[j]
+        end
+      end
+      return res
+    else
+      return a
+    end
+  else
+    return b
   end
+end
 
-  local function star(stars, after)
-    return (-after * (P(1) - pathsep)) ^ #stars * after
+--- Combines alternatives in braced patterns
+---
+--- @param a string|table First part
+--- @param b string|table Second part
+--- @return table #Combined alternatives
+local function add_cond(a, b)
+  if type(a) == 'string' then
+    if type(b) == 'string' then
+      return { a, b }
+    elseif type(b) == 'table' then
+      table.insert(b, 1, a)
+      return b
+    end
+  elseif type(a) == 'table' then
+    if type(b) == 'string' then
+      table.insert(a, b)
+      return a
+    elseif type(b) == 'table' then
+      for i = 1, #b do
+        table.insert(a, b[i])
+      end
+      return a
+    end
+    --- @diagnostic disable-next-line: missing-return
   end
+end
 
-  local function dstar(after)
-    return (-after * P(1)) ^ 0 * after
+--- Expands patterns handling segment boundaries
+--- `#` prefix is added for sub-grammar to detect in-segment flag
+---
+---@param a (any[]|vim.lpeg.Pattern[]) Array of patterns
+---@param b string Tail string
+---@param inseg boolean Whether inside a path segment
+---@return vim.lpeg.Pattern #Expanded pattern
+local function expand(a, b, inseg)
+  for i = 1, #a do
+    if inseg then
+      a[i] = '#' .. a[i]
+    end
+    a[i] = g:match(a[i] .. b)
+  end
+  local res = a[1]
+  for i = 2, #a do
+    res = res + a[i]
   end
+  return res
+end
+
+--- Converts a UTF-8 character to its Unicode codepoint
+---
+--- @param utf8_str string UTF-8 character
+--- @return number #Codepoint value
+local function to_codepoint(utf8_str)
+  local codepoint = 0
+  local byte_count = 0
+
+  for i = 1, #utf8_str do
+    local byte = utf8_str:byte(i)
 
-  -- luacheck: push ignore s
-  local function cut(_s, idx, match)
-    return idx, match
+    if byte_count ~= 0 then
+      codepoint = bit.bor(bit.lshift(codepoint, 6), bit.band(byte, 0x3F))
+      byte_count = byte_count - 1
+    else
+      if byte < 0x80 then
+        codepoint = byte
+      elseif byte < 0xE0 then
+        byte_count = 1
+        codepoint = bit.band(byte, 0x1F)
+      elseif byte < 0xF0 then
+        byte_count = 2
+        codepoint = bit.band(byte, 0x0F)
+      else
+        byte_count = 3
+        codepoint = bit.band(byte, 0x07)
+      end
+    end
+
+    if byte_count == 0 then
+      break
+    end
   end
-  -- luacheck: pop
-
-  --- @diagnostic disable-next-line: missing-fields
-  local p = P({
-    'Pattern',
-    Pattern = V('Elem') ^ -1 * V('End'),
-    Elem = Cmt(
-      Cf(
-        (V('DStar') + V('Star') + V('Ques') + V('Class') + V('CondList') + V('Literal'))
-          * (V('Elem') + V('End')),
-        mul
-      ),
-      cut
-    ),
-    DStar = (B(pathsep) + -B(P(1)))
-      * P('**')
-      * (pathsep * (V('Elem') + V('End')) + V('End'))
-      / dstar,
-    Star = C(P('*') ^ 1) * (V('Elem') + V('End')) / star,
-    Ques = P('?') * Cc(P(1) - pathsep),
-    Class = P('[')
-      * C(P('!') ^ -1)
-      * Ct(Ct(C(P(1)) * P('-') * C(P(1) - P(']'))) ^ 1 * P(']'))
-      / class,
-    CondList = P('{') * Ct(V('Cond') * (P(',') * V('Cond')) ^ 0) * P('}') * V('Pattern') / condlist,
-    -- TODO: '*' inside a {} condition is interpreted literally but should probably have the same
-    -- wildcard semantics it usually has.
-    -- Fixing this is non-trivial because '*' should match non-greedily up to "the rest of the
-    -- pattern" which in all other cases is the entire succeeding part of the pattern, but at the end of a {}
-    -- condition means "everything after the {}" where several other options separated by ',' may
-    -- exist in between that should not be matched by '*'.
-    Cond = Cmt(Cf((V('Ques') + V('Class') + V('Literal') - S(',}')) ^ 1, mul), cut) + Cc(P(0)),
-    Literal = P(1) / P,
-    End = P(-1) * Cc(P(-1)),
-  })
-
-  local lpeg_pattern = p:match(pattern) --[[@as vim.lpeg.Pattern?]]
+
+  return codepoint
+end
+
+--- Pattern for matching UTF-8 characters
+local cont = m.R('\128\191')
+local any_utf8 = m.R('\0\127')
+  + m.R('\194\223') * cont
+  + m.R('\224\239') * cont * cont
+  + m.R('\240\244') * cont * cont * cont
+
+--- Creates a character class pattern for glob ranges
+--- @param inv string Inversion flag ('!' or '')
+--- @param ranges (string|string[])[] Character ranges
+--- @return vim.lpeg.Pattern #Character class pattern
+local function class(inv, ranges)
+  local patt = m.P(false)
+  if #ranges == 0 then
+    if inv == '!' then
+      return m.P '[!]'
+    else
+      return m.P '[]'
+    end
+  end
+  for _, v in ipairs(ranges) do
+    patt = patt + (type(v) == 'table' and m.utfR(to_codepoint(v[1]), to_codepoint(v[2])) or m.P(v))
+  end
+  if inv == '!' then
+    patt = m.P(1) - patt --[[@as vim.lpeg.Pattern]]
+  end
+  return patt - m.P '/'
+end
+
+-- Parse constraints for optimizing braced conditions
+local noopt_condlist = re.compile [[
+  s <- '/' / '**' / . [^/*]* s
+]]
+
+local opt_tail = re.compile [[
+  s <- (!'**' [^{/])* &'/'
+]]
+
+-- stylua: ignore start
+--- @nodoc
+--- @diagnostic disable
+--- Main grammar for glob pattern matching
+g = {
+  'Glob',
+  Glob     = (m.P'#' * m.Cg(m.Cc(true), 'inseg') + m.Cg(m.Cc(false), 'inseg')) *
+             m.Cf(m.V'Element'^-1 * (slash * m.V'Element')^0 * (slash^-1 * eof), mt.__mul),
+  -- Elements handle segments, globstar patterns
+  Element  = m.V'DSeg' + m.V'DSEnd' + m.Cf(m.V'Segment' * (slash * m.V'Segment')^0 * (slash * eof + eof^-1), mt.__mul),
path: runtime/lua/vim/glob.lua
user: login: brynne8
body: I did indeed reference some rule names from the old implementation. I hope this will help Neovim maintainers understand the new implementation more easily.

> will that break any consumers?

Of course not. Using `vim.glob.to_lpeg()` externally doesn't involve directly using the internal rules of the lpeg grammar; it uses the compiled pattern result from lpeg.

I understand your concern. Because LPeg is not just a PEG implementation, but also functions as a parser combinator. In some libraries written with LPeg, there are indeed cases where lpeg is used to create reusable patterns that are combined to generate other patterns. Just like in my code:

```lua
local cont = m.R('\128\191')
local any_utf8 = m.R('\0\127')
  + m.R('\194\223') * cont
  + m.R('\224\239') * cont * cont
  + m.R('\240\244') * cont * cont * cont
```

However, I'm very certain that names like `Element`, `Class`, `CondList`, etc., will not be reused, because they are used directly in the `lpeg.P(<table>)` usage to create a grammar, and the rule names inside the grammar are not visible to the outside. I hope this explanation can address your concerns.
created_at: 2025-05-22T03:49:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: justinmk
created_at: 2025-04-24T13:37:37Z
body: > 1. `eq(false, match('*', ''))`:
>    `*` should match zero or more characters, including an empty string—so it's unclear why `false` is expected here.

agreed. @stevearc ?



> 3\. `eq(true, match('**/', '/a/b/c'))`:
>     The pattern ends with a slash, while the input ends with a character (`c`). It’s unclear why this is expected to match.

I would expect it to match if `c` is a directory.

The other changes seem fine to me. If these don't match "glob semantics from LSP 3.17.0", then we can document the differences. Most of these changes seem like improvements.
author_association: MEMBER
user: login: stevearc
created_at: 2025-04-24T15:31:34Z
body: Unfortunately, I think that we _do_ need the behavior to be standardized to "glob semantics from LSP 3.17.0". The main use case for `vim.glob` right now is interaction with LSP, both in Neovim core and in external plugins. It's being used to power LSP features. If the semantics differ from VS Code, then that _will_ get reported by end users as a bug (e.g. #28931). I don't see much value in a glob implementation that is different, because that that point we may as well just use `glob2regpat()` and have less code to maintain. 
author_association: CONTRIBUTOR
user: login: justinmk
created_at: 2025-04-24T15:56:00Z
body: > The main use case for `vim.glob` right now is interaction with LSP

Yes. Are all of the cases listed above, specified by LSP? Many of these cases are likely confusing. For example does LSP require `eq(false, match('*', ''))` ?

And another example, https://github.com/neovim/neovim/pull/29236 lost a feature but LSP spec doesn't actually specify it one way or another.
author_association: MEMBER
user: login: stevearc
created_at: 2025-04-24T16:33:03Z
body: The spec is here: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
```
The glob pattern to watch relative to the base path. Glob patterns can have
the following syntax:
- `*` to match one or more characters in a path segment
- `?` to match on one character in a path segment
- `**` to match any number of path segments, including none
- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript
  and JavaScript files)
- `[]` to declare a range of characters to match in a path segment
  (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
- `[!...]` to negate a range of characters to match in a path segment
  (e.g., `example.[!0-9]` to match on `example.a`, `example.b`,
  but not `example.0`)
```

1. `eq(false, match('*', ''))`:
This appears to match the spec, because `*` should match "one or more characters in a path segment"

2. `eq(true, match('a**', 'abc'))`:
Same, because we expect `**` to match "any number of path segments, including none"

3. `eq(true, match('**/', '/a/b/c'))`:
I agree that if we look purely at the spec, I would _not_ expect these to match. Curious if that clashes with VS Code's behavior.

4. `eq(true, match('{}', '')) and eq(true, match('{a}', 'a'))`:
The spec doesn't enforce that `{}` has any contents. I agree that it's a weird edge case, and if it's difficult to support I could see an argument for skipping it for now in the hope that users will never encounter it. However, we should be prepared to add support for it in the future if it turns out that users _do_ encounter this pattern in practice.

5. `eq(false, match('{a,{b,c}}', 'c'))`:
The spec doesn't explicitly mention nesting, but I agree with the interpretation that this pattern _should_ match.


Certainly the LSP definition of a glob is _still_ underspecified. My stance is that we should stick to their spec as much as possible, and when there is ambiguity try to match what VS Code does because that is what most LSP server authors are testing against. If there are any cases where the VS Code behavior does _not_ match their spec, I'm fully supportive of sticking to the spec and filing bug reports on VS Code.
author_association: CONTRIBUTOR
user: login: clason
created_at: 2025-04-24T16:36:57Z
body: > Certainly the LSP definition of a glob is still underspecified. My stance is that we should stick to their spec as much as possible, and when there is ambiguity try to match what VS Code does because that is what most LSP server authors are testing against.

No, that is _not_ our policy -- that is and always has been to stick to the spec, _period_, exactly in order to force them to improve their spec where it's underspecified. "VS Cute does this" is very explicitly _not_ a valid argument for any Neovim behavior.
author_association: MEMBER
user: login: brynne8
created_at: 2025-04-24T23:42:08Z
body: The new glob to lpeg converter is implemented with LSP 3.17 in mind as it was a motivation for me to be helpful to Neovim. Another consideration is that what's common practice of using glob. 

The globstar `**` pattern is commonly used and I think it's worth setting a clear boundary between it and star `*` pattern. Barely from the LSP 3.17 spec, of course, zero segments are allowing globstars to be mixed with other in-segment patterns, but it at least is not good practice and is uncommon. In my opinion, this could be solved by clarifying in the spec, enforcing '**/' combinations on specific situations, which is also beneficial for optimizations.

I'll also clarify in the next version of my paper:
![img](https://github.com/user-attachments/assets/59435046-f7bc-4600-aeab-6bc2885ab2ac)

author_association: CONTRIBUTOR
user: login: brynne8
created_at: 2025-04-25T03:26:15Z
body: The original test result for correctness of current `vim.glob.to_lpeg` could be an reason for migrating to new implementation:

```bash
$ luajit testvimglob.lua 
  Assertion failed for pattern '*' matching '': expected true, got nil
  Assertion failed for pattern 'a*' matching 'a': expected true, got nil
  Assertion failed for pattern '*a' matching 'a': expected true, got nil
  Assertion failed for pattern 'a\*b' matching 'a*b': expected true, got nil
  Assertion failed for pattern '[abc]' matching 'a': expected true, got nil
  Assertion failed for pattern '[abc]' matching 'b': expected true, got nil
  Assertion failed for pattern '[abc]' matching 'c': expected true, got nil
  Assertion failed for pattern 'x[abc]x' matching 'xax': expected true, got nil
  Assertion failed for pattern 'x[abc]x' matching 'xbx': expected true, got nil
  Assertion failed for pattern 'x[abc]x' matching 'xcx': expected true, got nil
  Assertion failed for pattern '[?]' matching '?': expected true, got nil
  Assertion failed for pattern '[*]' matching '*': expected true, got nil
  Assertion failed for pattern '[a-cx]' matching 'a': expected true, got nil
  Assertion failed for pattern '[a-cx]' matching 'b': expected true, got nil
  Assertion failed for pattern '[a-cx]' matching 'c': expected true, got nil
  Assertion failed for pattern '[a-cx]' matching 'x': expected true, got nil
  Assertion failed for pattern '[!abc]' matching 'd': expected true, got nil
  Assertion failed for pattern '[\!]' matching '!': expected true, got nil
Basic pattern tests complete
Test Summary: 35/53 tests passed
Accuracy: 66.04%
Precision: 48.57%

  Assertion failed for pattern '*' matching '': expected true, got nil
  Assertion failed for pattern '*.ts' matching '.ts': expected true, got nil
  Assertion failed for pattern 'a*b*c*d*e*' matching 'axbxcxdxe': expected true, got nil
  Assertion failed for pattern 'a*b*[cy]*d*e*' matching 'axbxcxdxexxx': expected true, got nil
  Assertion failed for pattern 'a*b*[cy]*d*e*' matching 'axbxyxdxexxx': expected true, got nil
  Assertion failed for pattern 'a*b*[cy]*d*e*' matching 'axbxxxyxdxexxx': expected true, got nil
  Assertion failed for pattern 'z*.js' matching 'z.js': expected true, got nil
  Assertion failed for pattern '*/z*.js' matching 'a/z.js': expected true, got nil
  Assertion failed for pattern 'a/z*.js' matching 'a/z.js': expected true, got nil
  Assertion failed for pattern '*/*' matching '/ab': expected true, got nil
  Assertion failed for pattern '*/*z*/*/*i' matching 'ab/zzz/ejkl/hi': expected true, got nil
  Assertion failed for pattern 'a*.txt' matching 'a.txt': expected true, got nil
  Assertion failed for pattern 'a/**/b' matching 'a/a/bb': expected false, got 7
  Assertion failed for pattern 'a/**/b' matching 'a/bb': expected false, got 5
  Assertion failed for pattern '**/' matching 'foo/bar': expected false, got 8
  Assertion failed for pattern '**/' matching 'a/a': expected false, got 4
  Assertion failed for pattern '**/' matching 'a/a': expected false, got 4
Star tests complete
Test Summary: 202/219 tests passed
Accuracy: 92.24%
Precision: 89.29%

  Assertion failed for pattern 'a/**/b' matching 'a/bb': expected false, got 5
  Assertion failed for pattern 'a/**/' matching 'a/b': expected false, got 4
  Assertion failed for pattern 'a/**/' matching 'a/b/c/d': expected false, got 8
  Assertion failed for pattern 'a/**/' matching 'a/bb': expected false, got 5
  Assertion failed for pattern 'a/**/' matching 'a/cb': expected false, got 5
  Assertion failed for pattern '**/' matching 'a': expected false, got 2
  Assertion failed for pattern '**/' matching 'a/b': expected false, got 4
  Assertion failed for pattern '**/' matching 'a/b/c': expected false, got 6
  Assertion failed for pattern '**/' matching 'a/b/c/d': expected false, got 8
Globstar tests complete
Test Summary: 186/195 tests passed
Accuracy: 95.38%
Precision: 100.00%

  Assertion failed for pattern 'foo[{a,b}]baz' matching 'foo{baz': expected true, got nil
  Assertion failed for pattern 'a{a,b/}*.txt' matching 'aa.txt': expected true, got nil
  Assertion failed for pattern 'a{a,b/}*.txt' matching 'ab/.txt': expected true, got nil
  Assertion failed for pattern 'a{,/}*.txt' matching 'a.txt': expected true, got nil
  Assertion failed for pattern 'a{,.*{foo,db},\(bar\)}.txt' matching 'a.txt': expected true, got nil
  Assertion failed for pattern 'a{,.*{foo,db},\(bar\)}.txt' matching 'a.db.txt': expected true, got nil
  Assertion failed for pattern 'a{,*.{foo,db},\(bar\)}.txt' matching 'a.txt': expected true, got nil
  Assertion failed for pattern 'a{,*.{foo,db},\(bar\)}.txt' matching 'a.db.txt': expected true, got nil
  Assertion failed for pattern 'a{,.*{foo,db},\(bar\)}' matching 'a.db': expected true, got nil
  Assertion failed for pattern 'a{,*.{foo,db},\(bar\)}' matching 'a.db': expected true, got nil
  Assertion failed for pattern '{,.*{foo,db},\(bar\)}' matching '.db': expected true, got nil
  Assertion failed for pattern '{*,*.{foo,db},\(bar\)}' matching 'a': expected true, got nil
  Assertion failed for pattern '{,*.{foo,db},\(bar\)}' matching 'a.db': expected true, got nil
  Assertion failed for pattern '*{*a,b}' matching 'xba': expected true, got nil
  Assertion failed for pattern 'test.{j*g,p*g}' matching 'test.jpg': expected true, got nil
  Assertion failed for pattern 'test.{j*g,p*g}' matching 'test.jpxxxg': expected true, got nil
  Assertion failed for pattern 'test.{j*g,p*g}' matching 'test.jxg': expected true, got nil
  Assertion failed for pattern 'test.{j*g,j*c}' matching 'test.jnc': expected true, got nil
  Assertion failed for pattern 'test.{jpg,p*g}' matching 'test.png': expected true, got nil
  Assertion failed for pattern 'test.{jpg,p*g}' matching 'test.pxg': expected true, got nil
  Assertion failed for pattern 'test.{jp\,g,png}' matching 'test.jp,g': expected true, got nil
  Assertion failed for pattern 'test/{foo*,bar*}/baz' matching 'test/foooooo/baz': expected true, got nil
  Assertion failed for pattern 'test/{foo*,bar*}/baz' matching 'test/barrrrr/baz': expected true, got nil
  Assertion failed for pattern 'test/{*foo,*bar}/baz' matching 'test/xxxxfoo/baz': expected true, got nil
  Assertion failed for pattern 'test/{*foo,*bar}/baz' matching 'test/xxxxbar/baz': expected true, got nil
  Assertion failed for pattern 'a/{a{a,b},b}' matching 'a/aa': expected true, got nil
  Assertion failed for pattern 'a/{a{a,b},b}' matching 'a/ab': expected true, got nil
  Assertion failed for pattern 'a/{a{a,b},b}' matching 'a/b': expected true, got nil
  Assertion failed for pattern 'a/{b,c[}]*}' matching 'a/b': expected true, got nil
  Assertion failed for pattern 'a/{b,c[}]*}' matching 'a/c}xx': expected true, got nil
Brace expansion tests complete
Test Summary: 71/101 tests passed
Accuracy: 70.30%
Precision: 58.33%

  Assertion failed for pattern 'F[ë£a]' matching 'Fë': expected true, got nil
  Assertion failed for pattern 'F[ë£a]' matching 'F£': expected true, got nil
  Assertion failed for pattern 'F[ë£a]' matching 'Fa': expected true, got nil
Unicode pattern tests complete
Test Summary: 8/11 tests passed
Accuracy: 72.73%
Precision: 70.00%

  Assertion failed for pattern '**-**.**-**' matching 'a-b.c-d': expected true, got nil
  Assertion failed for pattern '**-b**c-**' matching 'a-b.c-d': expected true, got nil
  Assertion failed for pattern '**-b.c-**' matching 'a-b.c-d': expected true, got nil
  Assertion failed for pattern '**.**-**' matching 'a-b.c-d': expected true, got nil
  Assertion failed for pattern '**.**-d' matching 'a-b.c-d': expected true, got nil
  Assertion failed for pattern '**.c-**' matching 'a-b.c-d': expected true, got nil
  Assertion failed for pattern 'a-**.**-d' matching 'a-b.c-d': expected true, got nil
  Assertion failed for pattern '**.**' matching 'a.b': expected true, got nil
  Assertion failed for pattern '**.b' matching 'a.b': expected true, got nil
  Assertion failed for pattern 'a.**' matching 'a.b': expected true, got nil
  Assertion failed for pattern 'a/**b' matching 'a/b': expected true, got nil
  Assertion failed for pattern 'a/**b**/c' matching 'a/aba/c': expected true, got nil
  Assertion failed for pattern 'a/**b**/c' matching 'a/b/c': expected true, got nil
  Assertion failed for pattern 'a/b/c**/*.js' matching 'a/b/c/d.js': expected true, got nil
Extension tests complete
Test Summary: 10/24 tests passed
Accuracy: 41.67%
Precision: 17.65%
```

And here's my Peglob's result:

```bash
$ luajit testpeglob.lua 
Basic pattern tests complete
Test Summary: 53/53 tests passed
Accuracy: 100.00%
Precision: 100.00%

Star tests complete
Test Summary: 219/219 tests passed
Accuracy: 100.00%
Precision: 100.00%

  Assertion failed for pattern 'a/**/*' matching 'a/': expected false, got 3
  Assertion failed for pattern 'a/**/**/*' matching 'a/': expected false, got 3
  Assertion failed for pattern 'a/**/**/**/*' matching 'a/': expected false, got 3
Globstar tests complete
Test Summary: 192/195 tests passed
Accuracy: 98.46%
Precision: 100.00%

  Invalid grammar for pattern 'a/{b,c[}]*}' matching 'a/b'
  Invalid grammar for pattern 'a/{b,c[}]*}' matching 'a/c}xx'
Brace expansion tests complete
Test Summary: 99/101 tests passed
Accuracy: 98.02%
Precision: 97.22%

Unicode pattern tests complete
Test Summary: 11/11 tests passed
Accuracy: 100.00%
Precision: 100.00%

  Invalid grammar for pattern '**-bc-**' matching 'a-b.c-d'
  Invalid grammar for pattern '**-**.**-**' matching 'a-b.c-d'
  Invalid grammar for pattern '**-b**c-**' matching 'a-b.c-d'
  Invalid grammar for pattern '**-b.c-**' matching 'a-b.c-d'
  Invalid grammar for pattern '**.**' matching 'a-b.c-d'
  Invalid grammar for pattern '**.**-**' matching 'a-b.c-d'
  Invalid grammar for pattern '**.**-d' matching 'a-b.c-d'
  Invalid grammar for pattern '**.c-**' matching 'a-b.c-d'
  Invalid grammar for pattern '**b.**d' matching 'a-b.c-d'
  Invalid grammar for pattern 'a**.c**' matching 'a-b.c-d'
  Invalid grammar for pattern 'a-**.**-d' matching 'a-b.c-d'
  Invalid grammar for pattern '**.**' matching 'a.b'
  Invalid grammar for pattern '**.b' matching 'a.b'
  Invalid grammar for pattern 'a.**' matching 'a.b'
  Invalid grammar for pattern '**c' matching 'a/b/c'
  Invalid grammar for pattern 'a/**b' matching 'a/b'
  Invalid grammar for pattern 'a/**c' matching 'a/b/c'
  Invalid grammar for pattern 'a/**z' matching 'a/b/c'
  Invalid grammar for pattern 'a/**b**/c' matching 'a/b/c/b/c'
  Invalid grammar for pattern 'a/**b**/c' matching 'a/aba/c'
  Invalid grammar for pattern 'a/**b**/c' matching 'a/b/c'
  Invalid grammar for pattern 'a/b/c**/*.js' matching 'a/b/c/d.js'
  Invalid grammar for pattern 'a/b/c**/*.js' matching 'a/b/c/d/e.js'
  Invalid grammar for pattern 'foo**bar' matching 'foo/baz/bar'
Extension tests complete
Test Summary: 0/24 tests passed
Accuracy: 0.00%
Precision: 0.00%
```

And the performance could be boosted a lot for complex patterns. (The data isn't covered in the paper because less than half the tests match as expected)

```bash
Peglob
{src,extensions}/**/test/**/{fixtures,browser,common}/**/*.{ts,js} time: 2.68 ms
{extensions,src}/**/{media,images,icons}/**/*.{svg,png,gif,jpg} time: 2.36 ms
{.github,build,test}/**/{workflows,azure-pipelines,integration,smoke}/**/*.{yml,yaml,json} time: 1.00 ms
src/vs/{base,editor,platform,workbench}/test/{browser,common,node}/**/[a-z]*[tT]est.ts time: 1.27 ms
src/vs/workbench/{contrib,services}/**/*{Editor,Workspace,Terminal}*.ts time: 2.89 ms
{extensions,src}/**/{markdown,json,javascript,typescript}/**/*.{ts,json} time: 2.70 ms
**/{electron-sandbox,electron-main,browser,node}/**/{*[sS]ervice*,*[cC]ontroller*}.ts time: 4.86 ms
{src,extensions}/**/{common,browser,electron-sandbox}/**/*{[cC]ontribution,[sS]ervice}.ts time: 4.31 ms
src/vs/{base,platform,workbench}/**/{test,browser}/**/*{[mM]odel,[cC]ontroller}*.ts time: 3.08 ms
extensions/**/{browser,common,node}/{**/*[sS]ervice*,**/*[pP]rovider*}.ts time: 1.53 ms
---
NeoVim Glob
{src,extensions}/**/test/**/{fixtures,browser,common}/**/*.{ts,js} (743 matched), expected 726
{extensions,src}/**/{media,images,icons}/**/*.{svg,png,gif,jpg} time: 4.30 ms
{.github,build,test}/**/{workflows,azure-pipelines,integration,smoke}/**/*.{yml,yaml,json} time: 1.29 ms
src/vs/{base,editor,platform,workbench}/test/{browser,common,node}/**/[a-z]*[tT]est.ts (0 matched), expected 224
src/vs/workbench/{contrib,services}/**/*{Editor,Workspace,Terminal}*.ts (126 matched), expected 155
{extensions,src}/**/{markdown,json,javascript,typescript}/**/*.{ts,json} time: 4.08 ms
**/{electron-sandbox,electron-main,browser,node}/**/{*[sS]ervice*,*[cC]ontroller*}.ts (0 matched), expected 419
{src,extensions}/**/{common,browser,electron-sandbox}/**/*{[cC]ontribution,[sS]ervice}.ts (0 matched), expected 586
src/vs/{base,platform,workbench}/**/{test,browser}/**/*{[mM]odel,[cC]ontroller}*.ts (0 matched), expected 95
extensions/**/{browser,common,node}/{**/*Service*,**/*Provider*}.ts (0 matched), expected 3
```
author_association: CONTRIBUTOR
user: login: clason
created_at: 2025-04-25T08:06:19Z
body: You need to `make formatlua` and `make doc`.

(And my point was not to argue for or against a specific choice, just saying that getting the specs clarified is the urgent first step here. If that codifies VS Code's behavior, so be it. It's just important to enforce the point that the _only_ source of truth is the specification, not VS Code.)
author_association: MEMBER
user: login: clason
created_at: 2025-04-25T09:25:55Z
body: You can (and should) put the "boilerplate" doc text in the docstring for the module; see other e.g. LSP modules for the format. 

(And please squash your commits when you're done; we expect a commit message of the form `feat(lpeg): ...`. Consider adding details from your PR description as well (since the commit log will survive even if Github burns down. But that can wait until the PR is ready to be merged.)
author_association: MEMBER
user: login: brynne8
created_at: 2025-04-26T08:15:06Z
body: Now lint errors in source code has been solved. Still not sure whether my proposed glob spec has been accepted. The last 2 tasks for me are changing the tests and squashing the commits.

When solving lint issues, I found LuaLS is tricky, `table<any,any>` isn't equivalent with `table`, and it's forcing me to write type like `string[]` than `table`. Even throwing error cannot suppress the `missing return` check. And I cannot understand why `m.P(1) - patt` cannot infer type.
author_association: CONTRIBUTOR
user: login: justinmk
created_at: 2025-04-26T13:23:05Z
body: I'm in favor of trying this out on `master`, that's the purpose of the "development branch" :)

If there are some high-risk changes that could be dropped/postponed, that would be helpful. In particular, I expect changing the behavior of `match('a**', 'abc'))` will cause a lot of bug reports, so if we can delay that change to a followup PR, that would greatly reduce the risk of this one.



> I cannot understand why `m.P(1) - patt` cannot infer type.

You can use `--[[@as ...]]` or `--[[@type ...]]` to force a type. Many such examples in the codebase.
author_association: MEMBER
user: login: brynne8
created_at: 2025-04-27T01:10:17Z
body: @justinmk @stevearc 

I looked into the behavior of `*` in VS Code’s glob implementation and found that it actually matches **zero** or more characters in a path segment—not one or more as LSP 3.17 states. Specifically:

- In the VS Code source ([glob.ts](https://github.com/microsoft/vscode/blob/main/src/vs/base/common/glob.ts#L520)), the comment reads “`*` to match zero or more characters in a path segment.”  
- The [“Glob Patterns” documentation on code.visualstudio.com](https://code.visualstudio.com/docs/editor/glob-patterns) also says “zero or more.”  
- Inspecting the implementation:

  ```ts
  function starsToRegExp(starCount: number, isLastPattern?: boolean): string {
    switch (starCount) {
      case 0:
        return '';
      case 1:
        // 1 star: match any number of characters except path separators (/ and \), non-greedy
        return `${NO_PATH_REGEX}*?`;
      default:
        // handler for **
    }
  }
  ```

  In regex syntax, `*` means “zero or more” of the preceding pattern, and the `?` makes it non-greedy.

So it looks like LSP 3.17’s description of `*` is simply a typo.
author_association: CONTRIBUTOR
user: login: clason
created_at: 2025-04-27T08:29:56Z
body: > So it looks like LSP 3.17’s description of * is simply a typo.

Nevertheless, it is the spec. And, again, we follow the spec and not VS Code. If you think the spec is in error, please raise an issue there.

I'm setting this to draft until these issues are resolved. While this refactor looks like something we want in general, we do **not** want an implementation that diverges from the spec. That is non-negotiable.
author_association: MEMBER
user: login: justinmk
created_at: 2025-04-27T11:38:12Z
body: > If you think the spec is in error, please raise an issue there.

To unblock this, link to the issue from the lsp issue-tracker here. Then we can decide how to proceed. If they say "the spec is wrong" in the issue, that's good enough. We don't need to wait on them to update the spec.
author_association: MEMBER
user: login: brynne8
created_at: 2025-04-29T07:33:54Z
body: > To unblock this, link to the issue from the lsp issue-tracker here. Then we can decide how to proceed. If they say "the spec is wrong" in the issue, that's good enough. We don't need to wait on them to update the spec.

The typo in LSP 3.17 could be tracked at https://github.com/microsoft/language-server-protocol/pull/2132 . I'm hoping we could proceed soon.
author_association: CONTRIBUTOR
user: login: brynne8
created_at: 2025-05-13T14:39:09Z
body: @justinmk Update from https://github.com/microsoft/language-server-protocol/pull/2132 shows that `*` means zero or more characters and maybe this draft could be reviewed.
author_association: CONTRIBUTOR
user: login: clason
created_at: 2025-05-13T14:41:41Z
body: Since long comment chains quickly get hidden to the point of being useless, could you please edit your PR on the state of the "five key cases" (which I consider a list of blockers to be resolved, one way or another)? Checkmarked list could be helpful.
author_association: MEMBER
user: login: brynne8
created_at: 2025-05-14T02:18:23Z
body: > Since long comment chains quickly get hidden to the point of being useless, could you please edit your PR on the state of the "five key cases" (which I consider a list of blockers to be resolved, one way or another)? Checkmarked list could be helpful.

@clason Thanks for the nudge! I’ve updated the PR description to include the current state of the five key cases. Please let me know if anything needs further adjustment
author_association: CONTRIBUTOR
user: login: brynne8
created_at: 2025-05-22T03:54:34Z
body: > Please add a news.txt description, either under PERFORMANCE or perhaps LUA. If this improves performance, give a hint about the most notable cases: `vim.glob performance for common patterns like "foo" improved by 5x"`

@justinmk Done adding a news.txt description. I also squashed my commits into a single commit.
author_association: CONTRIBUTOR