feat(ui): absorb button in peek, preserve newlines, demote promoted items
Stream peek now shows absorb button for unpromoted entries. Promoted items in stream show demote instead of delete. d double-tap demotes any card_type entity regardless of view. Parsers preserve newlines from Shift+Enter. Absorb popup truncates to first non-empty line.
This commit is contained in:
+42
-44
@@ -212,60 +212,58 @@ func tryPrefixTime(r *Result, text string) (string, bool) {
|
||||
// extractModifiers extracts tags, flags, time anchors, and card suffixes from text.
|
||||
// handleFlags controls whether !pin is extracted (true for body, false for title/desc in some contexts).
|
||||
func extractModifiers(r *Result, text string, handleFlags bool) (string, error) {
|
||||
tokens := strings.Fields(text)
|
||||
var parts []string
|
||||
seen := map[string]bool{}
|
||||
for _, t := range r.Tags {
|
||||
seen[strings.ToLower(t)] = true
|
||||
}
|
||||
|
||||
for _, tok := range tokens {
|
||||
switch {
|
||||
// !pin flag
|
||||
case handleFlags && strings.EqualFold(tok, "!pin"):
|
||||
r.Pin = true
|
||||
var outLines []string
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
tokens := strings.Fields(line)
|
||||
var lineParts []string
|
||||
for _, tok := range tokens {
|
||||
switch {
|
||||
case handleFlags && strings.EqualFold(tok, "!pin"):
|
||||
r.Pin = true
|
||||
|
||||
// ##word escape → literal #word in body
|
||||
case strings.HasPrefix(tok, "##") && len(tok) > 2:
|
||||
parts = append(parts, "#"+tok[2:])
|
||||
case strings.HasPrefix(tok, "##") && len(tok) > 2:
|
||||
lineParts = append(lineParts, "#"+tok[2:])
|
||||
|
||||
// @time inline time anchor (for todos: sets due_at)
|
||||
// If not valid time format, keep as body text
|
||||
case strings.HasPrefix(tok, "@") && len(tok) > 1:
|
||||
timeStr := tok[1:]
|
||||
if validateTime(timeStr) != nil {
|
||||
parts = append(parts, tok)
|
||||
} else if r.TimeAnchor != nil {
|
||||
return "", fmt.Errorf("multiple time anchors")
|
||||
} else {
|
||||
r.TimeAnchor = &timeStr
|
||||
case strings.HasPrefix(tok, "@") && len(tok) > 1:
|
||||
timeStr := tok[1:]
|
||||
if validateTime(timeStr) != nil {
|
||||
lineParts = append(lineParts, tok)
|
||||
} else if r.TimeAnchor != nil {
|
||||
return "", fmt.Errorf("multiple time anchors")
|
||||
} else {
|
||||
r.TimeAnchor = &timeStr
|
||||
}
|
||||
|
||||
case strings.HasPrefix(tok, "#") && len(tok) > 1:
|
||||
tag := strings.ToLower(tok[1:])
|
||||
if !seen[tag] {
|
||||
r.Tags = append(r.Tags, tag)
|
||||
seen[tag] = true
|
||||
}
|
||||
|
||||
case strings.HasPrefix(tok, "^") && len(tok) > 1:
|
||||
suffix := tok[1:]
|
||||
cardType, ok := validCardTypes[suffix]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid card type %q", suffix)
|
||||
}
|
||||
if r.CardSuffix != nil {
|
||||
return "", fmt.Errorf("multiple card suffixes")
|
||||
}
|
||||
r.CardSuffix = &cardType
|
||||
|
||||
default:
|
||||
lineParts = append(lineParts, tok)
|
||||
}
|
||||
|
||||
// #tag extraction (lowercased, deduplicated)
|
||||
case strings.HasPrefix(tok, "#") && len(tok) > 1:
|
||||
tag := strings.ToLower(tok[1:])
|
||||
if !seen[tag] {
|
||||
r.Tags = append(r.Tags, tag)
|
||||
seen[tag] = true
|
||||
}
|
||||
|
||||
// ^card suffix (kept for now, spec says remove later)
|
||||
case strings.HasPrefix(tok, "^") && len(tok) > 1:
|
||||
suffix := tok[1:]
|
||||
cardType, ok := validCardTypes[suffix]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid card type %q", suffix)
|
||||
}
|
||||
if r.CardSuffix != nil {
|
||||
return "", fmt.Errorf("multiple card suffixes")
|
||||
}
|
||||
r.CardSuffix = &cardType
|
||||
|
||||
default:
|
||||
parts = append(parts, tok)
|
||||
}
|
||||
outLines = append(outLines, strings.Join(lineParts, " "))
|
||||
}
|
||||
return strings.Join(parts, " "), nil
|
||||
return strings.Join(outLines, "\n"), nil
|
||||
}
|
||||
|
||||
func validateTime(s string) error {
|
||||
|
||||
@@ -94,6 +94,11 @@ func TestParse(t *testing.T) {
|
||||
{"inline desc", "body text // inline desc", "body text", "note", nil, sp("inline desc"), nil, nil, nil, false, false, nil, ""},
|
||||
{"url no split", "http://example.com // should not split", "http://example.com // should not split", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
|
||||
// Multiline body preserves newlines
|
||||
{"multiline body", "hello\nworld", "hello\nworld", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"multiline with tags", "line one #ops\nline two", "line one\nline two", "note", nil, nil, nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"title multiline body", "|my title\nfirst line\nsecond line", "first line\nsecond line", "note", sp("my title"), nil, nil, nil, nil, false, false, nil, ""},
|
||||
|
||||
// Edge cases
|
||||
{"empty input", "", "", "", nil, nil, nil, nil, nil, false, false, nil, "empty"},
|
||||
{"only glyph", "-", "", "", nil, nil, nil, nil, nil, false, false, nil, "empty body"},
|
||||
|
||||
Reference in New Issue
Block a user