tree-sitter highlighting in fenced code blocks within markdown
This commit is contained in:
parent
5342ddbe5f
commit
0466d7ca56
|
|
@ -1641,56 +1641,63 @@ private func applyCodeFileHighlighting(to textStorage: NSTextStorage, syn: Theme
|
|||
highlightBlockComments(textStorage: textStorage, syn: syn, baseFont: baseFont)
|
||||
}
|
||||
|
||||
private func applyTreeSitterHighlighting(spans: [RustBridge.HighlightSpan], textStorage: NSTextStorage, syn: Theme.SyntaxColors, baseFont: NSFont) {
|
||||
private func applyTreeSitterHighlighting(spans: [RustBridge.HighlightSpan], textStorage: NSTextStorage, syn: Theme.SyntaxColors, baseFont: NSFont, offset: Int = 0) {
|
||||
let palette = Theme.current
|
||||
let italicFont = NSFontManager.shared.convert(baseFont, toHaveTrait: .italicFontMask)
|
||||
let nsText = textStorage.string as NSString
|
||||
let textLen = nsText.length
|
||||
let textLen = (textStorage.string as NSString).length
|
||||
|
||||
// When offset > 0, spans are byte-relative to a substring starting at `offset` chars.
|
||||
// Extract that substring and convert bytes to chars within it.
|
||||
let sourceStr: String
|
||||
if offset > 0 {
|
||||
sourceStr = (textStorage.string as NSString).substring(from: offset)
|
||||
} else {
|
||||
sourceStr = textStorage.string
|
||||
}
|
||||
let sourceBytes = Array(sourceStr.utf8)
|
||||
|
||||
for span in spans {
|
||||
guard span.start < textLen && span.end <= textLen && span.start < span.end else { continue }
|
||||
guard span.start < sourceBytes.count && span.end <= sourceBytes.count && span.start < span.end else { continue }
|
||||
|
||||
let byteStart = span.start
|
||||
let byteEnd = span.end
|
||||
let str = textStorage.string
|
||||
let bytes = str.utf8
|
||||
let startIdx = bytes.index(bytes.startIndex, offsetBy: byteStart, limitedBy: bytes.endIndex) ?? bytes.endIndex
|
||||
let endIdx = bytes.index(bytes.startIndex, offsetBy: byteEnd, limitedBy: bytes.endIndex) ?? bytes.endIndex
|
||||
let charStart = str.distance(from: str.startIndex, to: String.Index(startIdx, within: str) ?? str.endIndex)
|
||||
let charEnd = str.distance(from: str.startIndex, to: String.Index(endIdx, within: str) ?? str.endIndex)
|
||||
// Convert byte offsets to character offsets within sourceStr
|
||||
let prefix = sourceStr.utf8.prefix(span.start)
|
||||
let charStart = sourceStr.distance(from: sourceStr.startIndex, to: sourceStr.utf8.index(sourceStr.utf8.startIndex, offsetBy: prefix.count))
|
||||
let endPrefix = sourceStr.utf8.prefix(span.end)
|
||||
let charEnd = sourceStr.distance(from: sourceStr.startIndex, to: sourceStr.utf8.index(sourceStr.utf8.startIndex, offsetBy: endPrefix.count))
|
||||
|
||||
guard charStart < textLen && charEnd <= textLen && charStart < charEnd else { continue }
|
||||
let range = NSRange(location: charStart, length: charEnd - charStart)
|
||||
let absStart = offset + charStart
|
||||
let absEnd = offset + charEnd
|
||||
guard absStart < textLen && absEnd <= textLen && absStart < absEnd else { continue }
|
||||
let range = NSRange(location: absStart, length: absEnd - absStart)
|
||||
|
||||
let color: NSColor
|
||||
var font: NSFont? = nil
|
||||
|
||||
// kind indices match HIGHLIGHT_NAMES in highlight.rs
|
||||
switch span.kind {
|
||||
case 0: color = syn.keyword // keyword
|
||||
case 1: color = syn.function // function
|
||||
case 2: color = syn.function // function.builtin
|
||||
case 3: color = syn.type // type
|
||||
case 4: color = syn.type // type.builtin
|
||||
case 5: color = syn.type // constructor
|
||||
case 6: color = palette.peach // constant
|
||||
case 7: color = palette.peach // constant.builtin
|
||||
case 8: color = syn.string // string
|
||||
case 9: color = syn.number // number
|
||||
case 10: color = syn.comment; font = italicFont // comment
|
||||
case 11: color = palette.text // variable
|
||||
case 12: color = palette.red // variable.builtin
|
||||
case 13: color = palette.maroon // variable.parameter
|
||||
case 14: color = syn.operator // operator
|
||||
case 15: color = palette.overlay2 // punctuation
|
||||
case 16: color = palette.overlay2 // punctuation.bracket
|
||||
case 17: color = palette.overlay2 // punctuation.delimiter
|
||||
case 18: color = palette.teal // property
|
||||
case 19: color = palette.red // tag (HTML)
|
||||
case 20: color = palette.yellow // attribute
|
||||
case 21: color = palette.sapphire // label
|
||||
case 22: color = palette.pink // escape
|
||||
case 23: color = palette.teal // embedded
|
||||
case 0: color = syn.keyword
|
||||
case 1: color = syn.function
|
||||
case 2: color = syn.function
|
||||
case 3: color = syn.type
|
||||
case 4: color = syn.type
|
||||
case 5: color = syn.type
|
||||
case 6: color = palette.peach
|
||||
case 7: color = palette.peach
|
||||
case 8: color = syn.string
|
||||
case 9: color = syn.number
|
||||
case 10: color = syn.comment; font = italicFont
|
||||
case 11: color = palette.text
|
||||
case 12: color = palette.red
|
||||
case 13: color = palette.maroon
|
||||
case 14: color = syn.operator
|
||||
case 15: color = palette.overlay2
|
||||
case 16: color = palette.overlay2
|
||||
case 17: color = palette.overlay2
|
||||
case 18: color = palette.teal
|
||||
case 19: color = palette.red
|
||||
case 20: color = palette.yellow
|
||||
case 21: color = palette.sapphire
|
||||
case 22: color = palette.pink
|
||||
case 23: color = palette.teal
|
||||
default: continue
|
||||
}
|
||||
|
||||
|
|
@ -2004,9 +2011,12 @@ private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: Catp
|
|||
let text = textStorage.string
|
||||
let nsText = text as NSString
|
||||
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
||||
let syn = Theme.syntax
|
||||
var fencedRanges: [NSRange] = []
|
||||
var lineStart = 0
|
||||
var openFence: Int? = nil
|
||||
var fenceLang: String? = nil
|
||||
var codeStart: Int = 0
|
||||
|
||||
while lineStart < nsText.length {
|
||||
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
||||
|
|
@ -2016,29 +2026,35 @@ private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: Catp
|
|||
if openFence == nil {
|
||||
if trimmed.hasPrefix("```") {
|
||||
openFence = lineRange.location
|
||||
// Mute the fence line
|
||||
codeStart = NSMaxRange(lineRange)
|
||||
let langId = String(trimmed.dropFirst(3)).trimmingCharacters(in: .whitespaces)
|
||||
fenceLang = langId.isEmpty ? nil : langId.lowercased()
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||
// Language identifier after ```
|
||||
if trimmed.count > 3 {
|
||||
let langStart = (nsText as NSString).range(of: "```", range: lineRange)
|
||||
if langStart.location != NSNotFound {
|
||||
let after = langStart.location + langStart.length
|
||||
let langRange = NSRange(location: after, length: NSMaxRange(lineRange) - after)
|
||||
if langRange.length > 0 {
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: langRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if trimmed == "```" {
|
||||
// Close fence
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||
let blockRange = NSRange(location: openFence!, length: NSMaxRange(lineRange) - openFence!)
|
||||
fencedRanges.append(blockRange)
|
||||
|
||||
// Apply tree-sitter highlighting to the code content
|
||||
let codeEnd = lineRange.location
|
||||
if let lang = fenceLang, codeEnd > codeStart {
|
||||
let codeRange = NSRange(location: codeStart, length: codeEnd - codeStart)
|
||||
let code = nsText.substring(with: codeRange)
|
||||
textStorage.addAttribute(.font, value: monoFont, range: codeRange)
|
||||
let spans = RustBridge.shared.highlight(source: code, lang: lang)
|
||||
if !spans.isEmpty {
|
||||
applyTreeSitterHighlighting(spans: spans, textStorage: textStorage, syn: syn, baseFont: monoFont, offset: codeStart)
|
||||
} else {
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: codeRange)
|
||||
}
|
||||
}
|
||||
|
||||
openFence = nil
|
||||
fenceLang = nil
|
||||
} else {
|
||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
||||
|
|
|
|||
Loading…
Reference in New Issue