diff --git a/src/Context.cpp b/src/Context.cpp index 10409288e..1347b7f0d 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -1126,6 +1126,10 @@ void Context::staticInitialization() { for (auto& var : config.all()) if (var.substr(0, 13) == "urgency.user." || var.substr(0, 12) == "urgency.uda.") Task::coefficients[var] = config.getReal(var); + + // Pre-parse the coefficient keys, so urgency_c() doesn't have to re-parse for + // each task. + Task::setUrgencyCoefficients(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Filter.cpp b/src/Filter.cpp index 2e6d20a00..b739749a8 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -44,31 +44,7 @@ void Filter::subset(const std::vector& input, std::vector& output) { Context::getContext().cli2.prepareFilter(); - std::vector> precompiled; - for (auto& a : Context::getContext().cli2._args) - if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - - if (precompiled.size()) { - Eval eval; - eval.addSource(domSource); - - // Debug output from Eval during compilation is useful. During evaluation - // it is mostly noise. - eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false); - eval.compileExpression(precompiled); - - for (auto& task : input) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } - - eval.debug(false); - } else - output = input; + filter_to_tasks(input, output); _endCount = (int)output.size(); Context::getContext().debug( @@ -86,51 +62,28 @@ void Filter::subset(std::vector& output) { for (auto& a : Context::getContext().cli2._args) if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - // Shortcut indicates that only pending.data needs to be loaded. + // Shortcut indicates that only tasks in the working set are loaded. bool shortcut = false; if (precompiled.size()) { Timer timer_pending; - auto pending = Context::getContext().tdb2.pending_tasks(); + const auto& pending = Context::getContext().tdb2.pending_tasks(); Context::getContext().time_filter_us -= timer_pending.total_us(); _startCount = (int)pending.size(); - Eval eval; - eval.addSource(domSource); - - // Debug output from Eval during compilation is useful. During evaluation - // it is mostly noise. - eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false); - eval.compileExpression(precompiled); - output.clear(); - for (auto& task : pending) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } + filter_to_tasks(pending, output); shortcut = pendingOnly(); if (!shortcut) { Timer timer_completed; - auto completed = Context::getContext().tdb2.completed_tasks(); + const auto& completed = Context::getContext().tdb2.completed_tasks(); Context::getContext().time_filter_us -= timer_completed.total_us(); _startCount += (int)completed.size(); - for (auto& task : completed) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } + filter_to_tasks(completed, output); } - - eval.debug(false); } else { safety(); @@ -145,7 +98,7 @@ void Filter::subset(std::vector& output) { Context::getContext().time_filter_us += timer.total_us(); } -//////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////// bool Filter::hasFilter() const { for (const auto& a : Context::getContext().cli2._args) if (a.hasTag("FILTER")) return true; @@ -153,65 +106,102 @@ bool Filter::hasFilter() const { return false; } +///////////////////////////////////////////////////////////////////////////////// +// Evaluates a pre-parsed filter against a set of tasks and stores their indices +// from the vector. The filter is parsed with prepareFilter(), but this +// function does not call that or safety() itself - callers are expected to do so. +void Filter::filter_to_indices(const std::vector& pending, std::vector& indices) const { + std::vector> precompiled; + for (auto& a : Context::getContext().cli2._args) + if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); + + if (precompiled.empty()) { + indices.reserve(pending.size()); + for (int i = 0; i < (int)pending.size(); ++i) indices.push_back(i); + } else { + Eval eval; + eval.addSource(domSource); + eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3); + eval.compileExpression(precompiled); + for (int i = 0; i < (int)pending.size(); ++i) { + auto currentTask = Context::getContext().withCurrentTask(&pending[i]); + Variant var; + eval.evaluateCompiledExpression(var); + if (var.get_bool()) indices.push_back(i); + } + eval.debug(false); + } + + Context::getContext().debug( + format("Filtered {1} tasks --> {2} tasks [pending only]", pending.size(), indices.size())); +} + +//////////////////////////////////////////////////////////////////////////////// +// Like filter_to_indices, but copies matched tasks into the output. +void Filter::filter_to_tasks(const std::vector& input, std::vector& output) const { + std::vector> precompiled; + for (auto& a : Context::getContext().cli2._args) + if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); + + if (precompiled.empty()) { + output = input; + } else { + Eval eval; + eval.addSource(domSource); + eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3); + eval.compileExpression(precompiled); + for (auto& task : input) { + auto currentTask = Context::getContext().withCurrentTask(&task); + Variant var; + eval.evaluateCompiledExpression(var); + if (var.get_bool()) output.push_back(task); + } + eval.debug(false); + } +} + //////////////////////////////////////////////////////////////////////////////// // If the filter contains no 'or', 'xor' or 'not' operators, and only includes // status values 'pending', 'waiting' or 'recurring', then the filter is // guaranteed to only need data from pending.data. + bool Filter::pendingOnly() const { - // When GC is off, there are no shortcuts. if (!Context::getContext().config.getBoolean("gc")) return false; - // To skip loading completed.data, there should be: - // - 'status' in filter - // - no 'completed' - // - no 'deleted' - // - no 'xor' - // - no 'or' - int countStatus = 0; - int countPending = 0; - int countWaiting = 0; - int countRecurring = 0; - int countId = (int)Context::getContext().cli2._id_ranges.size(); - int countUUID = (int)Context::getContext().cli2._uuid_list.size(); - int countOr = 0; - int countXor = 0; - int countNot = 0; - bool pendingTag = false; - bool activeTag = false; - - for (const auto& a : Context::getContext().cli2._args) { - if (a.hasTag("FILTER")) { - std::string raw = a.attribute("raw"); - std::string canonical = a.attribute("canonical"); - - if (a._lextype == Lexer::Type::op && raw == "or") ++countOr; - if (a._lextype == Lexer::Type::op && raw == "xor") ++countXor; - if (a._lextype == Lexer::Type::op && raw == "not") ++countNot; - if (a._lextype == Lexer::Type::dom && canonical == "status") ++countStatus; - if (raw == "pending") ++countPending; - if (raw == "waiting") ++countWaiting; - if (raw == "recurring") ++countRecurring; - } - } + const auto& cli = Context::getContext().cli2; + if (!cli._uuid_list.empty()) return false; - for (const auto& word : Context::getContext().cli2._original_args) { - if (word.attribute("raw") == "+PENDING") pendingTag = true; - if (word.attribute("raw") == "+ACTIVE") activeTag = true; - } - - if (countUUID) return false; + std::vector filter_args; + for (const auto& arg : cli._args) { + if (!arg.hasTag("FILTER")) continue; - if (countOr || countXor || countNot) return false; + const auto& raw = arg.attribute("raw"); + if (arg._lextype == Lexer::Type::op && + (raw == "or" || raw == "xor" || raw == "!" || raw == "not")) + return false; - if (pendingTag || activeTag) return true; - - if (countStatus) { - if (!countPending && !countWaiting && !countRecurring) return false; + filter_args.push_back(&arg); + } - return true; + for (size_t i = 0; i + 2 < filter_args.size(); ++i) { + const auto& left = *filter_args[i]; + const auto& op = *filter_args[i + 1]; + const auto& right = *filter_args[i + 2]; + const auto& value = right.attribute("raw"); + + if (left._lextype == Lexer::Type::dom && left.attribute("canonical") == "status" && + op._lextype == Lexer::Type::op && + (op.attribute("raw") == "=" || op.attribute("raw") == "==") && + (value == "pending" || value == "waiting" || value == "recurring")) + return true; + + if (left._lextype == Lexer::Type::dom && left.attribute("raw") == "tags" && + op._lextype == Lexer::Type::op && op.attribute("raw") == "_hastag_" && + (value == "PENDING" || value == "ACTIVE" || value == "READY" || value == "WAITING")) + return true; } - if (countId) return true; + if (!cli._id_ranges.empty()) return true; return false; } diff --git a/src/Filter.h b/src/Filter.h index bb7449318..dfd80a3e9 100644 --- a/src/Filter.h +++ b/src/Filter.h @@ -40,6 +40,13 @@ class Filter { void subset(std::vector&); bool hasFilter() const; bool pendingOnly() const; + + // These evaluate already-prepared filters (ie. caller calls + // cli2.prepareFilter() and safety()). filter_to_indices() stores matching + // indices, filter_to_tasks() copies task objects. + void filter_to_indices(const std::vector&, std::vector&) const; + void filter_to_tasks(const std::vector&, std::vector&) const; + void safety() const; void disableSafety(); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 1d1e440a4..b88b84e1b 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -279,6 +279,9 @@ void Hooks::onModify(Task& before, Task& after) const { if (!_enabled) return; Timer timer; + const auto id = after.id; + const auto is_blocked = after.is_blocked; + const auto is_blocking = after.is_blocking; std::vector matchingScripts = scripts("on-modify"); if (matchingScripts.size()) { @@ -314,6 +317,9 @@ void Hooks::onModify(Task& before, Task& after) const { } after = Task(input[1]); + after.id = id; + after.is_blocked = is_blocked; + after.is_blocking = is_blocking; } Context::getContext().time_hooks_us += timer.total_us(); @@ -322,6 +328,9 @@ void Hooks::onModify(Task& before, Task& after) const { //////////////////////////////////////////////////////////////////////////////// std::vector Hooks::list() const { return _scripts; } +//////////////////////////////////////////////////////////////////////////////// +bool Hooks::hasOnModify() const { return _enabled && !scripts("on-modify").empty(); } + //////////////////////////////////////////////////////////////////////////////// std::vector Hooks::scripts(const std::string& event) const { std::vector matching; diff --git a/src/Hooks.h b/src/Hooks.h index 448c59744..894599900 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -41,6 +41,7 @@ class Hooks { void onExit() const; void onAdd(Task&) const; void onModify(Task&, Task&) const; + bool hasOnModify() const; std::vector list() const; private: diff --git a/src/TDB2.cpp b/src/TDB2.cpp index d559e0c1d..5a3e06fe9 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,9 +43,16 @@ #include #include +#include "taskchampion-cpp/lib.h" + bool TDB2::debug_mode = false; // This functions main job is to set Task::is_blocked / Task::is_blocking flags. -static void dependency_scan(std::vector&, const std::unordered_map&); +static void dependency_scan(std::vector&, const std::unordered_map&, + std::unordered_map* = nullptr); +static void dependency_update(std::vector&, const std::unordered_map&, + std::unordered_map&, size_t, + const std::vector&, const std::vector&); +static bool participates_in_dependency_graph(const Task&); // Build maps for dependency queries. static DependencyGraph build_dependency_graph(const std::vector&, @@ -64,6 +72,8 @@ static tc::Status statusFromString(const std::string& s) { //////////////////////////////////////////////////////////////////////////////// void TDB2::open_replica(const std::string& location, bool create_if_missing, bool read_write) { _replica = tc::new_replica_on_disk(location, create_if_missing, read_write); + invalidate_cached_info(); + changes.clear(); } //////////////////////////////////////////////////////////////////////////////// @@ -143,10 +153,19 @@ void TDB2::modify(Task& task) { // invoke the hook and allow it to modify the task before updating Task original; - bool found_original = get(uuid, original); + bool found_original = false; + tc::Uuid tcuuid = tc::uuid_from_string(uuid); + if (!_pending_tasks && task.id > 0) { + auto maybe_original = replica()->get_task_data(tcuuid); + if (maybe_original.is_some()) { + original = Task{maybe_original.take(), task.id}; + found_original = true; + } + } else { + found_original = get(uuid, original); + } Context::getContext().hooks.onModify(original, task); - tc::Uuid tcuuid = tc::uuid_from_string(uuid); auto maybe_tctask = replica()->get_task(tcuuid); if (maybe_tctask.is_none()) { throw std::string("task no longer exists"); @@ -192,10 +211,11 @@ void TDB2::modify(Task& task) { replica()->commit_operations(std::move(ops)); - // If the task entered or left the pending set, we must invalidate the cache. - bool was_pending = found_original && (original.getStatus() == Task::pending); - bool now_pending = task.getStatus() == Task::pending; - if (was_pending != now_pending || !found_original) { + // If the task entered or left the working set/dependency graph, we must + // invalidate the cache. + bool was_active = found_original && participates_in_dependency_graph(original); + bool now_active = participates_in_dependency_graph(task); + if (was_active != now_active || !found_original) { invalidate_cached_info(); return; } @@ -211,7 +231,11 @@ void TDB2::modify(Task& task) { auto new_deps = task.getDependencyUUIDs(); if (old_deps != new_deps) { deps_changed = true; - dependency_scan(*_pending_tasks, pending_index()); + auto& index = pending_index(); + dependency_update(*_pending_tasks, index, *_pending_dependency_counts, index.at(uuid), + old_deps, new_deps); + task.is_blocked = pt->is_blocked; + task.is_blocking = pt->is_blocking; } } } @@ -273,15 +297,39 @@ void TDB2::gc() { Timer timer; // Allowed as an override, but not recommended. - if (Context::getContext().config.getBoolean("gc")) { + if (Context::getContext().config.getBoolean("gc") && !working_set_is_clean()) { replica()->rebuild_working_set(true); + invalidate_cached_info(); } Context::getContext().time_gc_us += timer.total_us(); } +bool TDB2::working_set_is_clean() { + const auto& ws = working_set(); + const auto& tasks = pending_tasks(); + const auto largest = ws->largest_index(); + + if (tasks.size() != largest) return false; + + for (size_t i = 1; i <= largest; ++i) { + if (ws->by_index(i).is_nil()) return false; + } + + return std::all_of(tasks.begin(), tasks.end(), [](const Task& task) { + const auto& status = task.get_ref("status"); + return status == "pending" || status == "recurring" || status == "iterative"; + }); +} + //////////////////////////////////////////////////////////////////////////////// -void TDB2::expire_tasks() { replica()->expire_tasks(); } +void TDB2::expire_tasks() { + replica()->expire_tasks(); + invalidate_cached_info(); +} + +//////////////////////////////////////////////////////////////////////////////// +void TDB2::invalidate_cache() { invalidate_cached_info(); } //////////////////////////////////////////////////////////////////////////////// // Latest ID is that of the last pending task. @@ -337,7 +385,8 @@ const std::vector& TDB2::pending_tasks() { for (size_t i = 0, n = result.size(); i < n; ++i) _pending_index->emplace(result[i].get_ref("uuid"), i); - dependency_scan(result, *_pending_index); + _pending_dependency_counts.emplace(); + dependency_scan(result, *_pending_index, &*_pending_dependency_counts); Context::getContext().time_load_us += timer.total_us(); _pending_tasks = std::move(result); @@ -387,6 +436,7 @@ const DependencyGraph& TDB2::dependency_graph() { ///////////////////////////////////////////////////////////////////////////////// // This builds and returns the UUID map if it is missing. const std::unordered_map& TDB2::pending_index() { + pending_tasks(); if (!_pending_index) { _pending_index.emplace(); _pending_index->reserve(_pending_tasks->size()); @@ -414,6 +464,7 @@ void TDB2::invalidate_cached_info() { _working_set = std::nullopt; _dependency_graph = std::nullopt; _pending_index = std::nullopt; + _pending_dependency_counts = std::nullopt; } //////////////////////////////////////////////////////////////////////////////// @@ -423,8 +474,6 @@ bool TDB2::get(int id, Task& task) { const auto tcuuid = ws->by_index(id); if (!tcuuid.is_nil()) { std::string uuid = static_cast(tcuuid.to_string()); - // Load index of pending tasks. - pending_tasks(); // Lookup the UUID in the index instead of scanning the vector. auto* pt = find_pending(uuid); if (pt) { @@ -439,8 +488,6 @@ bool TDB2::get(int id, Task& task) { //////////////////////////////////////////////////////////////////////////////// // Locate task by UUID, including by partial ID, wherever it is. bool TDB2::get(const std::string& uuid, Task& task) { - pending_tasks(); - // Try to match exact UUID within the index. auto* pt = find_pending(uuid); if (pt) { @@ -448,6 +495,18 @@ bool TDB2::get(const std::string& uuid, Task& task) { return true; } + if (uuid.length() == 36) { + Lexer lexer(uuid); + Lexer::Type type; + std::string token; + if (lexer.isUUID(token, type, true) && token.length() == uuid.length()) { + auto maybe_tctask = replica()->get_task_data(tc::uuid_from_string(uuid)); + if (maybe_tctask.is_none()) return false; + task = Task{maybe_tctask.take()}; + return true; + } + } + // try a partial match if (uuid.length() < 36) { for (const auto& pending_task : *_pending_tasks) { @@ -539,7 +598,9 @@ int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); } //////////////////////////////////////////////////////////////////////////////// // Set Task::is_blocked / Task::is_blocking flags using the pre-built UUID map static void dependency_scan(std::vector& tasks, - const std::unordered_map& uuid_index) { + const std::unordered_map& uuid_index, + std::unordered_map* dependency_counts) { + if (dependency_counts) dependency_counts->clear(); // Reset all flags first. This is for safety reasons - if we don't do this // dependency_scan() only sets them to true, so it can stay true (within the cache) // even after we have changed a task's dependencies after a modify when @@ -551,22 +612,69 @@ static void dependency_scan(std::vector& tasks, task.is_blocked = false; } for (size_t i = 0; i < tasks.size(); ++i) { - auto lstatus = tasks[i].getStatus(); + if (!participates_in_dependency_graph(tasks[i])) continue; for (const auto& dep : tasks[i].getDependencyUUIDs()) { auto it = uuid_index.find(dep); if (it == uuid_index.end()) continue; size_t j = it->second; - auto rstatus = tasks[j].getStatus(); - if (lstatus != Task::completed && lstatus != Task::deleted && rstatus != Task::completed && - rstatus != Task::deleted) { + if (participates_in_dependency_graph(tasks[j])) { tasks[i].is_blocked = true; tasks[j].is_blocking = true; + if (dependency_counts) ++(*dependency_counts)[dep]; } } } } +///////////////////////////////////////////////////////////////////////////////// +static void dependency_update(std::vector& tasks, + const std::unordered_map& uuid_index, + std::unordered_map& dependency_counts, + size_t task_index, const std::vector& old_deps, + const std::vector& new_deps) { + std::unordered_set old_set(old_deps.begin(), old_deps.end()); + std::unordered_set new_set(new_deps.begin(), new_deps.end()); + auto& task = tasks[task_index]; + + task.is_blocked = false; + bool task_is_active = participates_in_dependency_graph(task); + + for (const auto& dep : old_set) { + if (new_set.find(dep) != new_set.end()) continue; + auto target = uuid_index.find(dep); + if (target == uuid_index.end()) continue; + + auto count = dependency_counts.find(dep); + if (count != dependency_counts.end()) { + if (--count->second == 0) dependency_counts.erase(count); + } + tasks[target->second].is_blocking = dependency_counts.find(dep) != dependency_counts.end(); + } + + for (const auto& dep : new_set) { + auto target = uuid_index.find(dep); + if (target == uuid_index.end()) continue; + + bool target_is_active = participates_in_dependency_graph(tasks[target->second]); + if (!task_is_active || !target_is_active) continue; + + task.is_blocked = true; + if (old_set.find(dep) == old_set.end()) ++dependency_counts[dep]; + tasks[target->second].is_blocking = true; + } + + const auto& uuid = task.get_ref("uuid"); + task.is_blocking = dependency_counts.find(uuid) != dependency_counts.end(); +} + +////////////////////////////////////////////////////////////////////////////////// +// Defined here for reuse. +static bool participates_in_dependency_graph(const Task& task) { + auto status = task.getStatus(); + return status != Task::completed && status != Task::deleted; +} + ///////////////////////////////////////////////////////////////////////////////// // Build the full dependency map from the task vector. static DependencyGraph build_dependency_graph( diff --git a/src/TDB2.h b/src/TDB2.h index a3d671c59..8c8c55551 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -62,6 +62,7 @@ class TDB2 { void get_changes(std::vector&); void gc(); void expire_tasks(); + void invalidate_cache(); int latest_id(); // Generalized task accessors. @@ -111,6 +112,7 @@ class TDB2 { // Lazily cache UUIDs within the pending set. // Avoids scans of the vectors with get/modify.. std::optional> _pending_index; + std::optional> _pending_dependency_counts; void invalidate_cached_info(); // Return the full pending UUID map. @@ -120,6 +122,7 @@ class TDB2 { std::map changes; const rust::Box& working_set(); + bool working_set_is_clean(); void maybe_add_undo_point(rust::Vec&); }; diff --git a/src/Task.cpp b/src/Task.cpp index 6608db850..bd0a8f142 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -74,6 +74,8 @@ bool Task::regex = false; std::map Task::attributes; std::map Task::coefficients; +std::vector Task::userCoefficients; +bool Task::urgencyInherit = false; float Task::urgencyProjectCoefficient = 0.0; float Task::urgencyActiveCoefficient = 0.0; float Task::urgencyScheduledCoefficient = 0.0; @@ -139,7 +141,9 @@ Task::Task(const json::object* obj) { } //////////////////////////////////////////////////////////////////////////////// -Task::Task(rust::Box obj) { +Task::Task(rust::Box obj) : Task(std::move(obj), -1) {} + +Task::Task(rust::Box obj, int known_id) { id = 0; urgency_value = 0.0; recalc_urgency = true; @@ -147,7 +151,7 @@ Task::Task(rust::Box obj) { is_blocking = false; annotation_count = 0; - parseTC(std::move(obj)); + parseTC(std::move(obj), known_id); } //////////////////////////////////////////////////////////////////////////////// @@ -191,9 +195,9 @@ const std::string Task::identifier(bool shortened /* = false */) const { if (id != 0) return format(id); else if (shortened) - return get("uuid").substr(0, 8); + return get_ref("uuid").substr(0, 8); else - return get("uuid"); + return get_ref("uuid"); } //////////////////////////////////////////////////////////////////////////////// @@ -718,23 +722,23 @@ void Task::parseJSON(const json::object* root_obj) { //////////////////////////////////////////////////////////////////////////////// // Note that all fields undergo encode/decode. -void Task::parseTC(rust::Box task) { +void Task::parseTC(rust::Box task, int known_id) { auto items = task->items(); data.clear(); - for (auto& item : items) { - data[static_cast(item.prop)] = static_cast(item.value); - } // count annotations annotation_count = 0; - for (auto i : data) { - if (isAnnotationAttr(i.first)) { + for (auto& item : items) { + auto key = static_cast(item.prop); + data[key] = static_cast(item.value); + if (isAnnotationAttr(key)) { ++annotation_count; } } data["uuid"] = static_cast(task->get_uuid().to_string()); id = Context::getContext().tdb2.id(data["uuid"]); + id = known_id >= 0 ? known_id : Context::getContext().tdb2.id(data["uuid"]); } //////////////////////////////////////////////////////////////////////////////// @@ -1045,11 +1049,18 @@ bool Task::hasDependency(const std::string& uuid) const { //////////////////////////////////////////////////////////////////////////////// std::vector Task::getDependencyIDs() const { std::vector ids; - for (auto& attr : all()) { - if (!isDepAttr(attr)) continue; - auto dep = attr2Dep(attr); - ids.push_back(Context::getContext().tdb2.id(dep)); - } + + const auto& graph = Context::getContext().tdb2.dependency_graph(); + auto found = graph.dependencies.find(get_ref("uuid")); + if (found == graph.dependencies.end()) return ids; + + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + ids.reserve(found->second.size()); + for (auto idx : found->second) + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) + ids.push_back(tasks[idx].id); + + std::sort(ids.begin(), ids.end()); return ids; } @@ -1057,41 +1068,60 @@ std::vector Task::getDependencyIDs() const { //////////////////////////////////////////////////////////////////////////////// std::vector Task::getDependencyUUIDs() const { std::vector uuids; - for (auto& attr : all()) { - if (!isDepAttr(attr)) continue; - auto dep = attr2Dep(attr); - uuids.push_back(dep); + for (auto& pair : data) { + if (!isDepAttr(pair.first)) continue; + uuids.push_back(attr2Dep(pair.first)); } return uuids; } //////////////////////////////////////////////////////////////////////////////// +// Uses the cached dependency map instead of scanning the vector. std::vector Task::getDependencyTasks() const { - auto uuids = getDependencyUUIDs(); + const auto& uuid = get_ref("uuid"); - // NOTE: this may seem inefficient, but note that `TDB2::get` performs a - // linear search on each invocation, so scanning *once* is quite a bit more - // efficient. std::vector blocking; - if (uuids.size() > 0) - for (auto& it : Context::getContext().tdb2.pending_tasks()) - if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted && - std::find(uuids.begin(), uuids.end(), it.get("uuid")) != uuids.end()) - blocking.push_back(it); + + auto& graph = Context::getContext().tdb2.dependency_graph(); + auto found = graph.dependencies.find(uuid); + + if (found == graph.dependencies.end()) return blocking; + + blocking.reserve(found->second.size()); + + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) + blocking.push_back(tasks[idx]); + + std::sort(blocking.begin(), blocking.end(), + [](const Task& left, const Task& right) { return left.id < right.id; }); return blocking; } //////////////////////////////////////////////////////////////////////////////// +// Returns the pending tasks that depend on a given task. std::vector Task::getBlockedTasks() const { - auto uuid = get("uuid"); + const auto& uuid = get_ref("uuid"); std::vector blocked; - for (auto& it : Context::getContext().tdb2.pending_tasks()) - if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted && - it.hasDependency(uuid)) - blocked.push_back(it); + + auto& graph = Context::getContext().tdb2.dependency_graph(); + auto found = graph.dependents.find(uuid); + + if (found == graph.dependents.end()) return blocked; + + blocked.reserve(found->second.size()); + + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) + blocked.push_back(tasks[idx]); + + std::sort(blocked.begin(), blocked.end(), + [](const Task& left, const Task& right) { return left.id < right.id; }); return blocked; } @@ -1692,6 +1722,46 @@ int Task::determineVersion(const std::string& line) { return 0; } +///////////////////////////////////////////////////////////////////////////////// +// Pre-parse the user's UDA/urgency coefficient keys once, instead of reparsing +// for every task in the report. The keys are static. +void Task::setUrgencyCoefficients() { + urgencyInherit = Context::getContext().config.getBoolean("urgency.inherit"); + + userCoefficients.clear(); + userCoefficients.reserve(coefficients.size()); + + for (const auto& var : coefficients) { + const std::string& key = var.first; + float coeff = var.second; + if (fabs(coeff) <= epsilon) continue; + + auto end = key.find(".coefficient"); + if (end == std::string::npos) continue; + + if (!key.compare(0, 13, "urgency.user.", 13)) { + if (!key.compare(13, 8, "project.", 8)) + userCoefficients.push_back( + {UrgencyCoefficient::project, key.substr(21, end - 21), "", coeff}); + else if (!key.compare(13, 4, "tag.", 4)) + userCoefficients.push_back({UrgencyCoefficient::tag, key.substr(17, end - 17), "", coeff}); + else if (!key.compare(13, 8, "keyword.", 8)) + userCoefficients.push_back( + {UrgencyCoefficient::keyword, key.substr(21, end - 21), "", coeff}); + } else if (!key.compare(0, 12, "urgency.uda.", 12)) { + // covers both urgency.uda..coefficient and + // urgency.uda...coefficient + std::string uda = key.substr(12, end - 12); + auto dot = uda.find('.'); + if (dot == std::string::npos) + userCoefficients.push_back({UrgencyCoefficient::uda, std::move(uda), "", coeff}); + else + userCoefficients.push_back( + {UrgencyCoefficient::udaValue, uda.substr(0, dot), uda.substr(dot + 1), coeff}); + } + } +} + //////////////////////////////////////////////////////////////////////////////// // Urgency is defined as a polynomial, the value of which is calculated in this // function, according to: @@ -1740,58 +1810,45 @@ float Task::urgency_c() const { value += fabsf(Task::urgencyAgeCoefficient) > epsilon ? (urgency_age() * Task::urgencyAgeCoefficient) : 0.0; - - const std::string taskProjectName = get("project"); - // Tag- and project-specific coefficients. - for (auto& var : Task::coefficients) { - if (fabs(var.second) > epsilon) { - if (!var.first.compare(0, 13, "urgency.user.", 13)) { - // urgency.user.project..coefficient - auto end = std::string::npos; - if (var.first.substr(13, 8) == "project." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string project = var.first.substr(21, end - 21); - - if (taskProjectName == project || taskProjectName.find(project + '.') == 0) { - value += var.second; - } - } - - // urgency.user.tag..coefficient - if (var.first.substr(13, 4) == "tag." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string tag = var.first.substr(17, end - 17); - - if (hasTag(tag)) value += var.second; - } - - // urgency.user.keyword..coefficient - if (var.first.substr(13, 8) == "keyword." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string keyword = var.first.substr(21, end - 21); - - if (get("description").find(keyword) != std::string::npos) value += var.second; - } - } else if (var.first.substr(0, 12) == "urgency.uda.") { - // urgency.uda..coefficient - // urgency.uda...coefficient - auto end = var.first.find(".coefficient"); - if (end != std::string::npos) { - const std::string uda = var.first.substr(12, end - 12); - auto dot = uda.find('.'); - if (dot == std::string::npos) { - // urgency.uda..coefficient - if (has(uda)) value += var.second; - } else { - // urgency.uda...coefficient - if (get(uda.substr(0, dot)) == uda.substr(dot + 1)) value += var.second; - } - } + // Tag-, project-, keyword-, and UDA specific coefficients. + // We pre-parse with Task::userCoefficients so we can make + // per-task attribute lookups. We cache project and description + // across multiple coefficients of the same kind to avoid + // repeated lookups. + // The reason we do it with description, despite descriptions + // varying a large amount, is for the keyword coefficient. + // We don't do it with tag/UDAs because each refer to a unique one. + const std::string* projectName = nullptr; + const std::string* description = nullptr; + + for (const auto& uc : Task::userCoefficients) { + switch (uc.kind) { + case UrgencyCoefficient::project: { + if (projectName == nullptr) projectName = &get_ref("project"); + // Match exact project or subproject prefix. + if (*projectName == uc.name || (projectName->size() > uc.name.size() && + projectName->compare(0, uc.name.size(), uc.name) == 0 && + (*projectName)[uc.name.size()] == '.')) + value += uc.coefficient; + break; } + case UrgencyCoefficient::tag: + if (hasTag(uc.name)) value += uc.coefficient; + break; + case UrgencyCoefficient::keyword: + if (description == nullptr) description = &get_ref("description"); + if (description->find(uc.name) != std::string::npos) value += uc.coefficient; + break; + case UrgencyCoefficient::uda: + if (has(uc.name)) value += uc.coefficient; + break; + case UrgencyCoefficient::udaValue: + if (get_ref(uc.name) == uc.value) value += uc.coefficient; + break; } } - if (is_blocking && Context::getContext().config.getBoolean("urgency.inherit")) { + if (is_blocking && Task::urgencyInherit) { float prev = value; value = std::max(value, urgency_inherit()); @@ -1805,26 +1862,35 @@ float Task::urgency_c() const { } //////////////////////////////////////////////////////////////////////////////// -float Task::urgency() { +float Task::urgency() const { if (recalc_urgency) { - urgency_value = urgency_c(); - - // Return the sum of all terms. + // We set the guard first to avoid infinite recursion. It will be 0.0 on + // first call and then the computed value will be reused from the cache. + urgency_value = 0.0; recalc_urgency = false; + urgency_value = urgency_c(); } return urgency_value; } //////////////////////////////////////////////////////////////////////////////// +// Inherit urgency from the highest-urgency task dependent on this one. Uses +// the dependency graph via TDB2::dependency_graph() instead of scanning. float Task::urgency_inherit() const { float v = -FLT_MAX; #ifdef PRODUCT_TASKWARRIOR - // Calling getBlockedTasks is rather expensive. - // It is called recursively for each dependency in the chain here. - for (auto& task : getBlockedTasks()) { - // Find highest urgency in all blocked tasks. - v = std::max(v, task.urgency()); + auto& graph = Context::getContext().tdb2.dependency_graph(); + const auto& uuid = get_ref("uuid"); + auto found = graph.dependents.find(uuid); + + if (found != graph.dependents.end()) { + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) { + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) { + v = std::max(v, tasks[idx].urgency()); + } + } } #endif diff --git a/src/Task.h b/src/Task.h index 0b577baff..e1859245a 100644 --- a/src/Task.h +++ b/src/Task.h @@ -39,6 +39,15 @@ class Task { public: + // A user/UDA coefficient with a pre-parsed key, so urgency_c() + // doesn't re-parse each time. + struct UrgencyCoefficient { + enum Kind { project, tag, keyword, uda, udaValue } kind; + std::string name; // The name of the project/tag/keyword/UDA. + std::string value; // only for udaValue + float coefficient; + }; + static std::string defaultProject; static std::string defaultDue; static std::string defaultScheduled; @@ -46,6 +55,10 @@ class Task { static bool regex; static std::map attributes; // name -> type static std::map coefficients; + // Parsed version of coefficients, built by setUrgencyCoefficients(). + static std::vector userCoefficients; + // Cached value of rc.urgency.inherit. + static bool urgencyInherit; static std::map> customOrder; static float urgencyProjectCoefficient; static float urgencyActiveCoefficient; @@ -66,6 +79,7 @@ class Task { Task(const std::string&); Task(const json::object*); Task(rust::Box); + Task(rust::Box, int); void parse(const std::string&); std::string composeJSON(bool decorate = false); @@ -78,8 +92,8 @@ class Task { // Public data. int id{0}; - float urgency_value{0.0}; - bool recalc_urgency{true}; + mutable float urgency_value{0.0}; + mutable bool recalc_urgency{true}; bool is_blocked{false}; bool is_blocking{false}; int annotation_count{0}; @@ -88,6 +102,10 @@ class Task { static status textToStatus(const std::string&); static std::string statusToText(status); + // Parse coefficients into userCoefficients. This is called + // after that map has been initialized. + static void setUrgencyCoefficients(); + void setAsNow(const std::string&); bool has(const std::string&) const; std::vector all() const; @@ -172,7 +190,7 @@ class Task { void validate(bool applyDefault = true); float urgency_c() const; - float urgency(); + float urgency() const; #ifdef PRODUCT_TASKWARRIOR enum modType { modReplace, modPrepend, modAppend, modAnnotate }; @@ -185,7 +203,7 @@ class Task { int determineVersion(const std::string&); void parseJSON(const std::string&); void parseJSON(const json::object*); - void parseTC(rust::Box); + void parseTC(rust::Box, int); void parseLegacy(const std::string&); void validate_before(const std::string&, const std::string&); const std::string encode(const std::string&) const; diff --git a/src/ViewTask.cpp b/src/ViewTask.cpp index 5c04ec8f9..8fa40bd3d 100644 --- a/src/ViewTask.cpp +++ b/src/ViewTask.cpp @@ -105,7 +105,7 @@ ViewTask::~ViewTask() { // the larger fields. If the widest field is W0, and the second widest // field is W1, then a solution may be achievable by reducing W0 --> W1. // -std::string ViewTask::render(std::vector& data, std::vector& sequence) { +std::string ViewTask::render(const std::vector& data, std::vector& sequence) { Timer timer; bool const obfuscate = Context::getContext().config.getBoolean("obfuscate"); @@ -331,7 +331,7 @@ std::string ViewTask::render(std::vector& data, std::vector& sequence // changes. if (s > 0 && _breaks.size() > 0) { for (const auto& b : _breaks) { - if (data[sequence[s - 1]].get(b) != data[sequence[s]].get(b)) { + if (data[sequence[s - 1]].get_ref(b) != data[sequence[s]].get_ref(b)) { out += "\n"; ++_lines; diff --git a/src/ViewTask.h b/src/ViewTask.h index b606ec992..479edd6a4 100644 --- a/src/ViewTask.h +++ b/src/ViewTask.h @@ -66,7 +66,7 @@ class ViewTask { int rows() { return _rows; } // View rendering. - std::string render(std::vector&, std::vector&); + std::string render(const std::vector&, std::vector&); private: std::vector _columns; diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 446737d6d..dcfaf29cf 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #define STRING_COLUMN_LABEL_DEP "Depends" @@ -63,32 +64,32 @@ void ColumnDepends::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - auto deptasks = task.getDependencyTasks(); - if (deptasks.size() > 0) { - if (_style == "indicator") { + if (_style == "indicator") { + // We only need to know if the task has a dependency. We don't have to + // look at the whole list. The flags are set during cache construction. + if (task.is_blocked) { minimum = maximum = utf8_width(Context::getContext().config.get("dependency.indicator")); } + return; + } - else if (_style == "count") { - minimum = maximum = 2 + format((int)deptasks.size()).length(); - } + auto blocking_ids = task.getDependencyIDs(); - else if (_style == "default" || _style == "list") { + if (blocking_ids.size() > 0) { + if (_style == "count") { + minimum = maximum = 2 + format((int)blocking_ids.size()).length(); + } else if (_style == "default" || _style == "list") { minimum = maximum = 0; - std::vector blocking_ids; - blocking_ids.reserve(deptasks.size()); - for (auto& i : deptasks) blocking_ids.push_back(i.id); - auto all = join(" ", blocking_ids); maximum = all.length(); unsigned int length; - for (auto& i : deptasks) { - length = format(i.id).length(); + for (auto id : blocking_ids) { + length = format(id).length(); if (length > minimum) minimum = length; } } @@ -96,24 +97,24 @@ void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnDepends::render(std::vector& lines, Task& task, int width, Color& color) { - auto deptasks = task.getDependencyTasks(); - - if (deptasks.size() > 0) { - if (_style == "indicator") { +void ColumnDepends::render(std::vector& lines, const Task& task, int width, + Color& color) { + // We only need to know if the task has a dependency. We don't have to + // look at the whole list. The flags are set during cache construction. + if (_style == "indicator") { + if (task.is_blocked) renderStringRight(lines, width, color, Context::getContext().config.get("dependency.indicator")); - } - - else if (_style == "count") { - renderStringRight(lines, width, color, '[' + format(static_cast(deptasks.size())) + ']'); - } + return; + } - else if (_style == "default" || _style == "list") { - std::vector blocking_ids; - blocking_ids.reserve(deptasks.size()); - for (const auto& t : deptasks) blocking_ids.push_back(t.id); + auto blocking_ids = task.getDependencyIDs(); + if (blocking_ids.size() > 0) { + if (_style == "count") { + renderStringRight(lines, width, color, + '[' + format(static_cast(blocking_ids.size())) + ']'); + } else if (_style == "default" || _style == "list") { auto combined = join(" ", blocking_ids); std::vector all; @@ -152,9 +153,9 @@ void ColumnDepends::modify(Task& task, const std::string& value) { Task loaded_task; if (Context::getContext().tdb2.get(dep, loaded_task)) if (removal) - task.removeDependency(loaded_task.get("uuid")); + task.removeDependency(loaded_task.get_ref("uuid")); else - task.addDependency(loaded_task.get("uuid")); + task.addDependency(loaded_task.get_ref("uuid")); else throw format("Dependency could not be set - task with UUID '{1}' does not exist.", dep); } diff --git a/src/columns/ColDepends.h b/src/columns/ColDepends.h index ef79b8f06..80c3540e6 100644 --- a/src/columns/ColDepends.h +++ b/src/columns/ColDepends.h @@ -34,8 +34,8 @@ class ColumnDepends : public ColumnTypeString { ColumnDepends(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColDescription.cpp b/src/columns/ColDescription.cpp index 460465cff..757d4250a 100644 --- a/src/columns/ColDescription.cpp +++ b/src/columns/ColDescription.cpp @@ -71,8 +71,8 @@ ColumnDescription::ColumnDescription() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnDescription::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { - std::string description = task.get(_name); +void ColumnDescription::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { + const auto& description = task.get_ref(_name); // The text // @@ -130,8 +130,10 @@ void ColumnDescription::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnDescription::render(std::vector& lines, Task& task, int width, +void ColumnDescription::render(std::vector& lines, const Task& task, int width, Color& color) { + // Unlike measure, we use get() here because this render function appends + // annotation counts or strings. std::string description = task.get(_name); // This is a description diff --git a/src/columns/ColDescription.h b/src/columns/ColDescription.h index 0178316a5..fdac89005 100644 --- a/src/columns/ColDescription.h +++ b/src/columns/ColDescription.h @@ -32,8 +32,8 @@ class ColumnDescription : public ColumnTypeString { public: ColumnDescription(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: bool _hyphenate; diff --git a/src/columns/ColID.cpp b/src/columns/ColID.cpp index 69f572434..105958281 100644 --- a/src/columns/ColID.cpp +++ b/src/columns/ColID.cpp @@ -43,7 +43,7 @@ ColumnID::ColumnID() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnID::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnID::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { int length; if (task.id < 10) @@ -63,7 +63,7 @@ void ColumnID::measure(Task& task, unsigned int& minimum, unsigned int& maximum) } //////////////////////////////////////////////////////////////////////////////// -void ColumnID::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnID::render(std::vector& lines, const Task& task, int width, Color& color) { // Completed and deleted tasks have no ID. if (task.id) renderInteger(lines, width, color, task.id); diff --git a/src/columns/ColID.h b/src/columns/ColID.h index 8279995a3..4021a0b49 100644 --- a/src/columns/ColID.h +++ b/src/columns/ColID.h @@ -32,8 +32,8 @@ class ColumnID : public ColumnTypeNumeric { public: ColumnID(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColIMask.cpp b/src/columns/ColIMask.cpp index 067e8b85c..e59031d7c 100644 --- a/src/columns/ColIMask.cpp +++ b/src/columns/ColIMask.cpp @@ -42,14 +42,15 @@ ColumnIMask::ColumnIMask() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnIMask::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnIMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnIMask::render(std::vector& lines, Task& task, int width, Color& color) { - if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); +void ColumnIMask::render(std::vector& lines, const Task& task, int width, + Color& color) { + if (task.has(_name)) renderStringRight(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColIMask.h b/src/columns/ColIMask.h index 7fdc4b07f..c9abbedb6 100644 --- a/src/columns/ColIMask.h +++ b/src/columns/ColIMask.h @@ -32,8 +32,8 @@ class ColumnIMask : public ColumnTypeNumeric { public: ColumnIMask(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColLast.cpp b/src/columns/ColLast.cpp index 341f4dcee..ec8313808 100644 --- a/src/columns/ColLast.cpp +++ b/src/columns/ColLast.cpp @@ -42,14 +42,15 @@ ColumnLast::ColumnLast() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnLast::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnLast::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnLast::render(std::vector& lines, Task& task, int width, Color& color) { - if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); +void ColumnLast::render(std::vector& lines, const Task& task, int width, + Color& color) { + if (task.has(_name)) renderStringRight(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColLast.h b/src/columns/ColLast.h index 8f24c291f..a56afcc14 100644 --- a/src/columns/ColLast.h +++ b/src/columns/ColLast.h @@ -32,8 +32,8 @@ class ColumnLast : public ColumnTypeNumeric { public: ColumnLast(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColMask.cpp b/src/columns/ColMask.cpp index ca5a52f92..cd34df30b 100644 --- a/src/columns/ColMask.cpp +++ b/src/columns/ColMask.cpp @@ -42,14 +42,15 @@ ColumnMask::ColumnMask() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnMask::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnMask::render(std::vector& lines, Task& task, int width, Color& color) { - if (task.has(_name)) renderStringLeft(lines, width, color, task.get(_name)); +void ColumnMask::render(std::vector& lines, const Task& task, int width, + Color& color) { + if (task.has(_name)) renderStringLeft(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColMask.h b/src/columns/ColMask.h index b7f28a9f4..1a4e9c4d2 100644 --- a/src/columns/ColMask.h +++ b/src/columns/ColMask.h @@ -32,8 +32,8 @@ class ColumnMask : public ColumnTypeString { public: ColumnMask(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColParent.cpp b/src/columns/ColParent.cpp index e49315904..398048fd8 100644 --- a/src/columns/ColParent.cpp +++ b/src/columns/ColParent.cpp @@ -42,7 +42,7 @@ ColumnParent::ColumnParent() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnParent::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnParent::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "long") @@ -53,15 +53,16 @@ void ColumnParent::measure(Task& task, unsigned int& minimum, unsigned int& maxi } //////////////////////////////////////////////////////////////////////////////// -void ColumnParent::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnParent::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } } diff --git a/src/columns/ColParent.h b/src/columns/ColParent.h index 9332a729d..7fe814c6d 100644 --- a/src/columns/ColParent.h +++ b/src/columns/ColParent.h @@ -32,8 +32,8 @@ class ColumnParent : public ColumnTypeString { public: ColumnParent(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColProject.cpp b/src/columns/ColProject.cpp index 0454bb5bb..fcd9833f8 100644 --- a/src/columns/ColProject.cpp +++ b/src/columns/ColProject.cpp @@ -50,13 +50,13 @@ ColumnProject::ColumnProject() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnProject::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnProject::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { std::string project = task.get(_name); if (_style == "parent") { - auto period = project.find('.'); + auto period = project.find("."); if (period != std::string::npos) project = project.substr(0, period); } else if (_style == "indented") { project = indentProject(project, " ", '.'); @@ -68,7 +68,8 @@ void ColumnProject::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnProject::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnProject::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { std::string project = task.get(_name); if (_style == "parent") { diff --git a/src/columns/ColProject.h b/src/columns/ColProject.h index ae470f3b9..3097d4245 100644 --- a/src/columns/ColProject.h +++ b/src/columns/ColProject.h @@ -32,8 +32,8 @@ class ColumnProject : public ColumnTypeString { public: ColumnProject(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColRType.cpp b/src/columns/ColRType.cpp index 600e2201c..3c2651cae 100644 --- a/src/columns/ColRType.cpp +++ b/src/columns/ColRType.cpp @@ -56,25 +56,26 @@ void ColumnRType::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnRType::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnRType::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") - minimum = maximum = task.get(_name).length(); + minimum = maximum = task.get_ref(_name).length(); else if (_style == "indicator") minimum = maximum = 1; } } //////////////////////////////////////////////////////////////////////////////// -void ColumnRType::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnRType::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") - renderStringRight(lines, width, color, task.get(_name)); + renderStringRight(lines, width, color, task.get_ref(_name)); else if (_style == "indicator") { std::string value{" "}; - value[0] = toupper(task.get(_name)[0]); + value[0] = toupper(task.get_ref(_name)[0]); renderStringRight(lines, width, color, value); } } diff --git a/src/columns/ColRType.h b/src/columns/ColRType.h index dc7b86ffe..1b2482716 100644 --- a/src/columns/ColRType.h +++ b/src/columns/ColRType.h @@ -33,8 +33,8 @@ class ColumnRType : public ColumnTypeString { public: ColumnRType(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); bool validate(const std::string&) const; private: diff --git a/src/columns/ColRecur.cpp b/src/columns/ColRecur.cpp index dc6f0fefc..fd7687ff8 100644 --- a/src/columns/ColRecur.cpp +++ b/src/columns/ColRecur.cpp @@ -60,11 +60,11 @@ void ColumnRecur::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnRecur::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnRecur::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "duration") { - minimum = maximum = Duration(task.get(_name)).formatISO().length(); + minimum = maximum = Duration(task.get_ref(_name)).formatISO().length(); } else if (_style == "indicator") { minimum = maximum = utf8_width(Context::getContext().config.get("recurrence.indicator")); } @@ -72,10 +72,11 @@ void ColumnRecur::measure(Task& task, unsigned int& minimum, unsigned int& maxim } //////////////////////////////////////////////////////////////////////////////// -void ColumnRecur::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnRecur::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default" || _style == "duration") - renderStringRight(lines, width, color, Duration(task.get(_name)).formatISO()); + renderStringRight(lines, width, color, Duration(task.get_ref(_name)).formatISO()); else if (_style == "indicator") renderStringRight(lines, width, color, diff --git a/src/columns/ColRecur.h b/src/columns/ColRecur.h index 3de674aa9..c2fb104c3 100644 --- a/src/columns/ColRecur.h +++ b/src/columns/ColRecur.h @@ -35,8 +35,8 @@ class ColumnRecur : public ColumnTypeString { public: ColumnRecur(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColStart.cpp b/src/columns/ColStart.cpp index 4a4dcc1a4..703b085ae 100644 --- a/src/columns/ColStart.cpp +++ b/src/columns/ColStart.cpp @@ -51,7 +51,7 @@ void ColumnStart::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnStart::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnStart::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "active") @@ -64,7 +64,8 @@ void ColumnStart::measure(Task& task, unsigned int& minimum, unsigned int& maxim } //////////////////////////////////////////////////////////////////////////////// -void ColumnStart::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnStart::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "active") { if (!task.has("end")) diff --git a/src/columns/ColStart.h b/src/columns/ColStart.h index 2196ccabd..040c86aa7 100644 --- a/src/columns/ColStart.h +++ b/src/columns/ColStart.h @@ -33,8 +33,8 @@ class ColumnStart : public ColumnTypeDate { public: ColumnStart(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); }; #endif diff --git a/src/columns/ColStatus.cpp b/src/columns/ColStatus.cpp index 1fed6d73f..43fb1e6f3 100644 --- a/src/columns/ColStatus.cpp +++ b/src/columns/ColStatus.cpp @@ -51,7 +51,7 @@ void ColumnStatus::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnStatus::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnStatus::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { Task::status status = task.getStatus(); if (_style == "default" || _style == "long") { @@ -70,7 +70,8 @@ void ColumnStatus::measure(Task& task, unsigned int& minimum, unsigned int& maxi } //////////////////////////////////////////////////////////////////////////////// -void ColumnStatus::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnStatus::render(std::vector& lines, const Task& task, int width, + Color& color) { Task::status status = task.getStatus(); std::string value; diff --git a/src/columns/ColStatus.h b/src/columns/ColStatus.h index 9c6f2d9a4..1c2026ef4 100644 --- a/src/columns/ColStatus.h +++ b/src/columns/ColStatus.h @@ -33,8 +33,8 @@ class ColumnStatus : public ColumnTypeString { public: ColumnStatus(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColTags.cpp b/src/columns/ColTags.cpp index ceaf86187..d5a801688 100644 --- a/src/columns/ColTags.cpp +++ b/src/columns/ColTags.cpp @@ -64,7 +64,7 @@ void ColumnTags::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTags::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTags::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.getTagCount() > 0) { if (_style == "indicator") { @@ -86,7 +86,8 @@ void ColumnTags::measure(Task& task, unsigned int& minimum, unsigned int& maximu } //////////////////////////////////////////////////////////////////////////////// -void ColumnTags::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTags::render(std::vector& lines, const Task& task, int width, + Color& color) { auto all = task.getTags(); if (all.size() > 0) { if (_style == "default" || _style == "list") { diff --git a/src/columns/ColTags.h b/src/columns/ColTags.h index 86d58e335..709b7a554 100644 --- a/src/columns/ColTags.h +++ b/src/columns/ColTags.h @@ -33,8 +33,8 @@ class ColumnTags : public ColumnTypeString { public: ColumnTags(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColTemplate.cpp b/src/columns/ColTemplate.cpp index 02f536b1c..379110120 100644 --- a/src/columns/ColTemplate.cpp +++ b/src/columns/ColTemplate.cpp @@ -42,7 +42,7 @@ ColumnTemplate::ColumnTemplate() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTemplate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTemplate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "long") @@ -53,15 +53,16 @@ void ColumnTemplate::measure(Task& task, unsigned int& minimum, unsigned int& ma } //////////////////////////////////////////////////////////////////////////////// -void ColumnTemplate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTemplate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } } diff --git a/src/columns/ColTemplate.h b/src/columns/ColTemplate.h index 991f42a37..88d7880e1 100644 --- a/src/columns/ColTemplate.h +++ b/src/columns/ColTemplate.h @@ -32,8 +32,8 @@ class ColumnTemplate : public ColumnTypeString { public: ColumnTemplate(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColTypeDate.cpp b/src/columns/ColTypeDate.cpp index cb459cd3b..bf74358bb 100644 --- a/src/columns/ColTypeDate.cpp +++ b/src/columns/ColTypeDate.cpp @@ -58,7 +58,7 @@ ColumnTypeDate::ColumnTypeDate() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTypeDate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTypeDate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { Datetime date(task.get_date(_name)); @@ -102,7 +102,8 @@ void ColumnTypeDate::measure(Task& task, unsigned int& minimum, unsigned int& ma } //////////////////////////////////////////////////////////////////////////////// -void ColumnTypeDate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTypeDate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { Datetime date(task.get_date(_name)); diff --git a/src/columns/ColTypeDate.h b/src/columns/ColTypeDate.h index dc4bc9aea..29fd9617a 100644 --- a/src/columns/ColTypeDate.h +++ b/src/columns/ColTypeDate.h @@ -37,8 +37,8 @@ class ColumnTypeDate : public Column { public: ColumnTypeDate(); - virtual void measure(Task&, unsigned int&, unsigned int&); - virtual void render(std::vector&, Task&, int, Color&); + virtual void measure(const Task&, unsigned int&, unsigned int&); + virtual void render(std::vector&, const Task&, int, Color&); virtual bool validate(const std::string&) const; virtual void modify(Task&, const std::string&); }; diff --git a/src/columns/ColUDA.cpp b/src/columns/ColUDA.cpp index a6452c631..c9c3f50b6 100644 --- a/src/columns/ColUDA.cpp +++ b/src/columns/ColUDA.cpp @@ -63,11 +63,11 @@ bool ColumnUDAString::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDAString::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDAString::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - std::string value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") { auto stripped = Color::strip(value); maximum = longestLine(stripped); @@ -83,10 +83,11 @@ void ColumnUDAString::measure(Task& task, unsigned int& minimum, unsigned int& m } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDAString::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUDAString::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") { - std::string value = task.get(_name); + const auto& value = task.get_ref(_name); std::vector raw; wrapText(raw, value, width, _hyphenate); @@ -126,11 +127,11 @@ bool ColumnUDANumeric::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDANumeric::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDANumeric::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") minimum = maximum = value.length(); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -142,11 +143,11 @@ void ColumnUDANumeric::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDANumeric::render(std::vector& lines, Task& task, int width, +void ColumnUDANumeric::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); renderStringRight(lines, width, color, value); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -183,11 +184,11 @@ bool ColumnUDADate::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDADate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDADate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") { // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -210,10 +211,11 @@ void ColumnUDADate::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDADate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUDADate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -262,11 +264,11 @@ bool ColumnUDADuration::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDADuration::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDADuration::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") minimum = maximum = Duration(value).formatISO().length(); } else if (_style == "indicator") { if (task.has(_name)) { @@ -281,11 +283,11 @@ void ColumnUDADuration::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDADuration::render(std::vector& lines, Task& task, int width, +void ColumnUDADuration::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); renderStringRight(lines, width, color, Duration(value).formatISO()); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); diff --git a/src/columns/ColUDA.h b/src/columns/ColUDA.h index 04d57a8d7..662ed7e66 100644 --- a/src/columns/ColUDA.h +++ b/src/columns/ColUDA.h @@ -38,8 +38,8 @@ class ColumnUDAString : public ColumnTypeString { public: ColumnUDAString(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -53,8 +53,8 @@ class ColumnUDANumeric : public ColumnTypeNumeric { public: ColumnUDANumeric(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -65,8 +65,8 @@ class ColumnUDADate : public ColumnTypeDate { public: ColumnUDADate(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -77,8 +77,8 @@ class ColumnUDADuration : public ColumnTypeDuration { public: ColumnUDADuration(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; diff --git a/src/columns/ColUUID.cpp b/src/columns/ColUUID.cpp index 3b81a4419..df1353dad 100644 --- a/src/columns/ColUUID.cpp +++ b/src/columns/ColUUID.cpp @@ -42,7 +42,7 @@ ColumnUUID::ColumnUUID() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnUUID::measure(Task&, unsigned int& minimum, unsigned int& maximum) { +void ColumnUUID::measure(const Task&, unsigned int& minimum, unsigned int& maximum) { // Mandatory attribute, no need to check the value. if (_style == "default" || _style == "long") @@ -52,16 +52,17 @@ void ColumnUUID::measure(Task&, unsigned int& minimum, unsigned int& maximum) { } //////////////////////////////////////////////////////////////////////////////// -void ColumnUUID::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUUID::render(std::vector& lines, const Task& task, int width, + Color& color) { // No need to check the presence of UUID - all tasks have one. // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColUUID.h b/src/columns/ColUUID.h index 7a8232a41..b45808c95 100644 --- a/src/columns/ColUUID.h +++ b/src/columns/ColUUID.h @@ -32,8 +32,8 @@ class ColumnUUID : public ColumnTypeString { public: ColumnUUID(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColUrgency.cpp b/src/columns/ColUrgency.cpp index d202a7d91..87fcb058f 100644 --- a/src/columns/ColUrgency.cpp +++ b/src/columns/ColUrgency.cpp @@ -42,7 +42,7 @@ ColumnUrgency::ColumnUrgency() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnUrgency::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUrgency::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { if (_style == "default" || _style == "real") minimum = maximum = format(task.urgency(), 4, 3).length(); @@ -51,7 +51,8 @@ void ColumnUrgency::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnUrgency::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUrgency::render(std::vector& lines, const Task& task, int width, + Color& color) { if (_style == "default" || _style == "real") renderDouble(lines, width, color, task.urgency()); diff --git a/src/columns/ColUrgency.h b/src/columns/ColUrgency.h index c4c31ab0d..e826f4daf 100644 --- a/src/columns/ColUrgency.h +++ b/src/columns/ColUrgency.h @@ -32,8 +32,8 @@ class ColumnUrgency : public ColumnTypeNumeric { public: ColumnUrgency(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/Column.h b/src/columns/Column.h index 20dad26fb..a6cb728f9 100644 --- a/src/columns/Column.h +++ b/src/columns/Column.h @@ -58,10 +58,10 @@ class Column { virtual void setReport(const std::string& value) { _report = value; } virtual void measure(const std::string&, unsigned int&, unsigned int&) {}; - virtual void measure(Task&, unsigned int&, unsigned int&) {}; + virtual void measure(const Task&, unsigned int&, unsigned int&) {}; virtual void renderHeader(std::vector&, int, Color&); virtual void render(std::vector&, const std::string&, int, Color&) {}; - virtual void render(std::vector&, Task&, int, Color&) {}; + virtual void render(std::vector&, const Task&, int, Color&) {}; virtual bool validate(const std::string&) const { return false; }; virtual void modify(Task&, const std::string&) {}; diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index 8bba6c902..d14a84e8e 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -101,27 +101,44 @@ int CmdCustom::execute(std::string& output) { // Add the report filter to any existing filter. if (reportFilter != "") Context::getContext().cli2.addFilter(reportFilter); - // Apply filter. + // Apply filter. When the filter is pending only, we use indices with the pending + // cache. Otherwise we fall back to copying all tasks. Filter filter; + const std::vector* data = nullptr; + std::vector sequence; std::vector filtered; - filter.subset(filtered); - std::vector sequence; + // call prepareFilter() here so that filter_to_indices and filter_to_tasks + // don't have to call it. + Context::getContext().cli2.prepareFilter(); + + bool use_pending_indices = filter.pendingOnly(); + + if (use_pending_indices) { + const auto& pending = Context::getContext().tdb2.pending_tasks(); + filter.filter_to_indices(pending, sequence); + data = &pending; + } else { + auto all = Context::getContext().tdb2.all_tasks(); + filter.filter_to_tasks(all, filtered); + sequence.clear(); + for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); + data = &filtered; + } + if (sortOrder.size() && sortOrder[0] == "none") { - // Assemble a sequence vector that represents the tasks listed in - // Context::getContext ().cli2._uuid_ranges, in the order in which they appear. This - // equates to no sorting, just a specified order. + // If there is no sort order, we preserve the order they were specified + // and rebuild the sequence using indices. sortOrder.clear(); + sequence.clear(); for (auto& i : Context::getContext().cli2._uuid_list) - for (unsigned int t = 0; t < filtered.size(); ++t) - if (filtered[t].get("uuid") == i) sequence.push_back(t); + for (unsigned int t = 0; t < data->size(); ++t) + if ((*data)[t].get_ref("uuid") == i) sequence.push_back(t); } else { - // There is a sortOrder, so sorting will take place, which means the initial - // order of sequence is ascending. - for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); - - // Sort the tasks. - if (sortOrder.size()) sort_tasks(filtered, sequence, reportSort); + if (sortOrder.size()) { + // If there is a sortOrder, sorting will reorder the indices. + sort_tasks(*data, sequence, reportSort); + } } // Configure the view. @@ -197,19 +214,19 @@ int CmdCustom::execute(std::string& output) { // Render. std::stringstream out; - if (filtered.size()) { + if (sequence.size()) { view.truncateRows(maxrows); view.truncateLines(maxlines); - out << optionalBlankLine() << view.render(filtered, sequence) << optionalBlankLine(); + out << optionalBlankLine() << view.render(*data, sequence) << optionalBlankLine(); // Print the number of rendered tasks if (Context::getContext().verbose("affected")) { - out << (filtered.size() == 1 ? "1 task" : format("{1} tasks", filtered.size())); + out << (sequence.size() == 1 ? "1 task" : format("{1} tasks", sequence.size())); - if (maxrows && maxrows < (int)filtered.size()) out << ", " << format("{1} shown", maxrows); + if (maxrows && maxrows < (int)sequence.size()) out << ", " << format("{1} shown", maxrows); - if (maxlines && maxlines < (int)filtered.size()) + if (maxlines && maxlines < (int)sequence.size()) out << ", " << format("truncated to {1} lines", maxlines - table_header); out << '\n'; diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index 35b5c7ecf..055db2eac 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #define STRING_CMD_DELETE_TASK_R "Deleting recurring task {1} '{2}'." @@ -74,7 +75,17 @@ int CmdDelete::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; + + RecurrenceMaskUpdates recurrenceMaskUpdates; + const auto recurrenceConfirmation = Context::getContext().config.get("recurrence.confirmation"); + const auto hasRecurringParent = + std::any_of(filtered.begin(), filtered.end(), + [](const Task& task) { return task.getStatus() == Task::recurring; }); + RecurrenceMaskUpdates* recurrenceMaskUpdatesPtr = nullptr; + if (!Context::getContext().hooks.hasOnModify() && recurrenceConfirmation != "prompt" && + !Context::getContext().config.getBoolean("recurrence.confirmation") && !hasRecurringParent) + recurrenceMaskUpdatesPtr = &recurrenceMaskUpdates; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -92,14 +103,14 @@ int CmdDelete::execute(std::string&) { if (!task.has("end")) task.setAsNow("end"); if (permission(question, filtered.size())) { - updateRecurrenceMask(task); + updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); ++count; Context::getContext().tdb2.modify(task); feedback_affected("Deleting task {1} '{2}'.", task); - feedback_unblocked(task); + if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task); + projectChanges.insert_or_assign(task.get("project"), task); // Delete siblings. if (task.has("parent")) { @@ -112,7 +123,7 @@ int CmdDelete::execute(std::string&) { sibling.setStatus(Task::deleted); if (!sibling.has("end")) sibling.setAsNow("end"); - updateRecurrenceMask(sibling); + updateRecurrenceMask(sibling, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(sibling); feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); feedback_unblocked(sibling); @@ -141,7 +152,7 @@ int CmdDelete::execute(std::string&) { child.setStatus(Task::deleted); if (!child.has("end")) child.setAsNow("end"); - updateRecurrenceMask(child); + updateRecurrenceMask(child, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(child); feedback_affected(STRING_CMD_DELETE_TASK_R, child); feedback_unblocked(child); @@ -162,9 +173,11 @@ int CmdDelete::execute(std::string&) { } } + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); + // Now list the project changes. - for (const auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + for (auto& change : projectChanges) + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second)); feedback_affected(count == 1 ? "Deleted {1} task." : "Deleted {1} tasks.", count); diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index deb10a0dc..4416e4dd9 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -70,7 +70,11 @@ int CmdDone::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; + + RecurrenceMaskUpdates recurrenceMaskUpdates; + auto* recurrenceMaskUpdatesPtr = + Context::getContext().hooks.hasOnModify() ? nullptr : &recurrenceMaskUpdates; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -97,14 +101,14 @@ int CmdDone::execute(std::string&) { } if (permission(before.diff(task) + question, filtered.size())) { - updateRecurrenceMask(task); + updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(task); ++count; feedback_affected("Completed task {1} '{2}'.", task); - feedback_unblocked(task); + if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task); + projectChanges.insert_or_assign(task.get("project"), task); // Save unmodified task for potential nagging later modified.push_back(before); @@ -121,11 +125,12 @@ int CmdDone::execute(std::string&) { } } + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); nag(modified); // Now list the project changes. - for (const auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + for (auto& change : projectChanges) + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second)); feedback_affected(count == 1 ? "Completed {1} task." : "Completed {1} tasks.", count); return rc; diff --git a/src/commands/CmdExport.cpp b/src/commands/CmdExport.cpp index 2924645dd..7e382a6b3 100644 --- a/src/commands/CmdExport.cpp +++ b/src/commands/CmdExport.cpp @@ -97,7 +97,7 @@ int CmdExport::execute(std::string& output) { sortOrder.clear(); for (auto& i : Context::getContext().cli2._uuid_list) for (unsigned int t = 0; t < filtered.size(); ++t) - if (filtered[t].get("uuid") == i) sequence.push_back(t); + if (filtered[t].get_ref("uuid") == i) sequence.push_back(t); } else { // sort_tasks requires the order array initially be identity for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); diff --git a/src/commands/CmdImport.cpp b/src/commands/CmdImport.cpp index b3ccc0c2a..7260c4218 100644 --- a/src/commands/CmdImport.cpp +++ b/src/commands/CmdImport.cpp @@ -189,7 +189,8 @@ void CmdImport::importSingleTask(json::object* obj) { Task before; auto uuid = task.get("uuid"); uuid_occurrences[uuid]++; - if (Context::getContext().tdb2.get(uuid, before)) { + auto& tdb2 = Context::getContext().tdb2; + if (tdb2.has(uuid) && tdb2.get(uuid, before)) { // We need to neglect updates from attributes with dynamic defaults // unless they have been explicitly specified on import. // @@ -218,7 +219,7 @@ void CmdImport::importSingleTask(json::object* obj) { std::cout << " skip "; } } else { - Context::getContext().tdb2.add(task); + tdb2.add(task); std::cout << " add "; } diff --git a/src/commands/CmdStart.cpp b/src/commands/CmdStart.cpp index 057b61fc5..7cf0fbbb6 100644 --- a/src/commands/CmdStart.cpp +++ b/src/commands/CmdStart.cpp @@ -70,7 +70,7 @@ int CmdStart::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -103,7 +103,7 @@ int CmdStart::execute(std::string&) { feedback_affected("Starting task {1} '{2}'.", task); dependencyChainOnStart(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task, false); + projectChanges.insert_or_assign(task.get("project"), task); // Save unmodified task for potential nagging later modified.push_back(before); @@ -123,7 +123,7 @@ int CmdStart::execute(std::string&) { // Now list the project changes. for (auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second, false)); feedback_affected(count == 1 ? "Started {1} task." : "Started {1} tasks.", count); return rc; diff --git a/src/commands/CmdStop.cpp b/src/commands/CmdStop.cpp index 9924c5a58..5df670371 100644 --- a/src/commands/CmdStop.cpp +++ b/src/commands/CmdStop.cpp @@ -68,7 +68,7 @@ int CmdStop::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -94,7 +94,7 @@ int CmdStop::execute(std::string&) { feedback_affected("Stopping task {1} '{2}'.", task); dependencyChainOnStart(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task, false); + projectChanges.insert_or_assign(task.get("project"), task); } else { std::cout << "Task not stopped.\n"; rc = 1; @@ -110,7 +110,7 @@ int CmdStop::execute(std::string&) { // Now list the project changes. for (auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second, false)); feedback_affected(count == 1 ? "Stopped {1} task." : "Stopped {1} tasks.", count); return rc; diff --git a/src/commands/CmdSync.cpp b/src/commands/CmdSync.cpp index 6e966286a..7dafecd05 100644 --- a/src/commands/CmdSync.cpp +++ b/src/commands/CmdSync.cpp @@ -190,6 +190,8 @@ int CmdSync::execute(std::string& output) { if (context.config.getBoolean("purge.on-sync")) { context.tdb2.expire_tasks(); + } else { + context.tdb2.invalidate_cache(); } if (verbose) { diff --git a/src/commands/CmdUndo.cpp b/src/commands/CmdUndo.cpp index 83156410e..8cac605b9 100644 --- a/src/commands/CmdUndo.cpp +++ b/src/commands/CmdUndo.cpp @@ -61,6 +61,8 @@ int CmdUndo::execute(std::string&) { // need not be done here. if (!replica->commit_reversed_operations(std::move(undo_ops))) { std::cout << "Could not undo: other operations have occurred."; + } else { + Context::getContext().tdb2.invalidate_cache(); } } return 0; diff --git a/src/feedback.cpp b/src/feedback.cpp index efd2302b4..b33176131 100644 --- a/src/feedback.cpp +++ b/src/feedback.cpp @@ -90,7 +90,7 @@ void feedback_affected(const std::string& effect, int quantity) { // {2} Description void feedback_affected(const std::string& effect, const Task& task) { if (Context::getContext().verbose("affected")) { - std::cout << format(effect, task.identifier(true), task.get("description")) << "\n"; + std::cout << format(effect, task.identifier(true), task.get_ref("description")) << "\n"; } } @@ -152,10 +152,10 @@ void feedback_unblocked(const Task& task) { auto blocking = i.getDependencyTasks(); if (blocking.size() == 0) { if (i.id) - std::cout << format("Unblocked {1} '{2}'.", i.id, i.get("description")) << "\n"; + std::cout << format("Unblocked {1} '{2}'.", i.id, i.get_ref("description")) << "\n"; else { - std::string uuid = i.get("uuid"); - std::cout << format("Unblocked {1} '{2}'.", i.get("uuid"), i.get("description")) << "\n"; + const auto& uuid = i.get_ref("uuid"); + std::cout << format("Unblocked {1} '{2}'.", uuid, i.get_ref("description")) << "\n"; } } } @@ -182,7 +182,7 @@ void feedback_backlog() { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task, bool scope /* = true */) { std::stringstream msg; - std::string project = task.get("project"); + const auto& project = task.get_ref("project"); if (project != "") { if (scope) msg << format("The project '{1}' has changed.", project) << " "; @@ -220,7 +220,7 @@ std::string onProjectChange(Task& task, bool scope /* = true */) { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task1, Task& task2) { - if (task1.get("project") == task2.get("project")) return onProjectChange(task1, false); + if (task1.get_ref("project") == task2.get_ref("project")) return onProjectChange(task1, false); std::string messages1 = onProjectChange(task1); std::string messages2 = onProjectChange(task2); @@ -236,7 +236,7 @@ std::string onExpiration(Task& task) { if (Context::getContext().verbose("affected")) msg << format("Task {1} '{2}' expired and was deleted.", task.identifier(true), - task.get("description")); + task.get_ref("description")); return msg.str(); } @@ -245,7 +245,7 @@ std::string onExpiration(Task& task) { static void countTasks(const std::vector& all, const std::string& project, int& count_pending, int& count_done) { for (auto& it : all) { - if (it.get("project") == project) { + if (it.get_ref("project") == project) { switch (it.getStatus()) { case Task::pending: case Task::waiting: diff --git a/src/nag.cpp b/src/nag.cpp index 356287f45..5a65a3145 100644 --- a/src/nag.cpp +++ b/src/nag.cpp @@ -36,12 +36,13 @@ void nag(std::vector& tasks) { auto msg = Context::getContext().config.get("nag"); if (msg == "") return; - auto pending = Context::getContext().tdb2.pending_tasks(); - for (auto& t1 : tasks) { + const auto& pending = Context::getContext().tdb2.pending_tasks(); + for (const auto& t1 : tasks) { if (t1.hasTag("nonag")) continue; - for (auto& t2 : pending) { - if (t1.get("uuid") != t2.get("uuid") && t2.hasTag("READY") && t1.urgency() < t2.urgency()) { + for (const auto& t2 : pending) { + if (t1.get_ref("uuid") != t2.get_ref("uuid") && t2.hasTag("READY") && + t1.urgency() < t2.urgency()) { Context::getContext().footnote(msg); return; } diff --git a/src/recur.cpp b/src/recur.cpp index dc3d957d3..2360a9d47 100644 --- a/src/recur.cpp +++ b/src/recur.cpp @@ -365,35 +365,54 @@ std::optional getNextRecurrence(Datetime& current, std::string& period //////////////////////////////////////////////////////////////////////////////// // When the status of a recurring child task changes, the parent task must // update it's mask. -void updateRecurrenceMask(Task& task) { +static void updateRecurrenceMaskValue(Task& task, Task& parent) { + unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10); + auto mask = parent.get("mask"); + if (mask.length() > index) { + mask[index] = (task.getStatus() == Task::pending) ? '-' + : (task.getStatus() == Task::completed) ? '+' + : (task.getStatus() == Task::deleted) ? 'X' + : (task.getStatus() == Task::waiting) ? 'W' + : '?'; + } else { + mask.clear(); + for (unsigned int i = 0; i < index; ++i) mask += "?"; + + mask += (task.getStatus() == Task::pending) ? '-' + : (task.getStatus() == Task::completed) ? '+' + : (task.getStatus() == Task::deleted) ? 'X' + : (task.getStatus() == Task::waiting) ? 'W' + : '?'; + } + parent.set("mask", mask); +} + +void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates) { auto uuid = task.get("parent"); - Task parent; - - if (uuid != "" && Context::getContext().tdb2.get(uuid, parent)) { - unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10); - auto mask = parent.get("mask"); - if (mask.length() > index) { - mask[index] = (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; - } else { - std::string mask; - for (unsigned int i = 0; i < index; ++i) mask += "?"; - - mask += (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; + if (uuid == "") return; + + if (updates) { + auto existing = updates->find(uuid); + if (existing == updates->end()) { + Task parent; + if (!Context::getContext().tdb2.get(uuid, parent)) return; + existing = updates->emplace(uuid, std::move(parent)).first; } - parent.set("mask", mask); + updateRecurrenceMaskValue(task, existing->second); + } else { + Task parent; + if (!Context::getContext().tdb2.get(uuid, parent)) return; + updateRecurrenceMaskValue(task, parent); Context::getContext().tdb2.modify(parent); } } +void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates) { + for (auto& update : updates) Context::getContext().tdb2.modify(update.second); + updates.clear(); +} + //////////////////////////////////////////////////////////////////////////////// // Delete expired tasks. void handleUntil() { diff --git a/src/recur.h b/src/recur.h index db0b6306d..7ae82f6a4 100644 --- a/src/recur.h +++ b/src/recur.h @@ -43,13 +43,17 @@ #include #include +#include #include +using RecurrenceMaskUpdates = std::map; + std::optional checked_add_datetime(Datetime& base, time_t delta); void handleRecurrence(); bool generateDueDates(Task& parent, std::vector& allDue); std::optional getNextRecurrence(Datetime& current, std::string& period); -void updateRecurrenceMask(Task& task); +void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates = nullptr); +void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates); void handleUntil(); #endif diff --git a/src/rules.cpp b/src/rules.cpp index 21c46307d..9fabdc19e 100644 --- a/src/rules.cpp +++ b/src/rules.cpp @@ -85,51 +85,51 @@ static void applyColor(const Color& base, Color& c, bool merge) { } //////////////////////////////////////////////////////////////////////////////// -static void colorizeBlocked(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeBlocked(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_blocked) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeBlocking(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeBlocking(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_blocking) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTagged(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeTagged(const Task& task, const Color& base, Color& c, bool merge) { if (task.getTagCount()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeActive(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeActive(const Task& task, const Color& base, Color& c, bool merge) { // TODO: Not consistent with the implementation of the +ACTIVE virtual tag if (task.has("start") && !task.has("end")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeScheduled(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeScheduled(const Task& task, const Color& base, Color& c, bool merge) { // TODO: Not consistent with the implementation of the +SCHEDULED virtual tag if (task.has("scheduled") && Datetime(task.get_date("scheduled")) <= now) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeUntil(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeUntil(const Task& task, const Color& base, Color& c, bool merge) { if (task.has("until")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTag(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeTag(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { if (task.hasTag(rule.substr(10))) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeProject(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeProject(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Observe the case sensitivity setting. bool sensitive = Context::getContext().config.getBoolean("search.case.sensitive"); - auto project = task.get("project"); + const auto& project = task.get_ref("project"); auto rule_trunc = rule.substr(14); // Match project names leftmost. @@ -139,24 +139,24 @@ static void colorizeProject(Task& task, const std::string& rule, const Color& ba } //////////////////////////////////////////////////////////////////////////////// -static void colorizeProjectNone(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeProjectNone(const Task& task, const Color& base, Color& c, bool merge) { if (!task.has("project")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTagNone(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeTagNone(const Task& task, const Color& base, Color& c, bool merge) { if (task.getTagCount() == 0) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeKeyword(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeKeyword(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Observe the case sensitivity setting. auto sensitive = Context::getContext().config.getBoolean("search.case.sensitive"); // The easiest thing to check is the description, because it is just one // attribute. - if (find(task.get("description"), rule.substr(14), sensitive) != std::string::npos) + if (find(task.get_ref("description"), rule.substr(14), sensitive) != std::string::npos) applyColor(base, c, merge); // Failing the description check, look at all annotations, returning on the @@ -172,7 +172,7 @@ static void colorizeKeyword(Task& task, const std::string& rule, const Color& ba } //////////////////////////////////////////////////////////////////////////////// -static void colorizeUDA(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeUDA(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Is the rule color.uda.name.value or color.uda.name? auto pos = rule.find('.', 10); @@ -181,42 +181,42 @@ static void colorizeUDA(Task& task, const std::string& rule, const Color& base, } else { auto uda = rule.substr(10, pos - 10); auto val = rule.substr(pos + 1); - if ((val == "none" && !task.has(uda)) || task.get(uda) == val) applyColor(base, c, merge); + if ((val == "none" && !task.has(uda)) || task.get_ref(uda) == val) applyColor(base, c, merge); } } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDue(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDue(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_due()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDueToday(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDueToday(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_duetoday()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeOverdue(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeOverdue(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_overdue()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeRecurring(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeRecurring(const Task& task, const Color& base, Color& c, bool merge) { if (task.has("recur")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeCompleted(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeCompleted(const Task& task, const Color& base, Color& c, bool merge) { if (task.getStatus() == Task::completed) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDeleted(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDeleted(const Task& task, const Color& base, Color& c, bool merge) { if (task.getStatus() == Task::deleted) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -void autoColorize(Task& task, Color& c) { +void autoColorize(const Task& task, Color& c) { // The special tag 'nocolor' overrides all auto and specific colorization. if (!Context::getContext().color() || task.hasTag("nocolor")) { c = Color(); diff --git a/src/rules.h b/src/rules.h index d0d8c7f85..cbe73b56f 100644 --- a/src/rules.h +++ b/src/rules.h @@ -36,7 +36,7 @@ #include void initializeColorRules(); -void autoColorize(Task& task, Color& c); +void autoColorize(const Task& task, Color& c); std::string colorizeHeader(const std::string& input); std::string colorizeFootnote(const std::string& input); std::string colorizeError(const std::string& input); diff --git a/src/sort.cpp b/src/sort.cpp index f87be111a..5cfe5c16b 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -41,31 +41,105 @@ #include #include #include +#include #include -static std::vector* global_data = nullptr; -static std::vector global_keys; +static const std::vector* global_data = nullptr; static unsigned int sort_random_seed = 0; static bool sort_compare(int, int); +struct SortKey { + std::string field; + bool ascending; +}; + +static std::vector global_sort_keys; + +// Pre-computed values to avoid repeated parsing. Duration fields are parsed +// once into time_t and stored by field name (recur and duration UDAs). +static std::unordered_map> global_durations; + +// Pre-computed dependency UUIDs to avoid repeated sort calls. +static std::vector> global_sorted_dep_uuids; + +// UDA types to avoid repeated lookups. +static std::unordered_map global_uda_types; +static std::vector global_random_keys; + //////////////////////////////////////////////////////////////////////////////// -void sort_tasks(std::vector& data, std::vector& order, const std::string& keys) { +void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys) { Timer timer; global_data = &data; // Split the key defs. - global_keys = split(keys, ','); - - // Generate a random seend for sorting by "random". - if (sort_random_seed == 0) { - // For testing purposes, allow the seed to be specified in an undocumented configuration - // setting. - std::string seed_str = Context::getContext().config.get("debug.random.seed"); - if (seed_str.empty()) { - std::random_device rd; - sort_random_seed = rd(); - } else { - sort_random_seed = std::stoul(seed_str); + auto key_defs = split(keys, ','); + + // Pre-computing of sorting values. + global_durations.clear(); + global_uda_types.clear(); + global_sorted_dep_uuids.clear(); + global_random_keys.clear(); + global_sort_keys.clear(); + global_sort_keys.reserve(key_defs.size()); + for (auto& k : key_defs) { + std::string field; + bool ascending, breakIndicator; + Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + global_sort_keys.push_back({field, ascending}); + + // Generate a random seend for sorting by "random". + if (field == "random") { + if (sort_random_seed == 0) { + // For testing purposes, allow the seed to be specified in an undocumented configuration + // setting. + std::string seed_str = Context::getContext().config.get("debug.random.seed"); + if (seed_str.empty()) { + std::random_device rd; + sort_random_seed = rd(); + } else { + sort_random_seed = std::stoul(seed_str); + } + } + + auto seed = std::to_string(sort_random_seed); + global_random_keys.resize(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + global_random_keys[i] = + std::to_string(std::hash{}(data[i].get_ref("uuid") + seed)); + } + } + + if (field == "depends") { + global_sorted_dep_uuids.resize(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + auto deps = data[i].getDependencyUUIDs(); + std::sort(deps.begin(), deps.end()); + global_sorted_dep_uuids[i] = std::move(deps); + } + } + + if (field == "recur") { + auto& cache = global_durations[field]; + cache.resize(data.size(), 0); + for (size_t i = 0; i < data.size(); ++i) { + auto s = data[i].get_ref("recur"); + if (!s.empty()) cache[i] = Duration(s).toTime_t(); + } + continue; + } + + auto col_it = Context::getContext().columns.find(field); + if (col_it != Context::getContext().columns.end()) { + auto type = col_it->second->type(); + global_uda_types[field] = type; + if (type == "duration") { + auto& cache = global_durations[field]; + cache.resize(data.size(), 0); + for (size_t i = 0; i < data.size(); ++i) { + auto s = data[i].get_ref(field); + if (!s.empty()) cache[i] = Duration(s).toTime_t(); + } + } } } @@ -114,29 +188,14 @@ void sort_projects(std::list>& sorted, // // Essentially a static implementation of a dynamic operator<. static bool sort_compare(int left, int right) { - std::string field; - bool ascending; - bool breakIndicator; - Column* column; - int left_number; - int right_number; - float left_real; - float right_real; - - for (auto& k : global_keys) { - Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + for (const auto& key : global_sort_keys) { + const auto& field = key.field; + bool ascending = key.ascending; // Random. if (field == "random") { - // For "random" sort, we produce a stable number for each task based on a hash of its - // UUID plus the random seed. - std::string left_uuid = (*global_data)[left].get("uuid"); - std::string right_uuid = (*global_data)[right].get("uuid"); - - std::string left_scrambled = - std::to_string(std::hash{}(left_uuid + std::to_string(sort_random_seed))); - std::string right_scrambled = - std::to_string(std::hash{}(right_uuid + std::to_string(sort_random_seed))); + const auto& left_scrambled = global_random_keys[left]; + const auto& right_scrambled = global_random_keys[right]; if (left_scrambled == right_scrambled) continue; @@ -145,8 +204,8 @@ static bool sort_compare(int left, int right) { // Urgency. else if (field == "urgency") { - left_real = (*global_data)[left].urgency(); - right_real = (*global_data)[right].urgency(); + auto left_real = (*global_data)[left].urgency(); + auto right_real = (*global_data)[right].urgency(); if (left_real == right_real) continue; @@ -155,8 +214,8 @@ static bool sort_compare(int left, int right) { // Number. else if (field == "id") { - left_number = (*global_data)[left].id; - right_number = (*global_data)[right].id; + auto left_number = (*global_data)[left].id; + auto right_number = (*global_data)[right].id; if (left_number == right_number) continue; @@ -166,8 +225,8 @@ static bool sort_compare(int left, int right) { // String. else if (field == "description" || field == "project" || field == "status" || field == "tags" || field == "uuid" || field == "parent" || field == "imask" || field == "mask") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string == right_string) continue; @@ -177,8 +236,8 @@ static bool sort_compare(int left, int right) { // Due Date. else if (field == "due" || field == "end" || field == "entry" || field == "start" || field == "until" || field == "wait" || field == "modified" || field == "scheduled") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string != "" && right_string == "") return true; @@ -191,22 +250,18 @@ static bool sort_compare(int left, int right) { // Depends string. else if (field == "depends") { - // Raw data is an un-sorted list of UUIDs. We just need a stable - // sort, so we sort them lexically. - auto left_deps = (*global_data)[left].getDependencyUUIDs(); - std::sort(left_deps.begin(), left_deps.end()); - auto right_deps = (*global_data)[right].getDependencyUUIDs(); - std::sort(right_deps.begin(), right_deps.end()); + const auto& left_deps = global_sorted_dep_uuids[left]; + const auto& right_deps = global_sorted_dep_uuids[right]; if (left_deps == right_deps) continue; - if (left_deps.size() == 0 && right_deps.size() > 0) return ascending; + if (left_deps.empty() && right_deps.empty()) return ascending; - if (left_deps.size() > 0 && right_deps.size() == 0) return !ascending; + if (!left_deps.empty() && right_deps.empty()) return !ascending; // Sort on the first dependency. - left_number = Context::getContext().tdb2.id(left_deps[0]); - right_number = Context::getContext().tdb2.id(right_deps[0]); + auto left_number = Context::getContext().tdb2.id(left_deps[0]); + auto right_number = Context::getContext().tdb2.id(right_deps[0]); if (left_number == right_number) continue; @@ -215,19 +270,23 @@ static bool sort_compare(int left, int right) { // Duration. else if (field == "recur") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); - - if (left_string == right_string) continue; - - Duration left_duration(left_string); - Duration right_duration(right_string); - return ascending ? (left_duration < right_duration) : (left_duration > right_duration); + auto it = global_durations.find(field); + if (it != global_durations.end()) { + auto left_dur = it->second[left]; + auto right_dur = it->second[right]; + if (left_dur == right_dur) continue; + return ascending ? (left_dur < right_dur) : (left_dur > right_dur); + } } // UDAs. - else if ((column = Context::getContext().columns[field]) != nullptr) { - std::string type = column->type(); + else { + auto type_it = global_uda_types.find(field); + if (type_it == global_uda_types.end()) + throw format("The '{1}' column is not a valid sort field.", field); + + const auto& type = type_it->second; + if (type == "numeric") { auto left_real = strtof(((*global_data)[left].get_ref(field)).c_str(), nullptr); auto right_real = strtof(((*global_data)[right].get_ref(field)).c_str(), nullptr); @@ -236,8 +295,8 @@ static bool sort_compare(int left, int right) { return ascending ? (left_real < right_real) : (left_real > right_real); } else if (type == "string") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string == right_string) continue; @@ -260,8 +319,8 @@ static bool sort_compare(int left, int right) { } else if (type == "date") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string != "" && right_string == "") return true; @@ -271,17 +330,15 @@ static bool sort_compare(int left, int right) { return ascending ? (left_string < right_string) : (left_string > right_string); } else if (type == "duration") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); - - if (left_string == right_string) continue; - - Duration left_duration(left_string); - Duration right_duration(right_string); - return ascending ? (left_duration < right_duration) : (left_duration > right_duration); + auto it = global_durations.find(field); + if (it != global_durations.end()) { + auto left_dur = it->second[left]; + auto right_dur = it->second[right]; + if (left_dur == right_dur) continue; + return ascending ? (left_dur < right_dur) : (left_dur > right_dur); + } } - } else - throw format("The '{1}' column is not a valid sort field.", field); + } } return false; diff --git a/src/sort.h b/src/sort.h index 6972f9c2e..4d842d3e0 100644 --- a/src/sort.h +++ b/src/sort.h @@ -38,7 +38,7 @@ #include #include -void sort_tasks(std::vector& data, std::vector& order, const std::string& keys); +void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys); void sort_projects(std::list>& sorted, std::map& allProjects); diff --git a/test/completed.test.py b/test/completed.test.py index 91f612808..abfbe109f 100755 --- a/test/completed.test.py +++ b/test/completed.test.py @@ -54,6 +54,16 @@ def test_completed_report(self): self.assertNotIn("two", out) self.assertNotIn("three", out) + def test_complete_with_empty_filter(self): + """Verify that you can complete all tasks with an empty filter, + this should be successful with cold pending set cache""" + self.t("add one") + self.t("add two") + + code, out, err = self.t("rc.allow.empty.filter=1 rc.bulk=0 done", input="yes\n") + self.assertIn("Completed 2 tasks", out) + self.assertEqual([], self.t.export("status:pending")) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/debug.test.py b/test/debug.test.py index 1838a51d9..71bd5eb69 100755 --- a/test/debug.test.py +++ b/test/debug.test.py @@ -85,6 +85,19 @@ def test_debug_hooks_output(self): self.assertIn("Filtered 2 tasks --> 2 tasks [pending only]", err) self.assertIn("Perf task", err) + def test_status_filter_shortcut_ignores_other_filters(self): + """Checks that we aren't accidentally matching other filters + with the same keywords""" + self.t("1 modify description:pending") + self.t("1 done") + + for filter_ in ( + "status:completed description:pending", + "status.not:pending description:pending", + ): + code, out, err = self.t(f"{filter_} all") + self.assertIn("pending", out) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/dependencies.test.py b/test/dependencies.test.py index 951129ec3..d1d98da8d 100755 --- a/test/dependencies.test.py +++ b/test/dependencies.test.py @@ -87,6 +87,19 @@ def test_circular_5(self): code, out, err = self.t.runError("1 modify dep:5") self.assertIn("Circular dependency detected and disallowed.", err) + def test_completed_dependency_is_not_rendered(self): + """Completed dependencies should not be listed even if working set is stale""" + self.t("2 modify dep:1") + self.t("1 done rc.gc=off") + + self.assertIn("depends", self.t.export_one("rc.gc=off 2")) + code, out, err = self.t( + "2 list rc.gc=off rc.report.list.columns=id,depends " + "rc.report.list.labels=ID,Depends" + ) + + self.assertIn(["2"], [line.split() for line in out.splitlines()]) + def test_dag(self): """Check acyclic graph support""" self.t("add three") diff --git a/test/hooks.on-modify.test.py b/test/hooks.on-modify.test.py index 9d9a615f2..798047869 100755 --- a/test/hooks.on-modify.test.py +++ b/test/hooks.on-modify.test.py @@ -204,6 +204,18 @@ def test_onmodify_escaped_backslash(self): code, out, err = self.t(f"_get 1.description") self.assertEqual(out.strip(), r"tab\ttab foo") + def test_onmodify_recurring_bulk_completion(self): + hookname = "on-modify-accept" + self.t("add one due:tomorrow recur:daily") + self.t("list rc.recurrence.limit:3 rc.verbose:nothing") + self.t.hooks.add_default(hookname, log=True) + + self.t("2-4 done rc.bulk=0 rc.confirmation=off") + + hook = self.t.hooks[hookname] + hook.assertTriggeredCount(6) + self.assertEqual("+++", self.t.export_one("status:recurring")["mask"]) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/recurrence.test.py b/test/recurrence.test.py index 155eb9d1f..deaa80eba 100755 --- a/test/recurrence.test.py +++ b/test/recurrence.test.py @@ -649,6 +649,23 @@ def test_annual_creep(self): self.assertIn("17 20150101", out) +class TestBulkRecurrenceMask(TestCase): + def setUp(self): + self.t = Task() + self.t("add one due:tomorrow recur:daily") + self.t("list rc.recurrence.limit:3 rc.verbose:nothing") + + def test_done(self): + self.t("2-4 done rc.bulk=0 rc.confirmation=off") + self.assertEqual("+++", self.t.export_one("status:recurring")["mask"]) + + def test_delete(self): + self.t( + "2-4 delete rc.bulk=0 rc.confirmation=off rc.recurrence.confirmation=off" + ) + self.assertEqual("XXX", self.t.export_one("status:recurring")["mask"]) + + # TODO Wait a recurring task # TODO Downgrade a recurring task to a regular task # TODO Duplicate a recurring child task diff --git a/test/tdb2_test.cpp b/test/tdb2_test.cpp index f1baf2444..ff640c0de 100644 --- a/test/tdb2_test.cpp +++ b/test/tdb2_test.cpp @@ -44,7 +44,7 @@ void cleardb() { //////////////////////////////////////////////////////////////////////////////// int TEST_NAME(int, char**) { - UnitTest t(12); + UnitTest t(36); Context context; Context::setContext(&context); @@ -84,7 +84,7 @@ int TEST_NAME(int, char**) { t.is((int)pending.size(), 1, "TDB2 after add, 1 pending task"); t.is((int)completed.size(), 0, "TDB2 after add, 0 completed tasks"); t.is((int)num_reverts_possible, 1, "TDB2 after add, 1 revert possible"); - t.is((int)num_local_changes, 6, "TDB2 after add, 6 local changes"); + t.is((int)num_local_changes, 7, "TDB2 after add, 7 local changes"); task.set("description", "This is a test"); context.tdb2.modify(task); @@ -98,11 +98,106 @@ int TEST_NAME(int, char**) { t.is((int)completed.size(), 0, "TDB2 after set, 0 completed tasks"); t.is((int)num_reverts_possible, 1, "TDB2 after set, 1 revert possible"); - // At this point, there may be 7 or 8 local changes, depending on whether + // At this point, there may be 8 or 9 local changes, depending on whether // the `modified` property changed between the `add` and `modify` // invocation. That only happens if the clock ticks over to the next second // between those invocations. - t.ok(num_local_changes == 7 || num_local_changes == 8, "TDB2 after set, 7 or 8 local changes"); + t.ok(num_local_changes == 8 || num_local_changes == 9, "TDB2 after set, 7 or 8 local changes"); + + // Reset for reuse. + cleardb(); + context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + t.is((int)context.tdb2.pending_tasks().size(), 0, "Fresh invocation uses fresh cache"); + + Task blocker(R"([description:"blocking"])"); + Task first(R"([description:"first"])"); + Task second(R"([description:"second"])"); + + first.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + second.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(second); + + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency is blocking"); + t.ok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 first dependent is blocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent is blocked"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 2, + "TDB2 dependency graph contains two dependents"); + t.is((int)context.tdb2.dependency_graph().dependents.at(blocker.get_ref("uuid")).size(), 2, + "TDB2 dependency map contains both edges"); + + first.removeDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency remains blocking"); + t.notok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 first dependent is unblocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent remains blocked"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 1, + "TDB2 dependency map correctly removed the first dependent"); + t.is((int)context.tdb2.dependency_graph().dependents.at(blocker.get_ref("uuid")).size(), 1, + "TDB2 dependency map contains the second edge"); + + second.removeDependency(blocker.get_ref("uuid")); + context.tdb2.modify(second); + + t.notok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency no longer blocks tasks"); + t.notok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent is unblocked"); + t.is((int)context.tdb2.pending_tasks().size(), 3, + "TDB2 dependency changes keep correct pending number"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 0, + "TDB2 dependency changes invalidated the old graph - dependencies"); + t.is((int)context.tdb2.dependency_graph().dependents.size(), 0, + "TDB2 dependency changes remove dependents"); + + // Reset for reuse. + cleardb(); + + context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + + first.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + second.addDependency(blocker.get_ref("uuid")); + + Task waiting = *context.tdb2.find_pending(first.get_ref("uuid")); + waiting.set("wait", "20380118T000000Z"); + context.tdb2.modify(waiting); + + Task recurring = *context.tdb2.find_pending(second.get_ref("uuid")); + recurring.setStatus(Task::recurring); + context.tdb2.modify(recurring); + + t.ok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 waiting dependent is blocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 recurring dependent is blocked"); + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 active dependents keep the dependency as blocking"); + + waiting.setStatus(Task::completed); + context.tdb2.modify(waiting); + + t.notok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 completed waiting dependent is unblocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 recurring dependent stays blocked"); + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 active dependents keep the dependency as blocking - recurring tasks"); + + recurring.setStatus(Task::deleted); + context.tdb2.modify(recurring); + + t.notok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 deleted recurring dependent gets unblocked"); + t.notok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 inactive dependents cause dependency to clear blocking"); // Reset for reuse. cleardb();