aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2026-06-16 15:22:08 +0200
committerRuben Beltran del Rio <jj@r.bdr.sh>2026-06-16 15:22:08 +0200
commite5e024a5f4c8709cf2a28dc3bfab79d8037516e1 (patch)
treeb458cc77b4bbd0580ed5248e90e78148b697199c
parent3402f5764f794a60f85a152a13c4eacaa2a719f6 (diff)
Support all bare modifiers1.0.0
-rw-r--r--Sources/NorgKeyboardToolbar/EditorSnippet.swift29
-rw-r--r--Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift38
2 files changed, 66 insertions, 1 deletions
diff --git a/Sources/NorgKeyboardToolbar/EditorSnippet.swift b/Sources/NorgKeyboardToolbar/EditorSnippet.swift
index b87d2b1..4773eac 100644
--- a/Sources/NorgKeyboardToolbar/EditorSnippet.swift
+++ b/Sources/NorgKeyboardToolbar/EditorSnippet.swift
@@ -29,7 +29,7 @@ enum EditorSnippet: String, CaseIterable, Identifiable, Sendable {
switch self {
case .heading where isInBareMarker(characters, at: position, markers: ["*"]):
return promoteHeading(characters, at: position)
- case .task where isInBareMarker(characters, at: position, markers: ["*", "~"]):
+ case .task where isInBareDetachedModifier(characters, at: position):
return addTask(toMarker: characters, at: position)
default:
return insertOnNewLine(in: characters, cursor: position)
@@ -38,6 +38,11 @@ enum EditorSnippet: String, CaseIterable, Identifiable, Sendable {
// MARK: - Marker-aware behaviours
+ /// The detached-modifier markers that can carry a task status, matching
+ /// NorgKit's task scanner. Any of these — headings, un/ordered lists,
+ /// quotes, definitions, footnotes — may be turned into a task in place.
+ private static let detachedModifierMarkers: Set<Character> = ["*", "-", "~", ">", "$", "^", ":"]
+
/// Whether the is at a position with only `markers` and spaces, and contains
/// at least one marker.
private func isInBareMarker(_ characters: [Character], at position: Int, markers: Set<Character>)
@@ -48,6 +53,28 @@ enum EditorSnippet: String, CaseIterable, Identifiable, Sendable {
&& prefix.allSatisfy { markers.contains($0) || $0 == " " }
}
+ /// Whether the cursor sits just past a bare detached modifier: leading
+ /// spaces, a run of a single marker, then at least one space, with nothing
+ /// else before the cursor. The trailing space requirement keeps delimiters
+ /// such as `---` (which carry no whitespace) from being treated as tasks.
+ private func isInBareDetachedModifier(_ characters: [Character], at position: Int) -> Bool {
+ let lineStart = LineScanner.lineStart(in: characters, at: position)
+ var index = lineStart
+ while index < position, characters[index] == " " { index += 1 }
+
+ guard index < position, Self.detachedModifierMarkers.contains(characters[index]) else {
+ return false
+ }
+ let marker = characters[index]
+ while index < position, characters[index] == marker { index += 1 }
+
+ // A space must separate the marker run from the cursor, and the cursor may
+ // only sit within that run of separating spaces (i.e. before any content).
+ guard index < position, characters[index] == " " else { return false }
+ while index < position, characters[index] == " " { index += 1 }
+ return index == position
+ }
+
/// Adds a `*` after the leading run of spaces and stars on the cursor's line.
private func promoteHeading(_ characters: [Character], at position: Int) -> (
text: String, cursor: Int
diff --git a/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift b/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift
index 00a9332..a1ff135 100644
--- a/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift
+++ b/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift
@@ -147,4 +147,42 @@ struct EditorSnippetTests {
let result = EditorSnippet.heading.apply(to: "~ ", cursor: 2)
#expect(result.text == "~ \n* ")
}
+
+ // MARK: - Task button supports every detached modifier
+
+ @Test func taskAddsMarkerToBareUnorderedListItem() {
+ // "- |" -> "- ( ) |"
+ let result = EditorSnippet.task.apply(to: "- ", cursor: 2)
+ #expect(result.text == "- ( ) ")
+ #expect(result.cursor == 6)
+ }
+
+ @Test func taskAddsMarkerToEveryDetachedModifier() {
+ // Quotes, definitions, footnotes and the like all carry tasks per the
+ // Norg spec, matching NorgKit's task scanner.
+ for marker in [">", "$", "^", ":"] {
+ let result = EditorSnippet.task.apply(to: "\(marker) ", cursor: 2)
+ #expect(result.text == "\(marker) ( ) ")
+ #expect(result.cursor == 6)
+ }
+ }
+
+ @Test func taskAddsMarkerToNestedDetachedModifierPreservingContent() {
+ // ">> |Quote" -> ">> ( ) |Quote"
+ let result = EditorSnippet.task.apply(to: ">> Quote", cursor: 3)
+ #expect(result.text == ">> ( ) Quote")
+ #expect(result.cursor == 7)
+ }
+
+ @Test func taskIgnoresWeakDelimiterMadeOfDashes() {
+ // "---|" is a delimiter, not a list item — start a new task line instead.
+ let result = EditorSnippet.task.apply(to: "---", cursor: 3)
+ #expect(result.text == "---\n- ( ) ")
+ }
+
+ @Test func taskInsertsNewLineWhenMarkerHasNoSeparatingSpace() {
+ // A bare marker with no trailing space isn't a detached modifier yet.
+ let result = EditorSnippet.task.apply(to: ">", cursor: 1)
+ #expect(result.text == ">\n- ( ) ")
+ }
}