list: a named task list is not a grocery item (V-520)

"добавь в список" was a marker in two places: task_phrases.json for task
capture, and listCapturePrefixes for the grocery list. ListGrammars is wired
before TaskCaptureGrammar in buildRouter, so the list claimed every one of
them, and takeListTag does not know "дел" as a list name — "добавь в список
дел хлеб" filed a grocery item called "дел хлеб".

The bare marker stays a grocery item, because an unnamed list already defaults
to покупки and the task side always names its list. A named task list now
declines in ParseListCapture, ParseListQuery and ParseListRemove, so the turn
falls through to task capture. The bare forms are gone from task_phrases.json,
so the data says what the code does rather than being shadowed by grammar
order.

Reversible if he asks for the other default: move the two bare phrases back and
the list will need to decline them instead.
This commit is contained in:
2026-08-05 11:44:07 +04:00
parent c586346a60
commit 7b4fb6229a
3 changed files with 95 additions and 3 deletions
+50
View File
@@ -34,6 +34,46 @@ var listTags = []struct{ word, list string }{
{"хозяйство", "хозяйство"},
}
// taskListTags — the list names that belong to task capture, not here. One
// dictionary form each, read the same way listTags are (Vikunja #520).
//
// "добавь в список" is a marker on both sides: task capture has it in
// task_phrases.json and listCapturePrefixes has it below. ListGrammars is wired
// before TaskCaptureGrammar, so the list claimed every one of them, and
// takeListTag does not recognise "дел" as a list name — so "добавь в список дел
// хлеб" filed a grocery item called "дел хлеб". The bare marker stays a grocery
// item, because an unnamed list already defaults to покупки and the task side
// always names its list. A named task list refuses here and falls through.
var taskListTags = []string{"дело", "задача", "task", "todo", "todos"}
// namesTaskList reports whether the remainder after a list marker names a task
// list rather than one of the standing lists.
func namesTaskList(rest string) bool {
fields := strings.Fields(rest)
// The list noun and the prepositions around it are skipped, so the three
// callers can ask this before their own trimming: the remove path leaves
// "из списка дел хлеб" and the capture path leaves "список дел хлеб".
for len(fields) > 0 {
head := strings.ToLower(strings.Trim(fields[0], listTrimCut))
if morph.SameWord(head, "список") || head == "list" || head == "of" || head == "the" ||
head == "из" || head == "со" || head == "в" {
fields = fields[1:]
continue
}
break
}
if len(fields) == 0 {
return false
}
head := strings.ToLower(strings.Trim(fields[0], listTrimCut))
for _, w := range taskListTags {
if morph.SameWord(head, w) || head == w {
return true
}
}
return false
}
var listTagsEN = []struct{ word, list string }{
{"shopping", "покупки"},
{"groceries", "покупки"},
@@ -121,6 +161,10 @@ func ParseListCapture(text string) (ListCapture, bool) {
if !ok {
return ListCapture{}, false
}
// A named task list is task capture's, not the grocery list's (#520).
if namesTaskList(rest) {
return ListCapture{}, false
}
list, rest := takeListTag(rest)
rest = strings.Trim(rest, listTrimCut)
if rest == "" {
@@ -135,6 +179,9 @@ func ParseListQuery(text string) (string, bool) {
if !ok {
return "", false
}
if namesTaskList(rest) {
return "", false
}
list, _ := takeListTag(rest)
return list, true
}
@@ -162,6 +209,9 @@ func ParseListRemove(text string) (ListCapture, bool) {
if !ok {
return ListCapture{}, false
}
if namesTaskList(rest) {
return ListCapture{}, false
}
list, rest := takeListTag(rest)
rest = strings.Trim(rest, listTrimCut)
for _, lead := range []string{"из списка ", "со списка ", "из ", "from the list "} {