krz/orgstar

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

Commit 1d6a2cda53

1d6a2cda5366195c467051dc38b26571d54f4b9a

parent: eba8a09f88

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-09 06:50 UTC

Match Emacs word syntax for \<, \>, \b, \B, \w and \W in agenda and sparse-tree regexps

Layout: unified · split

Sources/OrgCore/Agenda/AgendaSearch.swift +9 −3
@@ -480,6 +480,9 @@ public struct TagsMatcher: Sendable {
480/// Emacs regular expressions run through ICU, case-insensitively as the matcher binds 480/// Emacs regular expressions run through ICU, case-insensitively as the matcher binds
481/// `case-fold-search`. 481/// `case-fold-search`.
482enum EmacsRegex { 482enum EmacsRegex {
483 /// Word syntax in an Org buffer: letters, digits, marks, `$` and `%`, but not `_`, `-` or `'`.
484 static let word = "[\\p{L}\\p{N}\\p{M}$%\\u00b7]"
485
483 static func translate(_ pattern: String) -> String { 486 static func translate(_ pattern: String) -> String {
484 var out = "" 487 var out = ""
485 var chars = Array(pattern) 488 var chars = Array(pattern)
@@ -522,11 +525,14 @@ enum EmacsRegex {
522 out += n == "s" ? "[\\t\\n\\f\\r \\u00a0]" : "[^\\t\\n\\f\\r \\u00a0]" 525 out += n == "s" ? "[\\t\\n\\f\\r \\u00a0]" : "[^\\t\\n\\f\\r \\u00a0]"
523 i += 3 526 i += 3
524 continue 527 continue
525 case "<", ">": out += "\\b" 528 case "<": out += "(?<!\(word))(?=\(word))"
529 case ">": out += "(?<=\(word))(?!\(word))"
530 case "b": out += "(?:(?<!\(word))(?=\(word))|(?<=\(word))(?!\(word)))"
531 case "B": out += "(?:(?<=\(word))(?=\(word))|(?<!\(word))(?!\(word)))"
526 case "`": out += "^" 532 case "`": out += "^"
527 case "'": out += "$" 533 case "'": out += "$"
528 case "w": out += "[\\p{L}\\p{N}]" 534 case "w": out += word
529 case "W": out += "[^\\p{L}\\p{N}]" 535 case "W": out += "[^" + word.dropFirst()
530 default: out += "\\" + String(n) 536 default: out += "\\" + String(n)
531 } 537 }
532 i += 2 538 i += 2
Tests/OrgCoreTests/AgendaSearchTests.swift +4
@@ -28,6 +28,7 @@ struct AgendaSearchTests {
28 * Two words 28 * Two words
29 The phrase two words 29 The phrase two words
30 spans here; foo_bar and foo-bar. 30 spans here; foo_bar and foo-bar.
31 Costs $5 or 50% (don't) for the café.
31 * The last 32 * The last
32 word ends this entry with last 33 word ends this entry with last
33 * word starts the next heading 34 * word starts the next heading
@@ -54,6 +55,9 @@ struct AgendaSearchTests {
54 "+alpha", "+alpha -beta", "+alpha +gamma", "-alpha", "+beta -nested", "{alph?a}", "+{^Gam}", "-{b[eo]}", 55 "+alpha", "+alpha -beta", "+alpha +gamma", "-alpha", "+beta -nested", "{alph?a}", "+{^Gam}", "-{b[eo]}",
55 "+{\\<rent\\>}", "+\"two words\"", "+\"two words\"", "+\"the line\"", "{boiler\\b}", "+alpha +\"in the body\"", 56 "+{\\<rent\\>}", "+\"two words\"", "+\"two words\"", "+\"the line\"", "{boiler\\b}", "+alpha +\"in the body\"",
56 "+{foo\\s-bar}", "+boiler -plate", "+{a b}", "+{alpha} -{x y}", 57 "+{foo\\s-bar}", "+boiler -plate", "+{a b}", "+{alpha} -{x y}",
58 // Emacs word syntax in Org buffers: `_`, `-` and `'` are not word characters; `$` and `%` are.
59 "+{\\<bar\\>}", "+{foo\\b}", "+{o\\B_}", "+{\\<5\\>}", "+{\\b50\\b}", "+{don\\>}", "+{\\w+_\\w+}", "+{\\W5}",
60 "+{caf\\w\\>}", "+{\\<\\$5}",
57 // Headlines only. 61 // Headlines only.
58 "*rent", "*alpha", "*+alpha -beta", "*+section", "*-alpha", "*pay rent", "*with alpha and", 62 "*rent", "*alpha", "*+alpha -beta", "*+section", "*-alpha", "*pay rent", "*with alpha and",
59 // TODO only and whole words. 63 // TODO only and whole words.