krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 7f4beb62cf

7f4beb62cf5772665d1f2c5df0b279208e68032d

parent: 570fb5fc74

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 03:26 UTC

Agenda filter description keeps every term and its sign

Reopening / on -/call/ pre-filled /call/, and only the first effort
and regexp terms were shown. Categories with - are quoted and read
back.

Layout: unified · split

Sources/OrgCore/Agenda/AgendaFilter.swift +13 −4
@@ -13,10 +13,19 @@ public struct AgendaFilter: Sendable, Equatable {
13 13
14 public var isEmpty: Bool { categories.isEmpty && tags.isEmpty && efforts.isEmpty && regexps.isEmpty } 14 public var isEmpty: Bool { categories.isEmpty && tags.isEmpty && efforts.isEmpty && regexps.isEmpty }
15 15
16 /// The filter as `/` shows it for editing. 16 /// The filter as `/` shows it for editing; `parse` reads it back to the same filter.
17 /// Categories holding `-` are quoted.
17 public var description: String { 18 public var description: String {
18 let regexp = regexps.first.map { "/" + $0.dropFirst().replacingOccurrences(of: "^\\+", with: "", options: .regularExpression) + "/" } ?? "" 19 func unsigned(_ term: String) -> (sign: String, body: String) {
19 return categories.joined() + tags.joined() + (efforts.first.map { String($0.dropFirst()) } ?? "") + regexp 20 (term.hasPrefix("-") ? "-" : "", String(term.dropFirst()))
21 }
22 let cats = categories.map { term -> String in
23 let (sign, body) = unsigned(term)
24 return (sign.isEmpty ? "+" : sign) + (body.contains("-") ? "\"\(body)\"" : body)
25 }
26 let efforts = efforts.map { unsigned($0).sign + unsigned($0).body }
27 let regexps = regexps.map { unsigned($0).sign + "/" + unsigned($0).body + "/" }
28 return (cats + tags + efforts + regexps).joined()
20 } 29 }
21 30
22 /// Reads `+cat-tag<0:10-/regexp/`: a word is a tag if a shown line has it, else a category 31 /// Reads `+cat-tag<0:10-/regexp/`: a word is a tag if a shown line has it, else a category
@@ -43,7 +52,7 @@ public struct AgendaFilter: Sendable, Equatable {
43 let word = ns.substring(with: m.range(at: 3)).replacingOccurrences(of: "~~~", with: "-") 52 let word = ns.substring(with: m.range(at: 3)).replacingOccurrences(of: "~~~", with: "-")
44 if shownTags.contains(word) { 53 if shownTags.contains(word) {
45 append(sign + word, to: &result.tags) 54 append(sign + word, to: &result.tags)
46 } else if shownCategories.contains(word) { 55 } else if shownCategories.contains(word.replacingOccurrences(of: "^\"(.*)\"$", with: "$1", options: .regularExpression)) {
47 append(sign + word.replacingOccurrences(of: "^\"(.*)\"$", with: "$1", options: .regularExpression), to: &result.categories) 56 append(sign + word.replacingOccurrences(of: "^\"(.*)\"$", with: "$1", options: .regularExpression), to: &result.categories)
48 } else { 57 } else {
49 ignored.append(sign + word) 58 ignored.append(sign + word)
Tests/OrgCoreTests/AgendaTests.swift +10
@@ -536,4 +536,14 @@ struct AgendaFilterTests {
536 let kept = filter.parse("++urgent", tags: ["urgent"], categories: []).filter 536 let kept = filter.parse("++urgent", tags: ["urgent"], categories: []).filter
537 #expect(kept.tags == ["-phone", "+urgent"]) 537 #expect(kept.tags == ["-phone", "+urgent"])
538 } 538 }
539
540 @Test func descriptionRoundTrips() {
541 let tags: Set<String> = ["phone", "urgent"], categories: Set<String> = ["work", "my-cat"]
542 for text in ["-/call/", "-work+phone-<0:10>1:00/a/-/b/", "\"my-cat\"-urgent", "-\"my-cat\""] {
543 let filter = AgendaFilter().parse(text, tags: tags, categories: categories).filter
544 #expect(AgendaFilter().parse(filter.description, tags: tags, categories: categories).filter == filter, "\(text)")
545 }
546 let negative = AgendaFilter().parse("-/call/", tags: [], categories: []).filter
547 #expect(negative.description == "-/call/")
548 }
539} 549}