From 717223e2706d601884c331eeabb88ffb46bfda92 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Fri, 6 Jan 2023 09:00:09 -0500 Subject: [PATCH 01/26] Improves type annotation of `Section.properties` --- lua/orgmode/parser/section.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/orgmode/parser/section.lua b/lua/orgmode/parser/section.lua index c07b53d73..4872382e9 100644 --- a/lua/orgmode/parser/section.lua +++ b/lua/orgmode/parser/section.lua @@ -27,13 +27,16 @@ local config = require('orgmode.config') ---@field file string ---@field content string[] ---@field dates Date[] ----@field properties table +---@field properties SectionProperties ---@field tags string[] ---@field own_tags string[] ---@field logbook Logbook ---@field clocked_in boolean local Section = {} +---@class SectionProperties +---@field items table + ---@class SectionTodoKeyword ---@field node unknown ---@field type 'TODO'|'DONE'|'' From 4bd06b3fa209e8be0ea997e542ae3af2b1b2ff7d Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Fri, 6 Jan 2023 09:12:45 -0500 Subject: [PATCH 02/26] Improves Search annotations --- lua/orgmode/parser/search.lua | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 20162a8c1..4890b0b75 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -4,10 +4,19 @@ ---@class Search ---@field term string ---@field expressions table ----@field logic table +---@field logic SearchLogicClause[] ---@field todo_search table local Search = {} +---@class SearchLogicClause +---@field contains string[] +---@field excludes string[] + +---@class Searchable +---@field props table +---@field tags string[] +---@field todo string + ---@param term string function Search:new(term) local data = { @@ -22,6 +31,8 @@ function Search:new(term) return data end +---@param item Searchable +---@return boolean function Search:check(item) for _, or_item in ipairs(self.logic) do local passes = self:_check_or(or_item, item) @@ -32,6 +43,9 @@ function Search:check(item) return false end +---@param or_item SearchLogicClause +---@param item Searchable +---@return boolean function Search:_check_or(or_item, item) for _, val in ipairs(or_item.contains) do if not self:_matches(val, item) then @@ -52,6 +66,9 @@ function Search:_check_or(or_item, item) return true end +---@param val string +---@param item Searchable +---@return boolean function Search:_matches(val, item) local prop_name, operator, prop_val = val:match('([^=<>]*)([=<>]+)([^=<>]*)') if not prop_name then @@ -111,7 +128,7 @@ function Search:_matches(val, item) end ---@private ----@return string +---@return nil function Search:_parse() local term = self.term local todo_search = term:match('/([^/]*)$') From 70230cf55f5d75c8dd183bdc786edba5ff0b7f93 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 11 Jan 2023 18:52:12 -0500 Subject: [PATCH 03/26] Comments the existing Search:_matches behavior --- lua/orgmode/parser/search.lua | 86 ++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 4890b0b75..719813518 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -17,6 +17,28 @@ local Search = {} ---@field tags string[] ---@field todo string +---@type table +local OPERATORS = { + ['='] = function(a, b) + return a == b + end, + ['<='] = function(a, b) + return a <= b + end, + ['<'] = function(a, b) + return a < b + end, + ['>='] = function(a, b) + return a >= b + end, + ['>'] = function(a, b) + return a > b + end, + ['<>'] = function(a, b) + return a ~= b + end, +} + ---@param term string function Search:new(term) local data = { @@ -70,61 +92,61 @@ end ---@param item Searchable ---@return boolean function Search:_matches(val, item) - local prop_name, operator, prop_val = val:match('([^=<>]*)([=<>]+)([^=<>]*)') - if not prop_name then + local query_prop_name, operator, query_prop_val = val:match('([^=<>]*)([=<>]+)([^=<>]*)') + + -- If its a simple tag search, then just search the tags + if not query_prop_name then + -- If no tags are defined, it definitely doesn't match if not item.tags then return false end + + -- If multiple tags are on the item, check each of them against the query if type(item.tags) == 'table' then return vim.tbl_contains(item.tags, val) end + + -- If its just a single tag, check that one return val == item.tags end - prop_name = string.lower(vim.trim(prop_name)) - prop_val = vim.trim(prop_val) - if not item.props or not item.props[prop_name] then + + ---@type string|number + local prop_name = string.lower(vim.trim(query_prop_name)) + ---@type string|number + local prop_val = vim.trim(query_prop_val) + + -- If the item doesn't define the property in question, it definitely can't match + if not item.props or not item.props[query_prop_name] then return false end + + --- @type string|number local item_val = item.props[prop_name] - if tonumber(prop_val) then - prop_val = tonumber(prop_val) - item_val = tonumber(item_val) - if not item_val then + -- If the value is a number, parse it as such + local prop_val_number = tonumber(prop_val) + if prop_val_number then + prop_val = prop_val_number + local item_val_number = tonumber(item_val) + if not item_val_number then return false + else + item_val = item_val_number end end + -- If the value could not be parsed as another value, strip any leading and trailing quotation mark from it. if type(prop_val) == 'string' then prop_val = prop_val:gsub('^"', ''):gsub('"$', '') end - local operators = { - ['='] = function(a, b) - return a == b - end, - ['<='] = function(a, b) - return a <= b - end, - ['<'] = function(a, b) - return a < b - end, - ['>='] = function(a, b) - return a >= b - end, - ['>'] = function(a, b) - return a > b - end, - ['<>'] = function(a, b) - return a ~= b - end, - } - - if not operators[operator] then + -- If the operator is not defined, we can't match this item + if not OPERATORS[operator] then return false end - return operators[operator](item_val, prop_val) + -- Perform the comparison with the appropriate operator function + return OPERATORS[operator](item_val, prop_val) end ---@private From e1fd21403935ad4613e81ff804e318e1bdcec060 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 11 Jan 2023 19:07:24 -0500 Subject: [PATCH 04/26] Implements operators for dates --- lua/orgmode/objects/date.lua | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lua/orgmode/objects/date.lua b/lua/orgmode/objects/date.lua index 4807aec65..a3194e6b6 100644 --- a/lua/orgmode/objects/date.lua +++ b/lua/orgmode/objects/date.lua @@ -25,7 +25,28 @@ local time_format = '%H:%M' ---@field related_date_range Date ---@field dayname string ---@field adjustments string[] -local Date = {} +local Date = { + ---@type fun(this: Date, other: Date): boolean + __eq = function(this, other) + return this.timestamp == other.timestamp + end, + ---@type fun(this: Date, other: Date): boolean + __lt = function(this, other) + return this.timestamp < other.timestamp + end, + ---@type fun(this: Date, other: Date): boolean + __le = function(this, other) + return this.timestamp <= other.timestamp + end, + ---@type fun(this: Date, other: Date): boolean + __gt = function(this, other) + return this.timestamp > other.timestamp + end, + ---@type fun(this: Date, other: Date): boolean + __ge = function(this, other) + return this.timestamp >= other.timestamp + end, +} ---@param source table ---@param target? table From aa0fb25865077c9a7c483b3c1c42c9bd9878e679 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 11 Jan 2023 21:06:30 -0500 Subject: [PATCH 05/26] Totally reworks the Search query parsing logic --- lua/orgmode/parser/search.lua | 496 ++++++++++++++++++++++++++++------ 1 file changed, 415 insertions(+), 81 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 719813518..37f54dba0 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -1,23 +1,65 @@ ---TODO: ---Support regex search and date search +--TODO: Support regex search + +local Date = require('orgmode.objects.date') ---@class Search ---@field term string ---@field expressions table ----@field logic SearchLogicClause[] +---@field logic OrItem[] ---@field todo_search table local Search = {} ----@class SearchLogicClause ----@field contains string[] ----@field excludes string[] - ---@class Searchable ---@field props table ---@field tags string[] ---@field todo string ----@type table +---@class OrItem +---@field and_items AndItem[] +local OrItem = {} +OrItem.__index = OrItem + +---@class AndItem +---@field contains Matchable[] +---@field excludes Matchable[] +local AndItem = {} +AndItem.__index = AndItem + +---@alias Matchable TagMatch|PropertyMatch + +---@class TagMatch +---@field value string +local TagMatch = {} +TagMatch.__index = TagMatch + +---@alias PropertyMatch PropertyDateMatch|PropertyStringMatch|PropertyNumberMatch +local PropertyMatch = {} +PropertyMatch.__index = PropertyMatch + +---@alias PropertyMatchOperator '='|'<>'|'<'|'<='|'>'|'>=' + +---@class PropertyDateMatch +---@field name string +---@field operator PropertyMatchOperator +---@field value Date +local PropertyDateMatch = {} +PropertyDateMatch.__index = PropertyDateMatch + +---@class PropertyStringMatch +---@field name string +---@field operator PropertyMatchOperator +---@field value string +local PropertyStringMatch = {} +PropertyStringMatch.__index = PropertyStringMatch + +---@class PropertyNumberMatch +---@field name string +---@field operator PropertyMatchOperator +---@field value number +local PropertyNumberMatch = {} +PropertyNumberMatch.__index = PropertyNumberMatch + +---@type table local OPERATORS = { ['='] = function(a, b) return a == b @@ -39,8 +81,68 @@ 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 and input:len() do + item, input = item_parser(input) + if not item then + return nil, original_input + end + + table.insert(sequence, item) + end + + return sequence, input +end + ---@param term string +---@return Search function Search:new(term) + ---@type Search local data = { term = term, expressions = {}, @@ -50,6 +152,7 @@ function Search:new(term) setmetatable(data, self) self.__index = self data:_parse() + return data end @@ -57,123 +160,354 @@ end ---@return boolean function Search:check(item) for _, or_item in ipairs(self.logic) do - local passes = self:_check_or(or_item, item) - if passes then + if or_item:match(item) then return true end end return false end ----@param or_item SearchLogicClause +---@private +function Search:_parse() + -- Parse the sequence of ORs + self.logic = parse_delimited_sequence(self.term, function(i) + return OrItem:parse(i) + end, '%|') + + -- If the sequence failed to parse, reset the array + if not self.logic then + self.logic = {} + end +end + +---@private +---@return OrItem +function OrItem:_new() + ---@type OrItem + local or_item = { + and_items = {}, + } + + setmetatable(or_item, OrItem) + return or_item +end + +---@param input string +---@return OrItem?, string +function OrItem:parse(input) + ---@type AndItem[]? + local and_items + local original_input = input + + and_items, input = parse_delimited_sequence(input, function() + return AndItem:parse(input) + end, '%&') + + if not and_items then + return nil, original_input + end + + local or_item = OrItem:_new() + or_item.and_items = and_items + + return or_item, input +end + +---Verifies that each AndItem contained within the OrItem matches ---@param item Searchable ---@return boolean -function Search:_check_or(or_item, item) - for _, val in ipairs(or_item.contains) do - if not self:_matches(val, item) then +function OrItem:match(item) + for _, and_item in ipairs(self.and_items) do + if not and_item:match(item) then return false end end - for _, val in ipairs(or_item.excludes) do - if self:_matches(val, item) then - return false - end + return true +end + +---@private +---@return AndItem +function AndItem:_new() + ---@type AndItem + local and_item = { + contains = {}, + excludes = {}, + } + + setmetatable(and_item, AndItem) + return and_item +end + +---@param input string +---@return AndItem?, string +function AndItem:parse(input) + ---@type AndItem + local and_item = AndItem:_new() + ---@type string? + local operator + local original_input = input + + operator, input = parse_pattern(input, '[%+%-]?') + + -- A '+' operator is implied if none is present + if operator == '' then + operator = '+' end - if self.todo_search then - return self.todo_search:check({ tags = item.todo }) + while operator do + ---@type Matchable? + local matchable + + -- Try to parse as a PropertyMatch first + matchable, input = PropertyMatch:parse(input) + + -- If it wasn't a property match, then try a tag match + if not matchable then + matchable, input = TagMatch:parse(input) + if not matchable then + return nil, original_input + end + end + + if operator == '+' then + table.insert(and_item.contains, matchable) + elseif operator == '-' then + table.insert(and_item.excludes, matchable) + else + -- This should never happen if I wrote the operator pattern correctly + end + + -- Attempt to parse the next operator + operator, input = parse_pattern(input, '[%+%-]') end - return true + return and_item, input end ----@param val string ---@param item Searchable ---@return boolean -function Search:_matches(val, item) - local query_prop_name, operator, query_prop_val = val:match('([^=<>]*)([=<>]+)([^=<>]*)') - - -- If its a simple tag search, then just search the tags - if not query_prop_name then - -- If no tags are defined, it definitely doesn't match - if not item.tags then +function AndItem:match(item) + for _, c in ipairs(self.contains) do + if not c:match(item) then return false end + end - -- If multiple tags are on the item, check each of them against the query - if type(item.tags) == 'table' then - return vim.tbl_contains(item.tags, val) + for _, e in ipairs(self.excludes) do + if e:match(item) then + return false end + end - -- If its just a single tag, check that one - return val == item.tags + return true +end + +---@private +---@param tag string +---@return TagMatch +function TagMatch:_new(tag) + ---@type TagMatch + local tag_match = { value = tag } + setmetatable(tag_match, TagMatch) + + return tag_match +end + +---@param input string +---@return TagMatch?, string +function TagMatch:parse(input) + local tag + tag, input = parse_pattern(input, '%w+') + if not tag then + return nil, input end - ---@type string|number - local prop_name = string.lower(vim.trim(query_prop_name)) - ---@type string|number - local prop_val = vim.trim(query_prop_val) + return TagMatch:_new(tag), input +end - -- If the item doesn't define the property in question, it definitely can't match - if not item.props or not item.props[query_prop_name] then - return false +---@param item Searchable +---@return boolean +function TagMatch:match(item) + for _, tag in ipairs(item.tags) do + if tag == self.value then + return true + end end - --- @type string|number - local item_val = item.props[prop_name] + return false +end - -- If the value is a number, parse it as such - local prop_val_number = tonumber(prop_val) - if prop_val_number then - prop_val = prop_val_number - local item_val_number = tonumber(item_val) - if not item_val_number then - return false +---@param input string +---@return PropertyMatch?, string +function PropertyMatch:parse(input) + ---@type string?, PropertyMatchOperator? + local name, operator, string, number_str, date_str + local original_input = input + + name, input = parse_pattern(input, '[^=<>]+') + if not name then + return nil, original_input + end + + operator, input = self:_parse_operator(input) + if not operator then + return nil, original_input + end + + -- Number property + number_str, input = 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, '"<[^>]+>"') + if date_str then + local unquoted_date_str = date_str:gsub('^"<', ''):gsub('>"$', '') + ---@type Date? + local date_value = Date.from_string(unquoted_date_str) + if date_value then + return PropertyDateMatch:new(name, operator, date_value), input else - item_val = item_val_number + -- It could be a string query so reset the parse input + input = date_str .. input end end - -- If the value could not be parsed as another value, strip any leading and trailing quotation mark from it. - if type(prop_val) == 'string' then - prop_val = prop_val:gsub('^"', ''):gsub('"$', '') + -- String property + string, input = parse_pattern(input, '"[^"]+"') + if string then + return PropertyStringMatch:new(name, operator, string), input end - -- If the operator is not defined, we can't match this item - if not OPERATORS[operator] then + return nil, original_input +end + +---@private +---Parses one of the comparison operators (=, <>, <, <=, >, >=) +---@param input string +---@return PropertyMatchOperator, string +function PropertyMatch:_parse_operator(input) + return parse_pattern_choice(input, '%=', '%<%>', '%<', '%<%=', '%>', '%>%=') --[[@as PropertyMatchOperator]] +end + +---Constructs a PropertyNumberMatch +---@param name string +---@param operator PropertyMatchOperator +---@param value number +---@return PropertyNumberMatch +function PropertyNumberMatch:new(name, operator, value) + ---@type PropertyNumberMatch + local number_match = { + name = name, + operator = operator, + value = value, + } + + setmetatable(number_match, PropertyNumberMatch) + + return number_match +end + +---@param item Searchable +---@return boolean +function PropertyNumberMatch:match(item) + local item_str_value = item.props[self.name] + + -- If the property in question is not a number, it's not a match + local item_num_value = tonumber(item_str_value) + if not item_num_value then return false end - -- Perform the comparison with the appropriate operator function - return OPERATORS[operator](item_val, prop_val) + return OPERATORS[self.operator](item_num_value, self.value) end ----@private ----@return nil -function Search:_parse() - local term = self.term - local todo_search = term:match('/([^/]*)$') - if todo_search then - self.todo_search = Search:new(todo_search) - term = term:gsub('/([^/]*)$', '') - end - for or_item in vim.gsplit(term, '|', true) do - local a = { - contains = {}, - excludes = {}, - } - for and_item in vim.gsplit(or_item, '&', true) do - for op, exp in and_item:gmatch('([%+%-]*)([^%-%+]+)') do - if op == '' or op:match('^%+*$') then - table.insert(a.contains, exp) - else - table.insert(a.excludes, exp) - end - end +---@param name string +---@param operator PropertyMatchOperator +---@param value Date +---@return PropertyDateMatch +function PropertyDateMatch:new(name, operator, value) + ---@type PropertyDateMatch + local date_match = { + name = name, + operator = operator, + value = value, + } + + setmetatable(date_match, PropertyDateMatch) + return date_match +end + +---@param item Searchable +---@return boolean +function PropertyDateMatch:match(item) + local should_print = item.props.foo + + if should_print then + print('PropertyDateMatch:match(' .. vim.inspect(item) .. ')') + end + + local item_value = item.props[self.name] + + -- If the property is missing, then it's not a match + if not item_value then + if should_print then + print('No foo value contained') end - table.insert(self.logic, a) + return false end + + -- Extract the content between the braces/brackets + local date_content = item_value:match('^[<%[]([^>%]]+)[>%]]$') + if not date_content then + if should_print then + print('Failed to extract date content: "' .. item_value .. '"') + end + return false + end + + ---@type Date? + local item_date = Date.from_string(date_content) + if not item_date then + if should_print then + print('Date did not parse: ' .. date_content) + end + return false + end + + local result = OPERATORS[self.operator](item_date, self.value) + if should_print then + print('The result is ' .. vim.inspect(result)) + end + + return result +end + +---@param name string +---@param operator PropertyMatchOperator +---@param value string +---@return PropertyStringMatch +function PropertyStringMatch:new(name, operator, value) + ---@type PropertyStringMatch + local string_match = { + name = name, + operator = operator, + value = value, + } + + setmetatable(string_match, PropertyStringMatch) + + return string_match +end + +---@param item Searchable +---@return boolean +function PropertyStringMatch:match(item) + local item_value = item.props[self.name] or '' + return OPERATORS[self.operator](item_value, self.value) end return Search From c26f86174d7a7a786ca142946d03b76af8c41536 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Sun, 15 Jan 2023 11:42:51 -0500 Subject: [PATCH 06/26] Fixes an error in the parse_delimited_sequence function --- lua/orgmode/parser/search.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 37f54dba0..60ef93627 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -127,13 +127,15 @@ local function parse_delimited_sequence(input, item_parser, delimiter_pattern) -- Continue parsing items while there's a trailing delimiter delimiter, input = parse_pattern(input, delimiter_pattern) - while delimiter and input:len() do + 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 From 9c4ba33b2034d604f470065e90726bcc1890d471 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Sun, 15 Jan 2023 11:57:15 -0500 Subject: [PATCH 07/26] Fixes error in parsing a sequence of `AndItem`s --- lua/orgmode/parser/search.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 60ef93627..4d0e7a9e9 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -201,8 +201,8 @@ function OrItem:parse(input) local and_items local original_input = input - and_items, input = parse_delimited_sequence(input, function() - return AndItem:parse(input) + and_items, input = parse_delimited_sequence(input, function(i) + return AndItem:parse(i) end, '%&') if not and_items then From 105d9c4c2a1740a59375b8b8638d50e448572f17 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Sun, 15 Jan 2023 12:00:48 -0500 Subject: [PATCH 08/26] Supports defines tags as a single string or array of strings --- lua/orgmode/parser/search.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 4d0e7a9e9..90db570bb 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -11,7 +11,7 @@ local Search = {} ---@class Searchable ---@field props table ----@field tags string[] +---@field tags string|string[] ---@field todo string ---@class OrItem @@ -331,7 +331,12 @@ end ---@param item Searchable ---@return boolean function TagMatch:match(item) - for _, tag in ipairs(item.tags) do + local item_tags = item.tags + if type(item_tags) == 'string' then + return item_tags == self.value + end + + for _, tag in ipairs(item_tags) do if tag == self.value then return true end From a71ea1c795ca728920389d345c38400efa87dc24 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Sun, 15 Jan 2023 21:12:05 -0500 Subject: [PATCH 09/26] Corrected a failing test to reflect the internal object changes --- tests/plenary/parser/search_spec.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/plenary/parser/search_spec.lua b/tests/plenary/parser/search_spec.lua index 4d5ff1bd8..dd8937754 100644 --- a/tests/plenary/parser/search_spec.lua +++ b/tests/plenary/parser/search_spec.lua @@ -54,10 +54,11 @@ describe('Search parser', function() assert.Is.False(result:check({ tags = 'OTHER' })) result = Search:new('TAGS|TWO+THREE-FOUR&FIVE') - assert.are.same({ - { contains = { 'TAGS' }, excludes = {} }, - { contains = { 'TWO', 'THREE', 'FIVE' }, excludes = { 'FOUR' } }, - }, result.logic) + assert.are.equal(result.or_items[1].and_items[1].contains[1].value, 'TAGS') + assert.are.equal(result.or_items[2].and_items[1].contains[1].value, 'TWO') + assert.are.equal(result.or_items[2].and_items[1].contains[2].value, 'THREE') + assert.are.equal(result.or_items[2].and_items[1].excludes[1].value, 'FOUR') + assert.are.equal(result.or_items[2].and_items[2].contains[1].value, 'FIVE') assert.Is.True(result:check({ tags = { 'TAGS', 'THREE' } })) assert.Is.True(result:check({ tags = { 'TWO', 'THREE', 'FIVE' } })) From e2a2616ca465d6760134f07831d983fa9f697c75 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Sun, 15 Jan 2023 21:12:53 -0500 Subject: [PATCH 10/26] Adds quoting to string query properties in testing as is required by Emacs --- tests/plenary/parser/search_spec.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/plenary/parser/search_spec.lua b/tests/plenary/parser/search_spec.lua index dd8937754..cd1c063c5 100644 --- a/tests/plenary/parser/search_spec.lua +++ b/tests/plenary/parser/search_spec.lua @@ -67,7 +67,7 @@ describe('Search parser', function() end) it('should parse search term and match string properties and value', function() - local result = Search:new('CATEGORY="test"&MYPROP=myval+WORK') + local result = Search:new('CATEGORY="test"&MYPROP="myval"+WORK') assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, @@ -128,7 +128,7 @@ describe('Search parser', function() end) it('should search props, tags and todo keywords', function() - local result = Search:new('CATEGORY="test"&MYPROP=myval+WORK/TODO|NEXT') + local result = Search:new('CATEGORY="test"&MYPROP="myval"+WORK/TODO|NEXT') assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, From 9515f10291d59b7a1ef54ea9111f5c925d52b9ad Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Sun, 15 Jan 2023 21:42:10 -0500 Subject: [PATCH 11/26] Fixes a bug in the todo matching logic --- lua/orgmode/parser/search.lua | 176 ++++++++++++++++++++++++++-------- 1 file changed, 136 insertions(+), 40 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 90db570bb..9b983d2d1 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -5,8 +5,8 @@ local Date = require('orgmode.objects.date') ---@class Search ---@field term string ---@field expressions table ----@field logic OrItem[] ----@field todo_search table +---@field or_items OrItem[] +---@field todo_search? TodoMatch local Search = {} ---@class Searchable @@ -59,10 +59,17 @@ PropertyStringMatch.__index = PropertyStringMatch local PropertyNumberMatch = {} PropertyNumberMatch.__index = PropertyNumberMatch +---@class TodoMatch +---@field anyOf string[] +---@field noneOf string[] +local TodoMatch = {} +TodoMatch.__index = TodoMatch + ---@type table local OPERATORS = { ['='] = function(a, b) - return a == b + local result = a == b + return result end, ['<='] = function(a, b) return a <= b @@ -148,7 +155,7 @@ function Search:new(term) local data = { term = term, expressions = {}, - logic = {}, + or_items = {}, todo_search = nil, } setmetatable(data, self) @@ -161,25 +168,38 @@ end ---@param item Searchable ---@return boolean function Search:check(item) - for _, or_item in ipairs(self.logic) do + local ors_match = false + for _, or_item in ipairs(self.or_items) do if or_item:match(item) then - return true + ors_match = true + break end end - return false + + local todos_match + if self.todo_search then + todos_match = self.todo_search:match(item) + else + todos_match = true + end + + print(vim.inspect({ todos_match = todos_match, ors_match = ors_match })) + return ors_match and todos_match end ---@private function Search:_parse() + local input = self.term -- Parse the sequence of ORs - self.logic = parse_delimited_sequence(self.term, function(i) + self.or_items, input = parse_delimited_sequence(input, function(i) return OrItem:parse(i) end, '%|') -- If the sequence failed to parse, reset the array - if not self.logic then - self.logic = {} - end + self.or_items = self.or_items or {} + + -- Parse the TODO word filters if present + self.todo_search, input = TodoMatch:parse(input) end ---@private @@ -349,13 +369,14 @@ end ---@return PropertyMatch?, string function PropertyMatch:parse(input) ---@type string?, PropertyMatchOperator? - local name, operator, string, number_str, date_str + local name, operator, string_str, number_str, date_str local original_input = input name, input = parse_pattern(input, '[^=<>]+') if not name then return nil, original_input end + name = name:lower() operator, input = self:_parse_operator(input) if not operator then @@ -370,11 +391,10 @@ function PropertyMatch:parse(input) end -- Date property - date_str, input = parse_pattern(input, '"<[^>]+>"') + date_str, input = parse_pattern(input, '"(<[^>]+>)"') if date_str then - local unquoted_date_str = date_str:gsub('^"<', ''):gsub('>"$', '') ---@type Date? - local date_value = Date.from_string(unquoted_date_str) + local date_value = Date.from_string(date_str) if date_value then return PropertyDateMatch:new(name, operator, date_value), input else @@ -384,9 +404,11 @@ function PropertyMatch:parse(input) end -- String property - string, input = parse_pattern(input, '"[^"]+"') - if string then - return PropertyStringMatch:new(name, operator, string), input + string_str, input = parse_pattern(input, '"[^"]+"') + if string_str then + ---@type string + local unquote_string = string_str:match('^"([^"]+)"$') + return PropertyStringMatch:new(name, operator, unquote_string), input end return nil, original_input @@ -397,7 +419,7 @@ end ---@param input string ---@return PropertyMatchOperator, string function PropertyMatch:_parse_operator(input) - return parse_pattern_choice(input, '%=', '%<%>', '%<', '%<%=', '%>', '%>%=') --[[@as PropertyMatchOperator]] + return parse_pattern_choice(input, '%=', '%<%>', '%<%=', '%<', '%>%=', '%>') --[[@as PropertyMatchOperator]] end ---Constructs a PropertyNumberMatch @@ -451,46 +473,26 @@ end ---@param item Searchable ---@return boolean function PropertyDateMatch:match(item) - local should_print = item.props.foo - - if should_print then - print('PropertyDateMatch:match(' .. vim.inspect(item) .. ')') - end - local item_value = item.props[self.name] -- If the property is missing, then it's not a match if not item_value then - if should_print then - print('No foo value contained') - end return false end -- Extract the content between the braces/brackets local date_content = item_value:match('^[<%[]([^>%]]+)[>%]]$') if not date_content then - if should_print then - print('Failed to extract date content: "' .. item_value .. '"') - end return false end ---@type Date? local item_date = Date.from_string(date_content) if not item_date then - if should_print then - print('Date did not parse: ' .. date_content) - end return false end - local result = OPERATORS[self.operator](item_date, self.value) - if should_print then - print('The result is ' .. vim.inspect(result)) - end - - return result + return OPERATORS[self.operator](item_date, self.value) end ---@param name string @@ -517,4 +519,98 @@ function PropertyStringMatch:match(item) return OPERATORS[self.operator](item_value, self.value) end +---@private +---@return TodoMatch +function TodoMatch:_new() + ---@type TodoMatch + local todo_match = { + anyOf = {}, + noneOf = {}, + } + + setmetatable(todo_match, TodoMatch) + + return todo_match +end + +---@param input string +---@return TodoMatch?, string +function TodoMatch:parse(input) + local original_input = input + + -- Parse the '/' or '/!' prefix that indicates a TodoMatch + ---@type string? + local prefix + prefix, input = 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+') + end, '%|') + if anyOf and #anyOf > 0 then + -- Successfully parsed the whitelist, return it + local todo_match = TodoMatch:_new() + todo_match.anyOf = anyOf + return todo_match, input + end + + -- Parse a blacklist of keywords + ---@type string? + local negation + negation, input = parse_pattern(input, '-') + if negation then + local negative_items + negative_items, input = parse_delimited_sequence(input, function(i) + return parse_pattern(i, '%w+') + end, '%-') + + if negative_items then + if #negation > 0 then + local todo_match = TodoMatch:_new() + todo_match.noneOf = negative_items + return todo_match, input + else + return nil, original_input + end + end + end + + return nil, original_input +end + +---@param item Searchable +---@return boolean +function TodoMatch:match(item) + print(('TodoMatch:match(%s)'):format(vim.inspect(item))) + local item_todo = item.todo + + if #self.anyOf > 0 then + print('Checking for anyof') + for _, todo_value in ipairs(self.anyOf) do + if item_todo == todo_value then + return true + end + end + + return false + elseif #self.noneOf > 0 then + print('Checking for noneOf') + for _, todo_value in ipairs(self.noneOf) do + print(('%s can not be %s'):format(item_todo, todo_value)) + if item_todo == todo_value then + return false + end + end + + return true + else + return true + end +end + return Search From 96c4320234086894f0f3c249f5e75de415442abc Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Sun, 15 Jan 2023 21:42:26 -0500 Subject: [PATCH 12/26] Fixes type issues in the `search_spec` test --- tests/plenary/parser/search_spec.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/plenary/parser/search_spec.lua b/tests/plenary/parser/search_spec.lua index cd1c063c5..305854d31 100644 --- a/tests/plenary/parser/search_spec.lua +++ b/tests/plenary/parser/search_spec.lua @@ -132,7 +132,7 @@ describe('Search parser', function() assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, - todo = { 'TODO' }, + todo = 'TODO', })) assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, @@ -142,38 +142,38 @@ describe('Search parser', function() assert.Is.False(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, - todo = { 'DONE' }, + todo = 'DONE', })) result = Search:new('CATEGORY="test"+WORK/-WAITING') assert.Is.True(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = { 'TODO' }, + todo = 'TODO', })) assert.Is.True(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = { 'DONE' }, + todo = 'DONE', })) assert.Is.False(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = { 'WAITING' }, + todo = 'WAITING', })) assert.Is.False(result:check({ props = { category = 'test_bad' }, tags = { 'WORK' }, - todo = { 'DONE' }, + todo = 'DONE', })) assert.Is.False(result:check({ props = { category = 'test' }, tags = { 'OFFICE' }, - todo = { 'DONE' }, + todo = 'DONE', })) end) end) From 4b12d20813222fdce22d35e2532bf57acb241848 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 18 Jan 2023 17:48:12 -0500 Subject: [PATCH 13/26] Removes print statements --- lua/orgmode/parser/search.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 9b983d2d1..0597f4b2f 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -586,11 +586,9 @@ end ---@param item Searchable ---@return boolean function TodoMatch:match(item) - print(('TodoMatch:match(%s)'):format(vim.inspect(item))) local item_todo = item.todo if #self.anyOf > 0 then - print('Checking for anyof') for _, todo_value in ipairs(self.anyOf) do if item_todo == todo_value then return true @@ -599,9 +597,7 @@ function TodoMatch:match(item) return false elseif #self.noneOf > 0 then - print('Checking for noneOf') for _, todo_value in ipairs(self.noneOf) do - print(('%s can not be %s'):format(item_todo, todo_value)) if item_todo == todo_value then return false end From 58dc769d61a7bfc08eefde2086edb6666f55a733 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 18 Jan 2023 18:13:49 -0500 Subject: [PATCH 14/26] Adds a `tomorrow` function --- lua/orgmode/objects/date.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/orgmode/objects/date.lua b/lua/orgmode/objects/date.lua index a3194e6b6..2435a27ea 100644 --- a/lua/orgmode/objects/date.lua +++ b/lua/orgmode/objects/date.lua @@ -187,6 +187,12 @@ local function today(data) return Date:new(opts) end +---@return Date +local function tomorrow() + local today_date = today() + return today_date:adjust('+1d') +end + ---@param data? table ---@return Date local function now(data) @@ -930,6 +936,7 @@ return { from_string = from_string, now = now, today = today, + tomorrow = tomorrow, parse_all_from_line = parse_all_from_line, is_valid_date = is_valid_date, is_date_instance = is_date_instance, From c37212fed0fb996fd34f43aa5988fb9eea0c0391 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 18 Jan 2023 18:13:59 -0500 Subject: [PATCH 15/26] Supports relative dates (e.g. <+1d> or ) --- lua/orgmode/parser/search.lua | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index 0597f4b2f..bb8d9e411 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -393,8 +393,26 @@ function PropertyMatch:parse(input) -- Date property date_str, input = parse_pattern(input, '"(<[^>]+>)"') if date_str then + ---@type string?, Date? + local date_content, date_value + if date_str == '' then + date_value = Date.today() + elseif date_str == '' then + date_value = Date.tomorrow() + else + -- Parse relative formats (e.g. <+1d>) as well as absolute + date_content = date_str:match('^<([%+%-]%d+[dm])>$') + if date_content then + date_value = Date.from_string(date_str) + else + date_content = date_str:match('^<([^>]+)>$') + if date_content then + date_value = Date.from_string(date_str) + end + end + end + ---@type Date? - local date_value = Date.from_string(date_str) if date_value then return PropertyDateMatch:new(name, operator, date_value), input else From 1530e437879ea086cbbd18ac7b3ebd51c474a2d9 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Tue, 24 Jan 2023 07:55:45 -0500 Subject: [PATCH 16/26] Includes SCHEDULED, CLOSED, and DEADLINE in props --- lua/orgmode/parser/section.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/orgmode/parser/section.lua b/lua/orgmode/parser/section.lua index 4872382e9..83b6ae245 100644 --- a/lua/orgmode/parser/section.lua +++ b/lua/orgmode/parser/section.lua @@ -137,6 +137,7 @@ function Section.from_node(section_node, file, parent) type = first_node_text end local timestamp = file:get_node_text(entry:named_child(1)) + data.properties.items[first_node_text:lower()] = timestamp utils.concat( data.dates, Date.from_org_date(timestamp, { From b775ad43dca409d710857531ffef28096215853f Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Tue, 24 Jan 2023 08:20:38 -0500 Subject: [PATCH 17/26] Fixes a bug in the relative date search --- lua/orgmode/parser/search.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index bb8d9e411..ad1bbd88c 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -401,9 +401,10 @@ function PropertyMatch:parse(input) date_value = Date.tomorrow() else -- Parse relative formats (e.g. <+1d>) as well as absolute - date_content = date_str:match('^<([%+%-]%d+[dm])>$') + date_content = date_str:match('^<([%+%-]%d+[dmyhwM])>$') if date_content then - date_value = Date.from_string(date_str) + date_value = Date.now() + date_value = date_value:adjust(date_content) else date_content = date_str:match('^<([^>]+)>$') if date_content then From 94da6be79e5cf37590d193826b69dd673f7b53c9 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Tue, 24 Jan 2023 08:20:49 -0500 Subject: [PATCH 18/26] Removes debug print --- lua/orgmode/parser/search.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/orgmode/parser/search.lua b/lua/orgmode/parser/search.lua index ad1bbd88c..a3265801e 100644 --- a/lua/orgmode/parser/search.lua +++ b/lua/orgmode/parser/search.lua @@ -183,7 +183,6 @@ function Search:check(item) todos_match = true end - print(vim.inspect({ todos_match = todos_match, ors_match = ors_match })) return ors_match and todos_match end From 9e12ee9ce8f5cfb4d446c32c99a0585a106cbbcd Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Tue, 24 Jan 2023 09:08:49 -0500 Subject: [PATCH 19/26] Fixes unit tests --- tests/plenary/parser/parser_spec.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/plenary/parser/parser_spec.lua b/tests/plenary/parser/parser_spec.lua index 09097fe64..b38956c77 100644 --- a/tests/plenary/parser/parser_spec.lua +++ b/tests/plenary/parser/parser_spec.lua @@ -245,6 +245,11 @@ describe('Parser', function() end_col = 6, }), }, + properties_items = { + deadline = '<2021-05-20 Thu>', + scheduled = '<2021-05-18>', + closed = '[2021-05-21 Fri]', + }, tags = { 'WORK' }, own_tags = { 'WORK' }, category = 'work', @@ -378,6 +383,7 @@ describe('Parser', function() own_tags = { 'WORK' }, category = 'work', properties_items = { + deadline = '<2021-05-10 11:00>', some_prop = 'some value', }, properties_range = Range:new({ @@ -417,7 +423,7 @@ describe('Parser', function() } local parsed = File.from_content(lines, 'work') local section = parsed:get_section(1) - assert.are.same({ items = {} }, section.properties) + assert.are.same({ items = { deadline = '<2021-05-10 11:00>' } }, section.properties) end) it('should parse properties only if its positioned after headline or planning date', function() @@ -433,7 +439,7 @@ describe('Parser', function() local parsed = File.from_content(lines, 'work') local headline = parsed:get_section(1) - assert.are.same({}, headline.properties.items) + assert.are.same({ deadline = '<2021-05-10 11:00>' }, headline.properties.items) lines = { '* TODO Test orgmode :WORK:', @@ -471,7 +477,7 @@ describe('Parser', function() parsed = File.from_content(lines, 'work') headline = parsed:get_section(1) - assert.are.same({ some_prop = 'some value' }, headline.properties.items) + assert.are.same({ deadline = '<2021-05-10 11:00>', some_prop = 'some value' }, headline.properties.items) end) it('should override headline category from property', function() @@ -635,6 +641,7 @@ describe('Parser', function() own_tags = { 'WORK' }, category = 'work', properties_items = { + deadline = '<2021-05-10 11:00>', some_prop = 'some value', }, properties_range = Range:new({ From 96c8515aa92f42daa98090cb3e08728fb19922fd Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 25 Jan 2023 07:58:23 -0500 Subject: [PATCH 20/26] Injects DEADLINE, SCHEDULED, and CLOSED as Searchable props --- lua/orgmode/parser/file.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lua/orgmode/parser/file.lua b/lua/orgmode/parser/file.lua index 41a81d24b..77a55018c 100644 --- a/lua/orgmode/parser/file.lua +++ b/lua/orgmode/parser/file.lua @@ -205,9 +205,17 @@ function File:apply_search(search, todo_only) if item:is_archived() or (todo_only and not item:is_todo()) then return false end + + local deadline = item:get_deadline_date() + local scheduled = item:get_scheduled_date() + local closed = item:get_closed_date() + return search:check({ props = vim.tbl_extend('keep', {}, item.properties.items, { category = item.category, + deadline = deadline and deadline:to_wrapped_string(true), + scheduled = scheduled and scheduled:to_wrapped_string(true), + closed = closed and closed:to_wrapped_string(false), }), tags = item.tags, todo = item.todo_keyword.value, From c6c0e215cbb88f1cf2ad9c071957a28d171c7df9 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 25 Jan 2023 08:00:08 -0500 Subject: [PATCH 21/26] Revert "Fixes unit tests" This reverts commit 373575031803c3a2737b4d5c73e32f7398116b44. --- tests/plenary/parser/parser_spec.lua | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/plenary/parser/parser_spec.lua b/tests/plenary/parser/parser_spec.lua index b38956c77..09097fe64 100644 --- a/tests/plenary/parser/parser_spec.lua +++ b/tests/plenary/parser/parser_spec.lua @@ -245,11 +245,6 @@ describe('Parser', function() end_col = 6, }), }, - properties_items = { - deadline = '<2021-05-20 Thu>', - scheduled = '<2021-05-18>', - closed = '[2021-05-21 Fri]', - }, tags = { 'WORK' }, own_tags = { 'WORK' }, category = 'work', @@ -383,7 +378,6 @@ describe('Parser', function() own_tags = { 'WORK' }, category = 'work', properties_items = { - deadline = '<2021-05-10 11:00>', some_prop = 'some value', }, properties_range = Range:new({ @@ -423,7 +417,7 @@ describe('Parser', function() } local parsed = File.from_content(lines, 'work') local section = parsed:get_section(1) - assert.are.same({ items = { deadline = '<2021-05-10 11:00>' } }, section.properties) + assert.are.same({ items = {} }, section.properties) end) it('should parse properties only if its positioned after headline or planning date', function() @@ -439,7 +433,7 @@ describe('Parser', function() local parsed = File.from_content(lines, 'work') local headline = parsed:get_section(1) - assert.are.same({ deadline = '<2021-05-10 11:00>' }, headline.properties.items) + assert.are.same({}, headline.properties.items) lines = { '* TODO Test orgmode :WORK:', @@ -477,7 +471,7 @@ describe('Parser', function() parsed = File.from_content(lines, 'work') headline = parsed:get_section(1) - assert.are.same({ deadline = '<2021-05-10 11:00>', some_prop = 'some value' }, headline.properties.items) + assert.are.same({ some_prop = 'some value' }, headline.properties.items) end) it('should override headline category from property', function() @@ -641,7 +635,6 @@ describe('Parser', function() own_tags = { 'WORK' }, category = 'work', properties_items = { - deadline = '<2021-05-10 11:00>', some_prop = 'some value', }, properties_range = Range:new({ From 72ada798a2824a6f5dbfb4ab5e2260ab555d43cb Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 25 Jan 2023 08:00:38 -0500 Subject: [PATCH 22/26] No longer includes scheduling dates as props --- lua/orgmode/parser/section.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/orgmode/parser/section.lua b/lua/orgmode/parser/section.lua index 83b6ae245..4872382e9 100644 --- a/lua/orgmode/parser/section.lua +++ b/lua/orgmode/parser/section.lua @@ -137,7 +137,6 @@ function Section.from_node(section_node, file, parent) type = first_node_text end local timestamp = file:get_node_text(entry:named_child(1)) - data.properties.items[first_node_text:lower()] = timestamp utils.concat( data.dates, Date.from_org_date(timestamp, { From afb5e36c1a2b00df57c31b705e0bfc42f6b19b4f Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 15 Mar 2023 08:30:51 -0400 Subject: [PATCH 23/26] Reverts changes to tests --- tests/plenary/parser/search_spec.lua | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/plenary/parser/search_spec.lua b/tests/plenary/parser/search_spec.lua index 305854d31..4d5ff1bd8 100644 --- a/tests/plenary/parser/search_spec.lua +++ b/tests/plenary/parser/search_spec.lua @@ -54,11 +54,10 @@ describe('Search parser', function() assert.Is.False(result:check({ tags = 'OTHER' })) result = Search:new('TAGS|TWO+THREE-FOUR&FIVE') - assert.are.equal(result.or_items[1].and_items[1].contains[1].value, 'TAGS') - assert.are.equal(result.or_items[2].and_items[1].contains[1].value, 'TWO') - assert.are.equal(result.or_items[2].and_items[1].contains[2].value, 'THREE') - assert.are.equal(result.or_items[2].and_items[1].excludes[1].value, 'FOUR') - assert.are.equal(result.or_items[2].and_items[2].contains[1].value, 'FIVE') + assert.are.same({ + { contains = { 'TAGS' }, excludes = {} }, + { contains = { 'TWO', 'THREE', 'FIVE' }, excludes = { 'FOUR' } }, + }, result.logic) assert.Is.True(result:check({ tags = { 'TAGS', 'THREE' } })) assert.Is.True(result:check({ tags = { 'TWO', 'THREE', 'FIVE' } })) @@ -67,7 +66,7 @@ describe('Search parser', function() end) it('should parse search term and match string properties and value', function() - local result = Search:new('CATEGORY="test"&MYPROP="myval"+WORK') + local result = Search:new('CATEGORY="test"&MYPROP=myval+WORK') assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, @@ -128,11 +127,11 @@ describe('Search parser', function() end) it('should search props, tags and todo keywords', function() - local result = Search:new('CATEGORY="test"&MYPROP="myval"+WORK/TODO|NEXT') + local result = Search:new('CATEGORY="test"&MYPROP=myval+WORK/TODO|NEXT') assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, - todo = 'TODO', + todo = { 'TODO' }, })) assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, @@ -142,38 +141,38 @@ describe('Search parser', function() assert.Is.False(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, - todo = 'DONE', + todo = { 'DONE' }, })) result = Search:new('CATEGORY="test"+WORK/-WAITING') assert.Is.True(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = 'TODO', + todo = { 'TODO' }, })) assert.Is.True(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = 'DONE', + todo = { 'DONE' }, })) assert.Is.False(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = 'WAITING', + todo = { 'WAITING' }, })) assert.Is.False(result:check({ props = { category = 'test_bad' }, tags = { 'WORK' }, - todo = 'DONE', + todo = { 'DONE' }, })) assert.Is.False(result:check({ props = { category = 'test' }, tags = { 'OFFICE' }, - todo = 'DONE', + todo = { 'DONE' }, })) end) end) From 3b24bd06810de6eb59ad34a3ddb0cbbde088c051 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 15 Mar 2023 08:48:56 -0400 Subject: [PATCH 24/26] Fixes a test that broke do to changing the "AST" of Search --- tests/plenary/parser/search_spec.lua | 33 +++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/plenary/parser/search_spec.lua b/tests/plenary/parser/search_spec.lua index 4d5ff1bd8..1e934420b 100644 --- a/tests/plenary/parser/search_spec.lua +++ b/tests/plenary/parser/search_spec.lua @@ -55,9 +55,36 @@ describe('Search parser', function() result = Search:new('TAGS|TWO+THREE-FOUR&FIVE') assert.are.same({ - { contains = { 'TAGS' }, excludes = {} }, - { contains = { 'TWO', 'THREE', 'FIVE' }, excludes = { 'FOUR' } }, - }, result.logic) + { + and_items = { + { + contains = { + { value = 'TAGS' }, + }, + excludes = {}, + }, + }, + }, + { + and_items = { + { + contains = { + { value = 'TWO' }, + { value = 'THREE' }, + }, + excludes = { + { value = 'FOUR' }, + }, + }, + { + contains = { + { value = 'FIVE' }, + }, + excludes = {}, + }, + }, + }, + }, result.or_items) assert.Is.True(result:check({ tags = { 'TAGS', 'THREE' } })) assert.Is.True(result:check({ tags = { 'TWO', 'THREE', 'FIVE' } })) From 96c35e7e2c7be78c3fd0b588effa79a70e176348 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 15 Mar 2023 08:49:53 -0400 Subject: [PATCH 25/26] Updates a test to quote string props as is required by Emacs --- tests/plenary/parser/search_spec.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/plenary/parser/search_spec.lua b/tests/plenary/parser/search_spec.lua index 1e934420b..0da38d9f0 100644 --- a/tests/plenary/parser/search_spec.lua +++ b/tests/plenary/parser/search_spec.lua @@ -93,7 +93,7 @@ describe('Search parser', function() end) it('should parse search term and match string properties and value', function() - local result = Search:new('CATEGORY="test"&MYPROP=myval+WORK') + local result = Search:new('CATEGORY="test"&MYPROP="myval"+WORK') assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, @@ -154,7 +154,7 @@ describe('Search parser', function() end) it('should search props, tags and todo keywords', function() - local result = Search:new('CATEGORY="test"&MYPROP=myval+WORK/TODO|NEXT') + local result = Search:new('CATEGORY="test"&MYPROP="myval"+WORK/TODO|NEXT') assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, From 335fdde67920b201811636c6db454e19fe24e1a7 Mon Sep 17 00:00:00 2001 From: Chuck Flowers Date: Wed, 15 Mar 2023 08:51:39 -0400 Subject: [PATCH 26/26] Corrects a type issue of the todo keyword prop --- tests/plenary/parser/search_spec.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/plenary/parser/search_spec.lua b/tests/plenary/parser/search_spec.lua index 0da38d9f0..f123aecfc 100644 --- a/tests/plenary/parser/search_spec.lua +++ b/tests/plenary/parser/search_spec.lua @@ -158,7 +158,7 @@ describe('Search parser', function() assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, - todo = { 'TODO' }, + todo = 'TODO', })) assert.Is.True(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, @@ -168,38 +168,38 @@ describe('Search parser', function() assert.Is.False(result:check({ props = { category = 'test', myprop = 'myval', age = 10 }, tags = { 'WORK', 'OFFICE' }, - todo = { 'DONE' }, + todo = 'DONE', })) result = Search:new('CATEGORY="test"+WORK/-WAITING') assert.Is.True(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = { 'TODO' }, + todo = 'TODO', })) assert.Is.True(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = { 'DONE' }, + todo = 'DONE', })) assert.Is.False(result:check({ props = { category = 'test' }, tags = { 'WORK' }, - todo = { 'WAITING' }, + todo = 'WAITING', })) assert.Is.False(result:check({ props = { category = 'test_bad' }, tags = { 'WORK' }, - todo = { 'DONE' }, + todo = 'DONE', })) assert.Is.False(result:check({ props = { category = 'test' }, tags = { 'OFFICE' }, - todo = { 'DONE' }, + todo = 'DONE', })) end) end)