Skip to content

Commit

Permalink
feat: added lpeg parser for parsing :Trouble args into lua tables
Browse files Browse the repository at this point in the history
  • Loading branch information
folke committed Oct 23, 2023
1 parent 48a7367 commit 72a4a90
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 25 deletions.
27 changes: 2 additions & 25 deletions lua/trouble/command.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local Config = require("trouble.config")
local Parser = require("trouble.config.parser")
local Util = require("trouble.util")

local M = {}
Expand All @@ -10,31 +11,7 @@ function M.parse(input)
Util.error("Invalid arguments: " .. input)
return
end
local parts = vim.split(args, "%s+")
---@type trouble.Config
local opts = {}

for _, part in ipairs(parts) do
local key, value = part:match("([^=]+)=(.*)")
if not key then
key = part
value = true
elseif value == "true" then
value = true
elseif value == "false" then
value = false
elseif tonumber(value) then
value = tonumber(value)
end
-- remove quotes
if type(value) == "string" then
---@diagnostic disable-next-line: no-unknown
value = value:gsub("^['\"]", ""):gsub("['\"]$", "")
end
---@diagnostic disable-next-line: no-unknown
opts[key] = value
end
return source, opts
return source, Parser.parse(args)
end

---@param line string
Expand Down
70 changes: 70 additions & 0 deletions lua/trouble/config/parser.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---@diagnostic disable: no-unknown
local lpeg = vim.lpeg
local P, S, R = lpeg.P, lpeg.S, lpeg.R
local C, Ct, Cs = lpeg.C, lpeg.Ct, lpeg.Cs

local M = {}

---@type vim.lpeg.Pattern|Capture
local pattern
do
-- Basic Definitions
local ws = S(" \t") ^ 0 -- Optional whitespace
local eq = P("=") -- Equals sign
local key_component = R("az", "AZ", "09", "__") ^ 1 -- Single component of a key
local dot = P(".") -- Dot separator
local full_key = C(key_component * (dot * key_component) ^ 0) -- Full dot-separated key
---@type vim.lpeg.Pattern|Capture
local value = (P('"') * (P("\\") * P(1) + (1 - P('"'))) ^ 0 * P('"')) -- Values that are quoted strings
+ (1 - S(" \t\n\r")) ^ 1 -- Unquoted values
local pair = full_key * ws * eq * ws * Cs(value) -- Capture the full key-value pair

-- Main pattern
pattern = Ct(ws * pair * (ws * pair) ^ 0 * ws)
end

---@return trouble.Config
function M.parse(input)
---@type string[]
local t = pattern:match(input)
if not t then
error("Invalid input: " .. input)
end

-- Convert list to a table of key-value pairs
local parts = {} ---@type string[]
for i = 1, #t, 2 do
local k = t[i]
local v = t[i + 1]
parts[#parts + 1] = string.format("{%q,%s}", k, v)
end

local chunk = loadstring("return {" .. table.concat(parts, ", ") .. "}")
if not chunk then
error("Failed to parse input: " .. input)
end
local ret = {}
---@diagnostic disable-next-line: no-unknown
for _, pair in pairs(chunk()) do
M.set(ret, pair[1], pair[2])
end
return ret
end

---@param t table
---@param dotted_key string
---@param value any
function M.set(t, dotted_key, value)
local keys = vim.split(dotted_key, ".", { plain = true })
for i = 1, #keys - 1 do
local key = keys[i]
t[key] = t[key] or {}
if type(t[key]) ~= "table" then
t[key] = {}
end
t = t[key]
end
t[keys[#keys]] = value
end

return M
50 changes: 50 additions & 0 deletions tests/parser_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
local Parser = require("trouble.config.parser")
describe("Input is parsed correctly", function()
local tests = {
{
input = [[a = "b" foo = true bar=1 c = "g"]],
expected = { a = "b", foo = true, bar = 1, c = "g" },
},
{
input = [[a.b = "c"]],
expected = { a = { b = "c" } },
},
{
input = [[x.y.z = "value" a.b = 2]],
expected = { x = { y = { z = "value" } }, a = { b = 2 } },
},
{
input = [[test="hello world"]],
expected = { test = "hello world" },
},
{
input = [[one.two.three.four = 4]],
expected = { one = { two = { three = { four = 4 } } } },
},
{
input = [[a="b" c="d" e.f="g" h.i.j="k"]],
expected = { a = "b", c = "d", e = { f = "g" }, h = { i = { j = "k" } } },
},
{
input = [[empty = "" nonempty="not empty"]],
expected = { empty = "", nonempty = "not empty" },
},
{
input = [[a.b="c" a = "b"]],
expected = { a = "b" }, -- This test is tricky as it will overwrite the first value of 'a'
},
{
input = [[a="b" a.b="c"]],
expected = { a = { b = "c" } }, -- This test is tricky as it will overwrite the first value of 'a'
},
}

for _, test in ipairs(tests) do
it("parses " .. test.input, function()
local actual = Parser.parse(test.input)
assert.same(test.expected, actual)
end)
end

return tests
end)

0 comments on commit 72a4a90

Please sign in to comment.