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)
|
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 palette = Theme.current
|
||||||
let italicFont = NSFontManager.shared.convert(baseFont, toHaveTrait: .italicFontMask)
|
let italicFont = NSFontManager.shared.convert(baseFont, toHaveTrait: .italicFontMask)
|
||||||
let nsText = textStorage.string as NSString
|
let textLen = (textStorage.string as NSString).length
|
||||||
let textLen = nsText.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 {
|
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
|
// Convert byte offsets to character offsets within sourceStr
|
||||||
let byteEnd = span.end
|
let prefix = sourceStr.utf8.prefix(span.start)
|
||||||
let str = textStorage.string
|
let charStart = sourceStr.distance(from: sourceStr.startIndex, to: sourceStr.utf8.index(sourceStr.utf8.startIndex, offsetBy: prefix.count))
|
||||||
let bytes = str.utf8
|
let endPrefix = sourceStr.utf8.prefix(span.end)
|
||||||
let startIdx = bytes.index(bytes.startIndex, offsetBy: byteStart, limitedBy: bytes.endIndex) ?? bytes.endIndex
|
let charEnd = sourceStr.distance(from: sourceStr.startIndex, to: sourceStr.utf8.index(sourceStr.utf8.startIndex, offsetBy: endPrefix.count))
|
||||||
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)
|
|
||||||
|
|
||||||
guard charStart < textLen && charEnd <= textLen && charStart < charEnd else { continue }
|
let absStart = offset + charStart
|
||||||
let range = NSRange(location: charStart, length: charEnd - 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
|
let color: NSColor
|
||||||
var font: NSFont? = nil
|
var font: NSFont? = nil
|
||||||
|
|
||||||
// kind indices match HIGHLIGHT_NAMES in highlight.rs
|
|
||||||
switch span.kind {
|
switch span.kind {
|
||||||
case 0: color = syn.keyword // keyword
|
case 0: color = syn.keyword
|
||||||
case 1: color = syn.function // function
|
case 1: color = syn.function
|
||||||
case 2: color = syn.function // function.builtin
|
case 2: color = syn.function
|
||||||
case 3: color = syn.type // type
|
case 3: color = syn.type
|
||||||
case 4: color = syn.type // type.builtin
|
case 4: color = syn.type
|
||||||
case 5: color = syn.type // constructor
|
case 5: color = syn.type
|
||||||
case 6: color = palette.peach // constant
|
case 6: color = palette.peach
|
||||||
case 7: color = palette.peach // constant.builtin
|
case 7: color = palette.peach
|
||||||
case 8: color = syn.string // string
|
case 8: color = syn.string
|
||||||
case 9: color = syn.number // number
|
case 9: color = syn.number
|
||||||
case 10: color = syn.comment; font = italicFont // comment
|
case 10: color = syn.comment; font = italicFont
|
||||||
case 11: color = palette.text // variable
|
case 11: color = palette.text
|
||||||
case 12: color = palette.red // variable.builtin
|
case 12: color = palette.red
|
||||||
case 13: color = palette.maroon // variable.parameter
|
case 13: color = palette.maroon
|
||||||
case 14: color = syn.operator // operator
|
case 14: color = syn.operator
|
||||||
case 15: color = palette.overlay2 // punctuation
|
case 15: color = palette.overlay2
|
||||||
case 16: color = palette.overlay2 // punctuation.bracket
|
case 16: color = palette.overlay2
|
||||||
case 17: color = palette.overlay2 // punctuation.delimiter
|
case 17: color = palette.overlay2
|
||||||
case 18: color = palette.teal // property
|
case 18: color = palette.teal
|
||||||
case 19: color = palette.red // tag (HTML)
|
case 19: color = palette.red
|
||||||
case 20: color = palette.yellow // attribute
|
case 20: color = palette.yellow
|
||||||
case 21: color = palette.sapphire // label
|
case 21: color = palette.sapphire
|
||||||
case 22: color = palette.pink // escape
|
case 22: color = palette.pink
|
||||||
case 23: color = palette.teal // embedded
|
case 23: color = palette.teal
|
||||||
default: continue
|
default: continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2004,9 +2011,12 @@ private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: Catp
|
||||||
let text = textStorage.string
|
let text = textStorage.string
|
||||||
let nsText = text as NSString
|
let nsText = text as NSString
|
||||||
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
||||||
|
let syn = Theme.syntax
|
||||||
var fencedRanges: [NSRange] = []
|
var fencedRanges: [NSRange] = []
|
||||||
var lineStart = 0
|
var lineStart = 0
|
||||||
var openFence: Int? = nil
|
var openFence: Int? = nil
|
||||||
|
var fenceLang: String? = nil
|
||||||
|
var codeStart: Int = 0
|
||||||
|
|
||||||
while lineStart < nsText.length {
|
while lineStart < nsText.length {
|
||||||
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
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 openFence == nil {
|
||||||
if trimmed.hasPrefix("```") {
|
if trimmed.hasPrefix("```") {
|
||||||
openFence = lineRange.location
|
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(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
textStorage.addAttribute(.font, value: monoFont, 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 {
|
} else {
|
||||||
if trimmed == "```" {
|
if trimmed == "```" {
|
||||||
// Close fence
|
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
let blockRange = NSRange(location: openFence!, length: NSMaxRange(lineRange) - openFence!)
|
let blockRange = NSRange(location: openFence!, length: NSMaxRange(lineRange) - openFence!)
|
||||||
fencedRanges.append(blockRange)
|
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
|
openFence = nil
|
||||||
|
fenceLang = nil
|
||||||
} else {
|
} else {
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue