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
Original file line number Diff line number Diff line change
Expand Up @@ -794,69 +794,148 @@ enum ReviewMonitorCommandOutputDisplayDocument {
) -> ReviewMonitorLog.Change {
switch change {
case .append(let append):
guard
let mappedRange = mappedChangeRange(
append.range,
blockID: append.blockID,
sourceBlocks: sourceBlocks,
displayBlocks: displayBlocks
if let panelMutation = mappedCommandPanelMutation(
sourceRange: append.range,
blockID: append.blockID,
sourceBlocks: sourceBlocks,
displayBlocks: displayBlocks,
previousDisplay: previousDisplay,
displayText: displayText
),
let validatedMutation = validatedChange(
panelMutation,
previousDisplay: previousDisplay,
displayText: displayText
)
else {
if let panelMutation = mappedCommandPanelMutation(
sourceRange: append.range,
blockID: append.blockID,
sourceBlocks: sourceBlocks,
displayBlocks: displayBlocks,
{
return validatedMutation
}
if let displayAppend = mappedDisplayAppend(
previousDisplay: previousDisplay,
displayText: displayText,
displayBlocks: displayBlocks
),
let validatedMutation = validatedChange(
.append(displayAppend),
previousDisplay: previousDisplay,
displayText: displayText
) {
return panelMutation
}
return .reload
)
{
return validatedMutation
}
return .append(
.init(
kind: append.kind,
blockID: append.blockID,
range: mappedRange,
text: append.text,
textUTF16Length: append.textUTF16Length,
animationSpans: append.animationSpans
))
return .reload
case .replace(let replacement):
guard
let mappedRange = mappedChangeRange(
replacement.range,
blockID: replacement.blockID,
sourceBlocks: sourceBlocks,
displayBlocks: displayBlocks
if let mappedReplacement = mappedBlockReplacement(
replacement,
sourceBlocks: sourceBlocks,
displayBlocks: displayBlocks,
previousDisplay: previousDisplay,
displayText: displayText
),
let validatedMutation = validatedChange(
.replace(mappedReplacement),
previousDisplay: previousDisplay,
displayText: displayText
)
else {
if let panelMutation = mappedCommandPanelMutation(
{
return validatedMutation
}
if let panelMutation = mappedCommandPanelMutation(
sourceRange: replacement.range,
blockID: replacement.blockID,
sourceBlocks: sourceBlocks,
displayBlocks: displayBlocks,
previousDisplay: previousDisplay,
displayText: displayText
) {
return panelMutation
}
return .reload
),
let validatedMutation = validatedChange(
panelMutation,
previousDisplay: previousDisplay,
displayText: displayText
)
{
return validatedMutation
}
return .replace(
.init(
kind: replacement.kind,
blockID: replacement.blockID,
range: mappedRange,
text: replacement.text,
textUTF16Length: replacement.textUTF16Length
))
return .reload
case .reload:
return .reload
}
}

private static func mappedDisplayAppend(
previousDisplay: ReviewMonitorLog.Document?,
displayText: String,
displayBlocks: [ReviewMonitorLog.Block]
) -> ReviewMonitorLog.Append? {
guard let previousDisplay else {
return nil
}

let displayString = displayText as NSString
let previousLength = previousDisplay.textUTF16Length
guard previousLength < displayString.length,
displayString.substring(with: NSRange(location: 0, length: previousLength)) == previousDisplay.text
else {
return nil
}

let suffixRange = NSRange(location: previousLength, length: displayString.length - previousLength)
let suffix = displayString.substring(with: suffixRange)
let block = displayBlocks.first {
NSIntersectionRange($0.range, suffixRange).length > 0
}
return .init(
kind: block?.kind ?? .event,
blockID: block?.id ?? ReviewMonitorLog.BlockID("commandOutputDisplayAppend"),
Comment thread
lynnswap marked this conversation as resolved.
range: suffixRange,
text: suffix,
textUTF16Length: suffixRange.length,
animationSpans: displayBlocks.flatMap { block in
let intersection = NSIntersectionRange(block.range, suffixRange)
guard intersection.length > 0 else {
return [] as [ReviewMonitorLog.AnimationSpan]
}
return ReviewMonitorLog.Append.animationSpans(
forKind: block.kind,
absoluteRange: intersection,
appendBaseLocation: previousLength
)
}
)
}

private static func mappedBlockReplacement(
_ replacement: ReviewMonitorLog.Replacement,
sourceBlocks: [ReviewMonitorLog.Block],
displayBlocks: [ReviewMonitorLog.Block],
previousDisplay: ReviewMonitorLog.Document?,
displayText: String
) -> ReviewMonitorLog.Replacement? {
guard let previousDisplay,
let sourceBlock = sourceBlocks.first(where: { $0.id == replacement.blockID }),
sourceBlock.kind != .command,
sourceBlock.kind != .commandOutput,
let displayBlock = displayBlocks.first(where: { $0.id == replacement.blockID }),
let previousDisplayBlock = previousDisplay.blocks.first(where: { $0.id == replacement.blockID })
else {
return nil
}

guard rangeIsValid(displayBlock.range, in: displayText),
rangeIsValid(previousDisplayBlock.range, in: previousDisplay.text)
else {
return nil
}
let replacementText = (displayText as NSString).substring(with: displayBlock.range)
return .init(
kind: displayBlock.kind,
blockID: displayBlock.id,
range: previousDisplayBlock.range,
text: replacementText,
textUTF16Length: (replacementText as NSString).length
)
}

private static func mappedCommandPanelMutation(
sourceRange: NSRange,
blockID: ReviewMonitorLog.BlockID,
Expand Down Expand Up @@ -907,6 +986,37 @@ enum ReviewMonitorCommandOutputDisplayDocument {
return .append(append)
}

private static func validatedChange(
_ change: ReviewMonitorLog.Change,
previousDisplay: ReviewMonitorLog.Document?,
displayText: String
) -> ReviewMonitorLog.Change? {
guard let previousDisplay else {
return nil
}
switch change {
case .append(let append):
guard append.textUTF16Length == (append.text as NSString).length,
append.range.location == previousDisplay.textUTF16Length,
append.range.length == append.textUTF16Length,
previousDisplay.text + append.text == displayText
else {
return nil
}
return change
case .replace(let replacement):
guard replacement.textUTF16Length == (replacement.text as NSString).length,
rangeIsValid(replacement.range, in: previousDisplay.text),
replacingText(in: previousDisplay.text, range: replacement.range, with: replacement.text) == displayText
else {
return nil
}
return change
case .reload:
return change
}
}

private static func mappedNewCommandPanelAppend(
displayBlock: ReviewMonitorLog.Block,
displayText: String,
Expand Down Expand Up @@ -958,25 +1068,25 @@ enum ReviewMonitorCommandOutputDisplayDocument {
}
}

private static func mappedChangeRange(
_ range: NSRange,
blockID: ReviewMonitorLog.BlockID,
sourceBlocks: [ReviewMonitorLog.Block],
displayBlocks: [ReviewMonitorLog.Block]
) -> NSRange? {
guard let sourceBlock = sourceBlocks.first(where: { $0.id == blockID }),
sourceBlock.kind != .command,
sourceBlock.kind != .commandOutput,
let displayBlock = displayBlocks.first(where: { $0.id == blockID }),
NSMaxRange(range) <= NSMaxRange(sourceBlock.range)
private static func rangeIsValid(_ range: NSRange, in text: String) -> Bool {
let length = (text as NSString).length
return range.location >= 0 && range.length >= 0 && NSMaxRange(range) <= length
}

private static func replacingText(in text: String, range: NSRange, with replacement: String) -> String {
let string = text as NSString
guard range.location >= 0,
range.length >= 0,
NSMaxRange(range) <= string.length
else {
return nil
return text
}
let mappedRange = map(range, from: sourceBlock.range, to: displayBlock.range)
guard NSMaxRange(mappedRange) <= NSMaxRange(displayBlock.range) else {
return nil
}
return mappedRange
let prefix = string.substring(with: NSRange(location: 0, length: range.location))
let suffixLocation = NSMaxRange(range)
let suffix = string.substring(
with: NSRange(location: suffixLocation, length: string.length - suffixLocation)
)
return prefix + replacement + suffix
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ final class ReviewMonitorLogDocumentView: NSView, NSUserInterfaceValidations,
}

if block.kind.requiresMarkdownPresentationInvalidationOnAppend {
return block.range
return NSUnionRange(block.range, append.range)
}
return append.range
}
Expand Down Expand Up @@ -2726,6 +2726,18 @@ final class ReviewMonitorLogDocumentView: NSView, NSUserInterfaceValidations,
return hitTest(NSPoint(x: rect.midX, y: rect.midY)) === self
}

func fontPointSizeForFirstOccurrenceForTesting(_ text: String) -> CGFloat? {
layoutTextViewport(force: true)
let range = (textStorage.string as NSString).range(of: text)
guard range.location != NSNotFound,
range.location < textStorage.length,
let font = textStorage.attribute(.font, at: range.location, effectiveRange: nil) as? NSFont
else {
return nil
}
return font.pointSize
}

func toggleFirstCommandOutputPanelForTesting() {
layoutTextViewport(force: true)
guard let blockID = currentCommandOutputPanels.first?.blockID else {
Expand Down
47 changes: 38 additions & 9 deletions Sources/ReviewChatLogUI/Detail/ReviewMonitorLogScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,21 +344,34 @@ final class ReviewMonitorLogScrollView: NSScrollView {
guard append.text.isEmpty == false else {
return false
}
let appendEnd = displayedUTF16Length + append.textUTF16Length
return append.textUTF16Length > 0 &&
append.range.location >= displayedUTF16Length &&
NSMaxRange(append.range) <= appendEnd &&
document.textUTF16Length == displayedUTF16Length + append.textUTF16Length
let expectedRange = NSRange(location: displayedUTF16Length, length: append.textUTF16Length)
guard append.textUTF16Length == (append.text as NSString).length,
append.range == expectedRange,
document.textUTF16Length == displayedUTF16Length + append.textUTF16Length,
displayedText + append.text == document.text
else {
assertionFailure("Log append change is not synchronized with the display document.")
return false
}
return true
}

private func canApplyReplacement(
_ replacement: ReviewMonitorLog.Replacement,
to document: ReviewMonitorLog.Document
) -> Bool {
replacement.textUTF16Length >= 0 &&
replacement.range.location >= 0 &&
NSMaxRange(replacement.range) <= displayedUTF16Length &&
document.textUTF16Length == displayedUTF16Length - replacement.range.length + replacement.textUTF16Length
guard replacement.textUTF16Length == (replacement.text as NSString).length,
replacement.range.location >= 0,
replacement.range.length >= 0,
NSMaxRange(replacement.range) <= displayedUTF16Length,
document.textUTF16Length == displayedUTF16Length - replacement.range.length + replacement.textUTF16Length,
let replacementResult = replacingDisplayedText(in: replacement.range, with: replacement.text),
replacementResult == document.text
else {
assertionFailure("Log replacement change is not synchronized with the display document.")
return false
}
return true
}

private func canApplyFallbackAppend(_ suffix: String, to document: ReviewMonitorLog.Document) -> Bool {
Expand Down Expand Up @@ -883,6 +896,18 @@ final class ReviewMonitorLogScrollView: NSScrollView {
displayedText = mutable as String
}

private func replacingDisplayedText(in range: NSRange, with text: String) -> String? {
guard range.location >= 0,
range.length >= 0,
NSMaxRange(range) <= displayedUTF16Length
else {
return nil
}
let mutable = NSMutableString(string: displayedText)
mutable.replaceCharacters(in: range, with: text)
return mutable as String
}

private func invalidateDocumentLayout() {
if syncDocumentFrameToTextLayout() {
logDocumentView.invalidateIntrinsicContentSize()
Expand Down Expand Up @@ -1536,6 +1561,10 @@ extension ReviewMonitorLogScrollView {
logDocumentView.hitTestTargetsDocumentViewForFirstOccurrenceForTesting(text)
}

func fontPointSizeForFirstLogOccurrenceForTesting(_ text: String) -> CGFloat? {
logDocumentView.fontPointSizeForFirstOccurrenceForTesting(text)
}

func toggleFirstCommandOutputPanelForTesting() {
logDocumentView.toggleFirstCommandOutputPanelForTesting()
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/ReviewChatLogUI/ReviewMonitorCodexChatLogTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,10 @@ package final class ReviewMonitorCodexChatLogTarget {
logScrollView.hitTestTargetsDocumentViewForFirstLogOccurrenceForTesting(text)
}

func fontPointSizeForFirstLogOccurrenceForTesting(_ text: String) -> CGFloat? {
logScrollView.fontPointSizeForFirstLogOccurrenceForTesting(text)
}

func toggleFirstCommandOutputPanelForTesting() {
logScrollView.toggleFirstCommandOutputPanelForTesting()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ final class ReviewMonitorTransportViewController: NSViewController {
chatLogTarget.hitTestTargetsDocumentViewForFirstLogOccurrenceForTesting(text)
}

func logFontPointSizeForFirstOccurrenceForTesting(_ text: String) -> CGFloat? {
chatLogTarget.fontPointSizeForFirstLogOccurrenceForTesting(text)
}

func toggleFirstLogCommandOutputPanelForTesting() {
chatLogTarget.toggleFirstCommandOutputPanelForTesting()
}
Expand Down
Loading