Skip to content
1 change: 1 addition & 0 deletions lua/orgmode/config/defaults.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---@class DefaultConfig
---@field org_log_done 'time' | 'note' | false
local DefaultConfig = {
org_agenda_files = '',
org_default_notes_file = '',
Expand Down
93 changes: 17 additions & 76 deletions lua/orgmode/parser/search.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
--TODO: Support regex search

local Date = require('orgmode.objects.date')
local parsing = require('orgmode.parser.utils')

---@class Search
---@field term string
Expand Down Expand Up @@ -88,66 +89,6 @@ local OPERATORS = {
end,
}

---Parses a pattern from the beginning of an input using Lua's pattern syntax
---@param input string
---@param pattern string
---@return string?, string
local function parse_pattern(input, pattern)
local value = input:match('^' .. pattern)
if value then
return value, input:sub(#value + 1)
else
return nil, input
end
end

---Parses the first of a sequence of patterns
---@param input string The input to parse
---@param ... string The patterns to accept
---@return string?, string
local function parse_pattern_choice(input, ...)
for _, pattern in ipairs({ ... }) do
local value, remaining = parse_pattern(input, pattern)
if value then
return value, remaining
end
end

return nil, input
end

---@generic T
---@param input string
---@param item_parser fun(input: string): (T?, string)
---@param delimiter_pattern string
---@return (T[])?, string
local function parse_delimited_sequence(input, item_parser, delimiter_pattern)
local sequence, item, delimiter = {}, nil, nil
local original_input = input

-- Parse the first item
item, input = item_parser(input)
if not item then
return sequence, input
end
table.insert(sequence, item)

-- Continue parsing items while there's a trailing delimiter
delimiter, input = parse_pattern(input, delimiter_pattern)
while delimiter do
item, input = item_parser(input)
if not item then
return nil, original_input
end

table.insert(sequence, item)

delimiter, input = parse_pattern(input, delimiter_pattern)
end

return sequence, input
end

---@param term string
---@return Search
function Search:new(term)
Expand Down Expand Up @@ -190,7 +131,7 @@ end
function Search:_parse()
local input = self.term
-- Parse the sequence of ORs
self.or_items, input = parse_delimited_sequence(input, function(i)
self.or_items, input = parsing.parse_delimited_sequence(input, function(i)
return OrItem:parse(i)
end, '%|')

Expand Down Expand Up @@ -220,7 +161,7 @@ function OrItem:parse(input)
local and_items
local original_input = input

and_items, input = parse_delimited_sequence(input, function(i)
and_items, input = parsing.parse_delimited_sequence(input, function(i)
return AndItem:parse(i)
end, '%&')

Expand Down Expand Up @@ -269,7 +210,7 @@ function AndItem:parse(input)
local operator
local original_input = input

operator, input = parse_pattern(input, '[%+%-]?')
operator, input = parsing.parse_pattern(input, '[%+%-]?')

-- A '+' operator is implied if none is present
if operator == '' then
Expand Down Expand Up @@ -300,7 +241,7 @@ function AndItem:parse(input)
end

-- Attempt to parse the next operator
operator, input = parse_pattern(input, '[%+%-]')
operator, input = parsing.parse_pattern(input, '[%+%-]')
end

return and_item, input
Expand Down Expand Up @@ -339,7 +280,7 @@ end
---@return TagMatch?, string
function TagMatch:parse(input)
local tag
tag, input = parse_pattern(input, '[%w_@#%%]+')
tag, input = parsing.parse_pattern(input, '[%w_@#%%]+')
if not tag then
return nil, input
end
Expand Down Expand Up @@ -371,7 +312,7 @@ function PropertyMatch:parse(input)
local name, operator, string_str, number_str, date_str
local original_input = input

name, input = parse_pattern(input, '[^=<>]+')
name, input = parsing.parse_pattern(input, '[^=<>]+')
if not name then
return nil, original_input
end
Expand All @@ -383,14 +324,14 @@ function PropertyMatch:parse(input)
end

-- Number property
number_str, input = parse_pattern(input, '%d+')
number_str, input = parsing.parse_pattern(input, '%d+')
if number_str then
local number = tonumber(number_str) --[[@as number]]
return PropertyNumberMatch:new(name, operator, number), input
end

-- Date property
date_str, input = parse_pattern(input, '"(<[^>]+>)"')
date_str, input = parsing.parse_pattern(input, '"(<[^>]+>)"')
if date_str then
---@type string?, Date?
local date_content, date_value
Expand Down Expand Up @@ -422,7 +363,7 @@ function PropertyMatch:parse(input)
end

-- String property
string_str, input = parse_pattern(input, '"[^"]+"')
string_str, input = parsing.parse_pattern(input, '"[^"]+"')
if string_str then
---@type string
local unquote_string = string_str:match('^"([^"]+)"$')
Expand All @@ -437,7 +378,7 @@ end
---@param input string
---@return PropertyMatchOperator, string
function PropertyMatch:_parse_operator(input)
return parse_pattern_choice(input, '%=', '%<%>', '%<%=', '%<', '%>%=', '%>') --[[@as PropertyMatchOperator]]
return parsing.parse_pattern_choice(input, '%=', '%<%>', '%<%=', '%<', '%>%=', '%>') --[[@as PropertyMatchOperator]]
end

---Constructs a PropertyNumberMatch
Expand Down Expand Up @@ -559,16 +500,16 @@ function TodoMatch:parse(input)
-- Parse the '/' or '/!' prefix that indicates a TodoMatch
---@type string?
local prefix
prefix, input = parse_pattern(input, '%/[%!]?')
prefix, input = parsing.parse_pattern(input, '%/[%!]?')
if not prefix then
return nil, original_input
end

-- Parse a whitelist of keywords
--- @type string[]?
local anyOf
anyOf, input = parse_delimited_sequence(input, function(i)
return parse_pattern(i, '%w+')
anyOf, input = parsing.parse_delimited_sequence(input, function(i)
return parsing.parse_pattern(i, '%w+')
end, '%|')
if anyOf and #anyOf > 0 then
-- Successfully parsed the whitelist, return it
Expand All @@ -580,11 +521,11 @@ function TodoMatch:parse(input)
-- Parse a blacklist of keywords
---@type string?
local negation
negation, input = parse_pattern(input, '-')
negation, input = parsing.parse_pattern(input, '-')
if negation then
local negative_items
negative_items, input = parse_delimited_sequence(input, function(i)
return parse_pattern(i, '%w+')
negative_items, input = parsing.parse_delimited_sequence(input, function(i)
return parsing.parse_pattern(i, '%w+')
end, '%-')

if negative_items then
Expand Down
187 changes: 187 additions & 0 deletions lua/orgmode/parser/todo-config.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
local parsing = require('orgmode.parser.utils')

--- @class TodoConfig
--- @field words TodoConfigWord[]
local TodoConfig = {}
TodoConfig.__index = TodoConfig

--- @alias TodoConfigRecordBehavior 'time' | 'note' | false

--- @class TodoConfigWord
--- @field name string
--- @field is_active boolean
--- @field hotkey string
--- @field on_enter TodoConfigRecordBehavior
--- @field on_leave TodoConfigRecordBehavior
local TodoConfigWord = {}

--- @param words TodoConfigWord[]
--- @return TodoConfig
function TodoConfig:_new(words)
--- @type TodoConfig
local instance = {}
setmetatable(instance, TodoConfig)

instance.words = words

return instance
end

--- @param input string
--- @return TodoConfig?, string
function TodoConfig:parse(input)
local original = input

--- @type TodoConfigWord[]
local active
active, input = parsing.parse_delimited_sequence(input, function(inner_input)
return TodoConfigWord:parse(inner_input, true)
end, '%s+')

if #active == 0 then
return nil, original
end

local pipe
pipe, input = parsing.parse_pattern(input, '%s*%|%s*')
if pipe == nil then
return nil, original
end

--- @type TodoConfigWord[]
local inactive
inactive, input = parsing.parse_delimited_sequence(input, function(inner_input)
return TodoConfigWord:parse(inner_input, false)
end, '%s+')

if #inactive == 0 then
return nil, original
end

--- @type TodoConfigWord[]
local words = {}
for _, x in ipairs(active) do
table.insert(words, x)
end
for _, x in ipairs(inactive) do
table.insert(words, x)
end

return TodoConfig:_new(words), input
end

--- @param from string
--- @param to string
--- @return TodoConfigRecordBehavior
function TodoConfig:get_logging_behavior(from, to)
--- Find the from config
local from_config = self:_find_word(from)
local to_config = self:_find_word(to)

-- Ensure the described transition is valid
if from_config == nil or to_config == nil then
return false
end

return to_config.on_enter or from_config.on_leave
end

--- Finds the word config with the associated name
--- @private
--- @param name string
--- @return TodoConfigWord?
function TodoConfig:_find_word(name)
for _, x in ipairs(self.words) do
if x.name == name then
return x
end
end

return nil
end

--- @param name string
--- @param hotkey string
--- @param is_active boolean
--- @param on_enter TodoConfigRecordBehavior
--- @param on_leave TodoConfigRecordBehavior
--- @return TodoConfigWord
function TodoConfigWord:_new(name, is_active, hotkey, on_enter, on_leave)
--- @type TodoConfigWord
local instance = {}
setmetatable(instance, TodoConfigWord)

instance.name = name
instance.is_active = is_active
instance.hotkey = hotkey
instance.on_enter = on_enter
instance.on_leave = on_leave

return instance
end

--- @param input string
--- @param is_active boolean
--- @return TodoConfigWord?, string
function TodoConfigWord:parse(input, is_active)
local original = input

--- @type string?, string?, string?, string?
local name, open, hotkey, enter, slash, leave, close

name, input = parsing.parse_pattern(input, '%w+')
if name == nil then
return nil, original
end

open, input = parsing.parse_pattern(input, '%(')
if open == nil then
return nil, original
end

hotkey, input = parsing.parse_pattern(input, '%w')
if hotkey == nil then
return nil, original
end

---@type TodoConfigRecordBehavior
local on_enter = false
enter, input = parsing.parse_pattern_choice(input, '%@', '%!')
if enter ~= nil then
if enter == '!' then
on_enter = 'time'
elseif enter == '@' then
on_enter = 'note'
else
return nil, original
end
end

--- @type TodoConfigRecordBehavior
local on_leave = false
slash, input = parsing.parse_pattern(input, '%/')
if slash ~= nil then
leave, input = parsing.parse_pattern_choice(input, '%@', '%!')
if leave == nil then
return nil, original
end

if leave == '!' then
on_leave = 'time'
elseif leave == '@' then
on_leave = 'note'
else
return nil, original
end
end

close, input = parsing.parse_pattern(input, '%)')
if close == nil then
return nil, original
end

local word = TodoConfigWord:_new(name, is_active, hotkey, on_enter, on_leave)
return word, input
end

return TodoConfig
Loading