Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for :LOGGING: property #584

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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