Inline single-backtick spans and multi-line fenced ```blocks``` now render with monospace styling as you type, plus real per-language syntax highlighting for fenced blocks (reusing the existing SyntaxHighlighter utility). Only complete, closed spans/fences light up — an unterminated backtick or fence is left as plain text until closed. Pure regex/range logic extracted into testable static functions (inlineCodeRanges, fencedCodeBlocks, fencedCodeBlockRanges) rather than living inline in the NSTextView coordinator.
175 lines
6.4 KiB
Swift
175 lines
6.4 KiB
Swift
//
|
|
// NativeTextEditorPureLogicTests.swift
|
|
// oAITests
|
|
//
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
// Copyright (C) 2026 Rune Olsen
|
|
|
|
import Testing
|
|
import Foundation
|
|
import AppKit
|
|
import SwiftUI
|
|
@testable import Confab
|
|
|
|
@Suite("NativeTextEditor.inlineCodeRanges")
|
|
struct NativeTextEditorPureLogicTests {
|
|
|
|
@Test("No backticks yields no ranges")
|
|
func noBackticks() {
|
|
#expect(NativeTextEditor.inlineCodeRanges(in: "hello world").isEmpty)
|
|
}
|
|
|
|
@Test("A single closed backtick pair is matched, backticks included")
|
|
func singleClosedPair() {
|
|
let text = "Hello `code` world"
|
|
let ranges = NativeTextEditor.inlineCodeRanges(in: text)
|
|
#expect(ranges.count == 1)
|
|
let matched = (text as NSString).substring(with: ranges[0])
|
|
#expect(matched == "`code`")
|
|
}
|
|
|
|
@Test("An unterminated single backtick is not matched")
|
|
func unterminatedBacktick() {
|
|
let text = "Hello `code world"
|
|
#expect(NativeTextEditor.inlineCodeRanges(in: text).isEmpty)
|
|
}
|
|
|
|
@Test("Multiple closed pairs are all matched")
|
|
func multiplePairs() {
|
|
let text = "`one` and `two` and `three`"
|
|
let ranges = NativeTextEditor.inlineCodeRanges(in: text)
|
|
#expect(ranges.count == 3)
|
|
let nsText = text as NSString
|
|
#expect(ranges.map { nsText.substring(with: $0) } == ["`one`", "`two`", "`three`"])
|
|
}
|
|
|
|
@Test("Backtick pair cannot span a newline")
|
|
func doesNotSpanNewline() {
|
|
let text = "`code\nblock`"
|
|
#expect(NativeTextEditor.inlineCodeRanges(in: text).isEmpty)
|
|
}
|
|
|
|
@Test("Empty string yields no ranges")
|
|
func emptyString() {
|
|
#expect(NativeTextEditor.inlineCodeRanges(in: "").isEmpty)
|
|
}
|
|
|
|
@Test("Adjacent backtick pairs on the same line are each matched separately")
|
|
func adjacentPairs() {
|
|
let text = "`a``b`"
|
|
let ranges = NativeTextEditor.inlineCodeRanges(in: text)
|
|
let nsText = text as NSString
|
|
#expect(ranges.map { nsText.substring(with: $0) } == ["`a`", "`b`"])
|
|
}
|
|
}
|
|
|
|
@Suite("NativeTextEditor.fencedCodeBlockRanges")
|
|
struct NativeTextEditorFencedBlockTests {
|
|
|
|
@Test("No fences yields no ranges")
|
|
func noFences() {
|
|
#expect(NativeTextEditor.fencedCodeBlockRanges(in: "hello world").isEmpty)
|
|
}
|
|
|
|
@Test("A closed multi-line fence is matched in full, including the language tag")
|
|
func closedMultilineFence() {
|
|
let text = "```python\nprint(\"hi\")\n```"
|
|
let ranges = NativeTextEditor.fencedCodeBlockRanges(in: text)
|
|
#expect(ranges.count == 1)
|
|
#expect((text as NSString).substring(with: ranges[0]) == text)
|
|
}
|
|
|
|
@Test("An unterminated fence (no closing triple) is not matched")
|
|
func unterminatedFence() {
|
|
let text = "```python\nprint(\"hi\")"
|
|
#expect(NativeTextEditor.fencedCodeBlockRanges(in: text).isEmpty)
|
|
}
|
|
|
|
@Test("Text before and after a fence is excluded from the match")
|
|
func surroundingTextExcluded() {
|
|
let text = "before\n```\ncode\n```\nafter"
|
|
let ranges = NativeTextEditor.fencedCodeBlockRanges(in: text)
|
|
#expect(ranges.count == 1)
|
|
#expect((text as NSString).substring(with: ranges[0]) == "```\ncode\n```")
|
|
}
|
|
}
|
|
|
|
@Suite("NativeTextEditor.codeStyledRanges")
|
|
struct NativeTextEditorCodeStyledRangesTests {
|
|
|
|
@Test("Combines a fenced block and a separate inline span")
|
|
func combinesFencedAndInline() {
|
|
let text = "See `foo` then:\n```\nbar\n```"
|
|
let ranges = NativeTextEditor.codeStyledRanges(in: text)
|
|
let nsText = text as NSString
|
|
#expect(ranges.map { nsText.substring(with: $0) } == ["`foo`", "```\nbar\n```"])
|
|
}
|
|
|
|
@Test("A backtick inside a fenced block's own content is not separately re-matched as inline")
|
|
func backtickInsideFenceNotDoubleMatched() {
|
|
let text = "```\nuse `code` here\n```"
|
|
let ranges = NativeTextEditor.codeStyledRanges(in: text)
|
|
#expect(ranges.count == 1)
|
|
#expect((text as NSString).substring(with: ranges[0]) == text)
|
|
}
|
|
}
|
|
|
|
@Suite("NativeTextEditor.fencedCodeBlocks (language + code-content extraction)")
|
|
struct NativeTextEditorFencedCodeBlocksTests {
|
|
|
|
@Test("Extracts the language tag and the code content, excluding fences and the tag line")
|
|
func extractsLanguageAndCodeContent() {
|
|
let text = "```python\nprint(\"hi\")\n```"
|
|
let blocks = NativeTextEditor.fencedCodeBlocks(in: text)
|
|
#expect(blocks.count == 1)
|
|
#expect(blocks[0].language == "python")
|
|
let nsText = text as NSString
|
|
#expect(nsText.substring(with: blocks[0].codeRange) == "print(\"hi\")\n")
|
|
}
|
|
|
|
@Test("No language tag yields a nil language")
|
|
func noLanguageTagYieldsNil() {
|
|
let text = "```\ncode\n```"
|
|
let blocks = NativeTextEditor.fencedCodeBlocks(in: text)
|
|
#expect(blocks.count == 1)
|
|
#expect(blocks[0].language == nil)
|
|
}
|
|
|
|
@Test("Language alias is passed through as typed, unresolved (SyntaxHighlighter resolves aliases itself)")
|
|
func languageAliasPassthrough() {
|
|
let text = "```py\nprint(1)\n```"
|
|
let blocks = NativeTextEditor.fencedCodeBlocks(in: text)
|
|
#expect(blocks[0].language == "py")
|
|
}
|
|
|
|
@Test("Unterminated fence yields no blocks")
|
|
func unterminatedFenceYieldsNoBlocks() {
|
|
#expect(NativeTextEditor.fencedCodeBlocks(in: "```python\nprint(1)").isEmpty)
|
|
}
|
|
}
|
|
|
|
@Suite("SyntaxHighlighter runs → NSColor")
|
|
struct SyntaxHighlighterBridgingTests {
|
|
|
|
// NB: bridging via `NSAttributedString(highlighted)` does NOT work — that path stores
|
|
// SwiftUI's `.foregroundColor` under a private `SwiftUI.ForegroundColor` key, not the
|
|
// standard Cocoa `.foregroundColor` key (confirmed empirically before landing on this
|
|
// `.runs`-based approach, which is what `NativeTextEditor` actually uses in production).
|
|
|
|
@Test("A Python keyword's foreground color is readable via AttributedString.runs and converts to NSColor")
|
|
func keywordColorReadableViaRuns() {
|
|
let highlighted = SyntaxHighlighter.highlight(code: "def foo():", language: "python")
|
|
#expect(!highlighted.runs.isEmpty)
|
|
|
|
var foundKeywordColor = false
|
|
let expected = NSColor(SyntaxHighlighter.keywordColor)
|
|
for run in highlighted.runs {
|
|
guard let color = run.foregroundColor else { continue }
|
|
if NSColor(color).usingColorSpace(.deviceRGB) == expected.usingColorSpace(.deviceRGB) {
|
|
foundKeywordColor = true
|
|
}
|
|
}
|
|
#expect(foundKeywordColor)
|
|
}
|
|
}
|