1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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)
}
}
|