60 lines
2.7 KiB
Swift
60 lines
2.7 KiB
Swift
import AppKit
|
|
|
|
// MARK: - Block-quote / Callout Continuation on Enter
|
|
//
|
|
// Pressing Return inside a block quote (including a callout) repeats the quote
|
|
// prefix on the next line — `> ` — so the quote/callout keeps going, the same
|
|
// way list items auto-continue. Pressing Return on an *empty* quote line removes
|
|
// the prefix and breaks out of the quote.
|
|
|
|
extension EditorTextView {
|
|
|
|
/// Leading indent + one or more `>` levels (each with an optional single
|
|
/// space), at the start of a line. Captures the *full* nesting depth so a
|
|
/// nested callout/quote line `> > …` continues as `> > `, not `> `.
|
|
private static let blockquotePrefixRegex = try! NSRegularExpression(
|
|
pattern: #"^[ \t]*(?:>[ \t]?)+"#
|
|
)
|
|
|
|
/// Continues a block quote / callout on Return. Returns true if it handled
|
|
/// the newline.
|
|
func handleBlockquoteNewline(at location: Int) -> Bool {
|
|
let ns = rawSource as NSString
|
|
guard location <= ns.length else { return false }
|
|
|
|
// The line containing the cursor (without its trailing newline).
|
|
let lineRange = ns.lineRange(for: NSRange(location: location, length: 0))
|
|
var lineEnd = lineRange.upperBound
|
|
if lineEnd > lineRange.location, ns.character(at: lineEnd - 1) == 0x0A { lineEnd -= 1 }
|
|
let lineStart = lineRange.location
|
|
let line = ns.substring(with: NSRange(location: lineStart, length: lineEnd - lineStart))
|
|
let lineNS = line as NSString
|
|
|
|
guard let m = Self.blockquotePrefixRegex.firstMatch(
|
|
in: line, range: NSRange(location: 0, length: lineNS.length)) else { return false }
|
|
|
|
let prefix = lineNS.substring(with: m.range)
|
|
let hasContent = m.range.length < lineNS.length
|
|
|
|
if hasContent {
|
|
// Continue the quote/callout at the same depth: newline + same prefix.
|
|
insertText("\n" + prefix, replacementRange: NSRange(location: location, length: 0))
|
|
} else {
|
|
// Empty quote line → step out one nesting level (drop the last `>`).
|
|
// At the top level this empties the line, breaking out of the quote.
|
|
insertText(Self.reduceQuotePrefix(prefix),
|
|
replacementRange: NSRange(location: lineStart, length: lineEnd - lineStart))
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// Drops the deepest `>` level from a quote prefix: `> > ` → `> `,
|
|
/// ` > ` → ` ` (indent kept), `> ` → `` (broken out).
|
|
static func reduceQuotePrefix(_ prefix: String) -> String {
|
|
let ns = prefix as NSString
|
|
let lastGT = ns.range(of: ">", options: .backwards)
|
|
guard lastGT.location != NSNotFound else { return "" }
|
|
return ns.substring(to: lastGT.location)
|
|
}
|
|
}
|