Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/MarkdownView/MarkdownTextBuilder/BlockProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import MarkdownParser
// MARK: - BlockProcessor

final class BlockProcessor {
// Synchronous rendering must avoid the isolated-deinit back-deployment path.
nonisolated deinit {}

private let theme: MarkdownTheme
private let viewProvider: ReusableViewProvider
private let context: MarkdownTextView.PreprocessedContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ private class ObjectPool<T: Equatable & Hashable> {
}

public final class ReusableViewProvider {
// Synchronous rendering must avoid the isolated-deinit back-deployment path.
nonisolated deinit {}

private let codeViewPool: ObjectPool<CodeView> = .init {
CodeView(frame: .zero)
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/MarkdownView/MarkdownTextBuilder/TextBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import MarkdownParser
#endif

final class TextBuilder {
// Synchronous rendering must avoid the isolated-deinit back-deployment path.
nonisolated deinit {}

private let nodes: [MarkdownBlockNode]
private let viewProvider: ReusableViewProvider
private var theme: MarkdownTheme = .default
Expand Down
8 changes: 4 additions & 4 deletions Tests/MarkdownViewTests/DiffViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ final class DiffViewTests: XCTestCase {
func testSingleTokenFenceAutoDetectsUnifiedDiffAndUsesInnerLanguage() {
let content = makeContent(
from: """
```swift
```javascript
@@ -1 +1 @@
-let title = "Design Engineer"
+let title = "Designer"
-const title = "Design Engineer";
+const title = "Designer";
```
"""
)
Expand All @@ -136,7 +136,7 @@ final class DiffViewTests: XCTestCase {
return XCTFail("Expected auto-detected diff render block")
}

XCTAssertEqual(renderBlock.language, "swift")
XCTAssertEqual(renderBlock.language, "javascript")
XCTAssertEqual(renderBlock.rows[1].kind, .removed)
XCTAssertFalse(renderBlock.rows[1].syntaxHighlights.isEmpty)
}
Expand Down
16 changes: 8 additions & 8 deletions Tests/MarkdownViewTests/HighlighterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,21 @@ final class HighlighterTests: XCTestCase {

func testHighlightReturnsMap() {
let highlighter = CodeHighlighter.current
let code = "def greet(name):\n return f\"Hello, {name}!\""
let map = highlighter.highlight(key: nil, content: code, language: "python")
// Should produce some color ranges for Python syntax
XCTAssertFalse(map.isEmpty, "Highlight map should contain color entries for Python code")
let code = "function greet(name) { return `Hello, ${name}!`; }"
let map = highlighter.highlight(key: nil, content: code, language: "javascript")
XCTAssertFalse(map.isEmpty, "Highlight map should contain color entries for JavaScript code")
}

func testHighlightCacheHit() {
let highlighter = CodeHighlighter.current
let code = "def cache_hit():\n return 42"
let key = highlighter.key(for: code, language: "python")
let code = "function cacheHit() { return 42; }"
let key = highlighter.key(for: code, language: "javascript")

let map1 = highlighter.highlight(key: key, content: code, language: "python")
let map2 = highlighter.highlight(key: key, content: code, language: "python")
let map1 = highlighter.highlight(key: key, content: code, language: "javascript")
let map2 = highlighter.highlight(key: key, content: code, language: "javascript")

// Both should return the same ranges (from cache on second call)
XCTAssertFalse(map1.isEmpty, "Cache test must exercise highlighted content")
XCTAssertEqual(map1.count, map2.count)
for (range, _) in map1 {
XCTAssertNotNil(map2[range], "Cached map should contain same ranges")
Expand Down
65 changes: 65 additions & 0 deletions Tests/MarkdownViewTests/RenderingLifetimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,71 @@ final class RenderingLifetimeTests: XCTestCase {
}
}

func testBlockProcessorAndOwnedCacheReleaseInsideSynchronousTaskLocalScope() {
let theme = MarkdownTheme.default
let context = MarkdownTextView.PreprocessedContent(markdown: "**Hello** `world`", theme: theme)
let viewProvider = ReusableViewProvider()
checkSynchronousRelease {
let processor = BlockProcessor(
theme: theme,
viewProvider: viewProvider,
context: context,
layoutWidth: 320,
thematicBreakDrawing: nil,
codeDrawing: nil,
diffDrawing: nil,
tableDrawing: nil,
blockquoteMarking: nil,
blockquoteDrawing: nil
)
let text = processor.processParagraph(contents: [.text("Hello world")])
XCTAssertTrue(text.string.contains("Hello world"))
return processor
}
}

func testTextBuilderReleaseInsideSynchronousTaskLocalScope() {
let context = MarkdownTextView.PreprocessedContent(markdown: "Hello", theme: .default)
let viewProvider = ReusableViewProvider()
checkSynchronousRelease {
TextBuilder(nodes: context.blocks, context: context, viewProvider: viewProvider)
}
}

func testTextBuilderAndOwnedProviderReleaseAfterSynchronousRender() {
let context = MarkdownTextView.PreprocessedContent(markdown: "**Hello** `world`", theme: .default)
checkSynchronousRelease {
let builder = TextBuilder(
nodes: context.blocks,
context: context,
viewProvider: ReusableViewProvider()
)
let result = builder.build()
XCTAssertTrue(result.document.string.contains("Hello world"))
return builder
}
}

func testTextBuilderAndOwnedProviderReleaseAfterSynchronousIncrementalRender() {
let context = MarkdownTextView.PreprocessedContent(markdown: "**Hello** `world`", theme: .default)
checkSynchronousRelease {
let builder = TextBuilder(
nodes: context.blocks,
context: context,
viewProvider: ReusableViewProvider()
)
let result = builder.buildIncremental(changes: [.rebuild(newIndex: 0)], cachedSegments: [])
XCTAssertTrue(result.document.string.contains("Hello world"))
return builder
}
}

func testReusableViewProviderReleaseInsideSynchronousTaskLocalScope() {
checkSynchronousRelease {
ReusableViewProvider()
}
}

// Keep this synchronous to match UIKit layout callbacks outside a Swift task.
private func checkSynchronousRelease<T: AnyObject>(_ makeObject: () -> T) {
for _ in 0 ..< 100 {
Expand Down
Loading