aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgKeyboardToolbar/EditorSnippet.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgKeyboardToolbar/EditorSnippet.swift')
-rw-r--r--Sources/NorgKeyboardToolbar/EditorSnippet.swift90
1 files changed, 90 insertions, 0 deletions
diff --git a/Sources/NorgKeyboardToolbar/EditorSnippet.swift b/Sources/NorgKeyboardToolbar/EditorSnippet.swift
new file mode 100644
index 0000000..b87d2b1
--- /dev/null
+++ b/Sources/NorgKeyboardToolbar/EditorSnippet.swift
@@ -0,0 +1,90 @@
+import Foundation
+
+/// The available editor snippets to show in the toolbar.
+enum EditorSnippet: String, CaseIterable, Identifiable, Sendable {
+ case heading
+ case task
+ case weakReverse
+ case strongReverse
+ case code
+
+ public var id: String { rawValue }
+
+ /// The text inserted, with the cursor offset (in characters).
+ public var template: (text: String, cursor: Int) {
+ switch self {
+ case .heading: return ("* ", 2)
+ case .task: return ("- ( ) ", 6)
+ case .weakReverse: return ("---", 3)
+ case .strongReverse: return ("===", 3)
+ case .code: return ("@code\n\t\n@end", 7)
+ }
+ }
+
+ /// Applies the snippet, returning the updated text and new cursor offset.
+ public func apply(to text: String, cursor: Int) -> (text: String, cursor: Int) {
+ let characters = Array(text)
+ let position = min(max(cursor, 0), characters.count)
+
+ switch self {
+ case .heading where isInBareMarker(characters, at: position, markers: ["*"]):
+ return promoteHeading(characters, at: position)
+ case .task where isInBareMarker(characters, at: position, markers: ["*", "~"]):
+ return addTask(toMarker: characters, at: position)
+ default:
+ return insertOnNewLine(in: characters, cursor: position)
+ }
+ }
+
+ // MARK: - Marker-aware behaviours
+
+ /// 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>)
+ -> Bool {
+ let lineStart = LineScanner.lineStart(in: characters, at: position)
+ let prefix = characters[lineStart..<position]
+ return prefix.contains(where: markers.contains)
+ && prefix.allSatisfy { markers.contains($0) || $0 == " " }
+ }
+
+ /// 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
+ ) {
+ let lineStart = LineScanner.lineStart(in: characters, at: position)
+
+ var insertAt = lineStart
+ while insertAt < characters.count, characters[insertAt] == " " { insertAt += 1 }
+ while insertAt < characters.count, characters[insertAt] == "*" { insertAt += 1 }
+
+ var result = characters
+ result.insert("*", at: insertAt)
+ return (String(result), insertAt <= position ? position + 1 : position)
+ }
+
+ /// Inserts a task marker `( ) ` at the cursor, turning a heading or
+ /// ordered-list item into a task.
+ private func addTask(toMarker characters: [Character], at position: Int) -> (
+ text: String, cursor: Int
+ ) {
+ var result = characters
+ result.insert(contentsOf: "( ) ", at: position)
+ return (String(result), position + 4)
+ }
+
+ /// Inserts the snippet's text on a new line beneath the cursor's line.
+ private func insertOnNewLine(in characters: [Character], cursor position: Int) -> (
+ text: String, cursor: Int
+ ) {
+ // An empty document takes the snippet as-is, with no leading newline.
+ guard !characters.isEmpty else { return template }
+
+ let lineEnd = LineScanner.lineEnd(in: characters, at: position)
+
+ let prefix = String(characters[0..<lineEnd])
+ let suffix = String(characters[lineEnd...])
+ let newText = prefix + "\n" + template.text + suffix
+ return (newText, lineEnd + 1 + template.cursor)
+ }
+}