mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			501 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			501 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
local P = {}
 | 
						|
 | 
						|
---Take characters until the target characters (The escape sequence is '\' + char)
 | 
						|
---@param targets string[] The character list for stop consuming text.
 | 
						|
---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
 | 
						|
P.take_until = function(targets, specials)
 | 
						|
  targets = targets or {}
 | 
						|
  specials = specials or {}
 | 
						|
 | 
						|
  return function(input, pos)
 | 
						|
    local new_pos = pos
 | 
						|
    local raw = {}
 | 
						|
    local esc = {}
 | 
						|
    while new_pos <= #input do
 | 
						|
      local c = string.sub(input, new_pos, new_pos)
 | 
						|
      if c == '\\' then
 | 
						|
        table.insert(raw, '\\')
 | 
						|
        new_pos = new_pos + 1
 | 
						|
        c = string.sub(input, new_pos, new_pos)
 | 
						|
        if not vim.tbl_contains(targets, c) and not vim.tbl_contains(specials, c) then
 | 
						|
          table.insert(esc, '\\')
 | 
						|
        end
 | 
						|
        table.insert(raw, c)
 | 
						|
        table.insert(esc, c)
 | 
						|
        new_pos = new_pos + 1
 | 
						|
      else
 | 
						|
        if vim.tbl_contains(targets, c) then
 | 
						|
          break
 | 
						|
        end
 | 
						|
        table.insert(raw, c)
 | 
						|
        table.insert(esc, c)
 | 
						|
        new_pos = new_pos + 1
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    if new_pos == pos then
 | 
						|
      return P.unmatch(pos)
 | 
						|
    end
 | 
						|
 | 
						|
    return {
 | 
						|
      parsed = true,
 | 
						|
      value = {
 | 
						|
        raw = table.concat(raw, ''),
 | 
						|
        esc = table.concat(esc, ''),
 | 
						|
      },
 | 
						|
      pos = new_pos,
 | 
						|
    }
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.unmatch = function(pos)
 | 
						|
  return {
 | 
						|
    parsed = false,
 | 
						|
    value = nil,
 | 
						|
    pos = pos,
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
P.map = function(parser, map)
 | 
						|
  return function(input, pos)
 | 
						|
    local result = parser(input, pos)
 | 
						|
    if result.parsed then
 | 
						|
      return {
 | 
						|
        parsed = true,
 | 
						|
        value = map(result.value),
 | 
						|
        pos = result.pos,
 | 
						|
      }
 | 
						|
    end
 | 
						|
    return P.unmatch(pos)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.lazy = function(factory)
 | 
						|
  return function(input, pos)
 | 
						|
    return factory()(input, pos)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.token = function(token)
 | 
						|
  return function(input, pos)
 | 
						|
    local maybe_token = string.sub(input, pos, pos + #token - 1)
 | 
						|
    if token == maybe_token then
 | 
						|
      return {
 | 
						|
        parsed = true,
 | 
						|
        value = maybe_token,
 | 
						|
        pos = pos + #token,
 | 
						|
      }
 | 
						|
    end
 | 
						|
    return P.unmatch(pos)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.pattern = function(p)
 | 
						|
  return function(input, pos)
 | 
						|
    local maybe_match = string.match(string.sub(input, pos), '^' .. p)
 | 
						|
    if maybe_match then
 | 
						|
      return {
 | 
						|
        parsed = true,
 | 
						|
        value = maybe_match,
 | 
						|
        pos = pos + #maybe_match,
 | 
						|
      }
 | 
						|
    end
 | 
						|
    return P.unmatch(pos)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.many = function(parser)
 | 
						|
  return function(input, pos)
 | 
						|
    local values = {}
 | 
						|
    local new_pos = pos
 | 
						|
    while new_pos <= #input do
 | 
						|
      local result = parser(input, new_pos)
 | 
						|
      if not result.parsed then
 | 
						|
        break
 | 
						|
      end
 | 
						|
      table.insert(values, result.value)
 | 
						|
      new_pos = result.pos
 | 
						|
    end
 | 
						|
    if #values > 0 then
 | 
						|
      return {
 | 
						|
        parsed = true,
 | 
						|
        value = values,
 | 
						|
        pos = new_pos,
 | 
						|
      }
 | 
						|
    end
 | 
						|
    return P.unmatch(pos)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.any = function(...)
 | 
						|
  local parsers = { ... }
 | 
						|
  return function(input, pos)
 | 
						|
    for _, parser in ipairs(parsers) do
 | 
						|
      local result = parser(input, pos)
 | 
						|
      if result.parsed then
 | 
						|
        return result
 | 
						|
      end
 | 
						|
    end
 | 
						|
    return P.unmatch(pos)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.opt = function(parser)
 | 
						|
  return function(input, pos)
 | 
						|
    local result = parser(input, pos)
 | 
						|
    return {
 | 
						|
      parsed = true,
 | 
						|
      value = result.value,
 | 
						|
      pos = result.pos,
 | 
						|
    }
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
P.seq = function(...)
 | 
						|
  local parsers = { ... }
 | 
						|
  return function(input, pos)
 | 
						|
    local values = {}
 | 
						|
    local new_pos = pos
 | 
						|
    for i, parser in ipairs(parsers) do
 | 
						|
      local result = parser(input, new_pos)
 | 
						|
      if result.parsed then
 | 
						|
        values[i] = result.value
 | 
						|
        new_pos = result.pos
 | 
						|
      else
 | 
						|
        return P.unmatch(pos)
 | 
						|
      end
 | 
						|
    end
 | 
						|
    return {
 | 
						|
      parsed = true,
 | 
						|
      value = values,
 | 
						|
      pos = new_pos,
 | 
						|
    }
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
local Node = {}
 | 
						|
 | 
						|
Node.Type = {
 | 
						|
  SNIPPET = 0,
 | 
						|
  TABSTOP = 1,
 | 
						|
  PLACEHOLDER = 2,
 | 
						|
  VARIABLE = 3,
 | 
						|
  CHOICE = 4,
 | 
						|
  TRANSFORM = 5,
 | 
						|
  FORMAT = 6,
 | 
						|
  TEXT = 7,
 | 
						|
}
 | 
						|
 | 
						|
function Node:__tostring()
 | 
						|
  local insert_text = {}
 | 
						|
  if self.type == Node.Type.SNIPPET then
 | 
						|
    for _, c in ipairs(self.children) do
 | 
						|
      table.insert(insert_text, tostring(c))
 | 
						|
    end
 | 
						|
  elseif self.type == Node.Type.CHOICE then
 | 
						|
    table.insert(insert_text, self.items[1])
 | 
						|
  elseif self.type == Node.Type.PLACEHOLDER then
 | 
						|
    for _, c in ipairs(self.children or {}) do
 | 
						|
      table.insert(insert_text, tostring(c))
 | 
						|
    end
 | 
						|
  elseif self.type == Node.Type.TEXT then
 | 
						|
    table.insert(insert_text, self.esc)
 | 
						|
  end
 | 
						|
  return table.concat(insert_text, '')
 | 
						|
end
 | 
						|
 | 
						|
--@see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
 | 
						|
 | 
						|
local S = {}
 | 
						|
S.dollar = P.token('$')
 | 
						|
S.open = P.token('{')
 | 
						|
S.close = P.token('}')
 | 
						|
S.colon = P.token(':')
 | 
						|
S.slash = P.token('/')
 | 
						|
S.comma = P.token(',')
 | 
						|
S.pipe = P.token('|')
 | 
						|
S.plus = P.token('+')
 | 
						|
S.minus = P.token('-')
 | 
						|
S.question = P.token('?')
 | 
						|
S.int = P.map(P.pattern('[0-9]+'), function(value)
 | 
						|
  return tonumber(value, 10)
 | 
						|
end)
 | 
						|
S.var = P.pattern('[%a_][%w_]+')
 | 
						|
S.text = function(targets, specials)
 | 
						|
  return P.map(P.take_until(targets, specials), function(value)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.TEXT,
 | 
						|
      raw = value.raw,
 | 
						|
      esc = value.esc,
 | 
						|
    }, Node)
 | 
						|
  end)
 | 
						|
end
 | 
						|
 | 
						|
S.toplevel = P.lazy(function()
 | 
						|
  return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
 | 
						|
end)
 | 
						|
 | 
						|
S.format = P.any(
 | 
						|
  P.map(P.seq(S.dollar, S.int), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.FORMAT,
 | 
						|
      capture_index = values[2],
 | 
						|
    }, Node)
 | 
						|
  end),
 | 
						|
  P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.FORMAT,
 | 
						|
      capture_index = values[3],
 | 
						|
    }, Node)
 | 
						|
  end),
 | 
						|
  P.map(
 | 
						|
    P.seq(
 | 
						|
      S.dollar,
 | 
						|
      S.open,
 | 
						|
      S.int,
 | 
						|
      S.colon,
 | 
						|
      S.slash,
 | 
						|
      P.any(
 | 
						|
        P.token('upcase'),
 | 
						|
        P.token('downcase'),
 | 
						|
        P.token('capitalize'),
 | 
						|
        P.token('camelcase'),
 | 
						|
        P.token('pascalcase')
 | 
						|
      ),
 | 
						|
      S.close
 | 
						|
    ),
 | 
						|
    function(values)
 | 
						|
      return setmetatable({
 | 
						|
        type = Node.Type.FORMAT,
 | 
						|
        capture_index = values[3],
 | 
						|
        modifier = values[6],
 | 
						|
      }, Node)
 | 
						|
    end
 | 
						|
  ),
 | 
						|
  P.map(
 | 
						|
    P.seq(
 | 
						|
      S.dollar,
 | 
						|
      S.open,
 | 
						|
      S.int,
 | 
						|
      S.colon,
 | 
						|
      P.seq(
 | 
						|
        S.question,
 | 
						|
        P.opt(P.take_until({ ':' }, { '\\' })),
 | 
						|
        S.colon,
 | 
						|
        P.opt(P.take_until({ '}' }, { '\\' }))
 | 
						|
      ),
 | 
						|
      S.close
 | 
						|
    ),
 | 
						|
    function(values)
 | 
						|
      return setmetatable({
 | 
						|
        type = Node.Type.FORMAT,
 | 
						|
        capture_index = values[3],
 | 
						|
        if_text = values[5][2] and values[5][2].esc or '',
 | 
						|
        else_text = values[5][4] and values[5][4].esc or '',
 | 
						|
      }, Node)
 | 
						|
    end
 | 
						|
  ),
 | 
						|
  P.map(
 | 
						|
    P.seq(
 | 
						|
      S.dollar,
 | 
						|
      S.open,
 | 
						|
      S.int,
 | 
						|
      S.colon,
 | 
						|
      P.seq(S.plus, P.opt(P.take_until({ '}' }, { '\\' }))),
 | 
						|
      S.close
 | 
						|
    ),
 | 
						|
    function(values)
 | 
						|
      return setmetatable({
 | 
						|
        type = Node.Type.FORMAT,
 | 
						|
        capture_index = values[3],
 | 
						|
        if_text = values[5][2] and values[5][2].esc or '',
 | 
						|
        else_text = '',
 | 
						|
      }, Node)
 | 
						|
    end
 | 
						|
  ),
 | 
						|
  P.map(
 | 
						|
    P.seq(
 | 
						|
      S.dollar,
 | 
						|
      S.open,
 | 
						|
      S.int,
 | 
						|
      S.colon,
 | 
						|
      S.minus,
 | 
						|
      P.opt(P.take_until({ '}' }, { '\\' })),
 | 
						|
      S.close
 | 
						|
    ),
 | 
						|
    function(values)
 | 
						|
      return setmetatable({
 | 
						|
        type = Node.Type.FORMAT,
 | 
						|
        capture_index = values[3],
 | 
						|
        if_text = '',
 | 
						|
        else_text = values[6] and values[6].esc or '',
 | 
						|
      }, Node)
 | 
						|
    end
 | 
						|
  ),
 | 
						|
  P.map(
 | 
						|
    P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.take_until({ '}' }, { '\\' })), S.close),
 | 
						|
    function(values)
 | 
						|
      return setmetatable({
 | 
						|
        type = Node.Type.FORMAT,
 | 
						|
        capture_index = values[3],
 | 
						|
        if_text = '',
 | 
						|
        else_text = values[5] and values[5].esc or '',
 | 
						|
      }, Node)
 | 
						|
    end
 | 
						|
  )
 | 
						|
)
 | 
						|
 | 
						|
S.transform = P.map(
 | 
						|
  P.seq(
 | 
						|
    S.slash,
 | 
						|
    P.take_until({ '/' }, { '\\' }),
 | 
						|
    S.slash,
 | 
						|
    P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))),
 | 
						|
    S.slash,
 | 
						|
    P.opt(P.pattern('[ig]+'))
 | 
						|
  ),
 | 
						|
  function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.TRANSFORM,
 | 
						|
      pattern = values[2].raw,
 | 
						|
      format = values[4],
 | 
						|
      option = values[6],
 | 
						|
    }, Node)
 | 
						|
  end
 | 
						|
)
 | 
						|
 | 
						|
S.tabstop = P.any(
 | 
						|
  P.map(P.seq(S.dollar, S.int), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.TABSTOP,
 | 
						|
      tabstop = values[2],
 | 
						|
    }, Node)
 | 
						|
  end),
 | 
						|
  P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.TABSTOP,
 | 
						|
      tabstop = values[3],
 | 
						|
    }, Node)
 | 
						|
  end),
 | 
						|
  P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.TABSTOP,
 | 
						|
      tabstop = values[3],
 | 
						|
      transform = values[4],
 | 
						|
    }, Node)
 | 
						|
  end)
 | 
						|
)
 | 
						|
 | 
						|
S.placeholder = P.any(
 | 
						|
  P.map(
 | 
						|
    P.seq(
 | 
						|
      S.dollar,
 | 
						|
      S.open,
 | 
						|
      S.int,
 | 
						|
      S.colon,
 | 
						|
      P.opt(P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' })))),
 | 
						|
      S.close
 | 
						|
    ),
 | 
						|
    function(values)
 | 
						|
      return setmetatable({
 | 
						|
        type = Node.Type.PLACEHOLDER,
 | 
						|
        tabstop = values[3],
 | 
						|
        -- insert empty text if opt did not match.
 | 
						|
        children = values[5] or {
 | 
						|
          setmetatable({
 | 
						|
            type = Node.Type.TEXT,
 | 
						|
            raw = '',
 | 
						|
            esc = '',
 | 
						|
          }, Node),
 | 
						|
        },
 | 
						|
      }, Node)
 | 
						|
    end
 | 
						|
  )
 | 
						|
)
 | 
						|
 | 
						|
S.choice = P.map(
 | 
						|
  P.seq(
 | 
						|
    S.dollar,
 | 
						|
    S.open,
 | 
						|
    S.int,
 | 
						|
    S.pipe,
 | 
						|
    P.many(P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
 | 
						|
      return values[1].esc
 | 
						|
    end)),
 | 
						|
    S.pipe,
 | 
						|
    S.close
 | 
						|
  ),
 | 
						|
  function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.CHOICE,
 | 
						|
      tabstop = values[3],
 | 
						|
      items = values[5],
 | 
						|
    }, Node)
 | 
						|
  end
 | 
						|
)
 | 
						|
 | 
						|
S.variable = P.any(
 | 
						|
  P.map(P.seq(S.dollar, S.var), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.VARIABLE,
 | 
						|
      name = values[2],
 | 
						|
    }, Node)
 | 
						|
  end),
 | 
						|
  P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.VARIABLE,
 | 
						|
      name = values[3],
 | 
						|
    }, Node)
 | 
						|
  end),
 | 
						|
  P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
 | 
						|
    return setmetatable({
 | 
						|
      type = Node.Type.VARIABLE,
 | 
						|
      name = values[3],
 | 
						|
      transform = values[4],
 | 
						|
    }, Node)
 | 
						|
  end),
 | 
						|
  P.map(
 | 
						|
    P.seq(
 | 
						|
      S.dollar,
 | 
						|
      S.open,
 | 
						|
      S.var,
 | 
						|
      S.colon,
 | 
						|
      P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))),
 | 
						|
      S.close
 | 
						|
    ),
 | 
						|
    function(values)
 | 
						|
      return setmetatable({
 | 
						|
        type = Node.Type.VARIABLE,
 | 
						|
        name = values[3],
 | 
						|
        children = values[5],
 | 
						|
      }, Node)
 | 
						|
    end
 | 
						|
  )
 | 
						|
)
 | 
						|
 | 
						|
S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values)
 | 
						|
  return setmetatable({
 | 
						|
    type = Node.Type.SNIPPET,
 | 
						|
    children = values,
 | 
						|
  }, Node)
 | 
						|
end)
 | 
						|
 | 
						|
local M = {}
 | 
						|
 | 
						|
---The snippet node type enum
 | 
						|
---@types table<string, number>
 | 
						|
M.NodeType = Node.Type
 | 
						|
 | 
						|
---Parse snippet string and returns the AST
 | 
						|
---@param input string
 | 
						|
---@return table
 | 
						|
function M.parse(input)
 | 
						|
  local result = S.snippet(input, 1)
 | 
						|
  if not result.parsed then
 | 
						|
    error('snippet parsing failed.')
 | 
						|
  end
 | 
						|
  return result.value
 | 
						|
end
 | 
						|
 | 
						|
return M
 |