import DebounceFunction import Foundation import Perception import SwiftUI public struct AsyncCodeBlock: View { private struct Constants { static let paddingLeading = 5.0 static let paddingBottom = 10.0 static let paddingTrailing = 10.0 } @Environment(\.colorScheme) var colorScheme @Binding var isExpanded: Bool @State private var isHovering: Bool = false @AppStorage(\.completionHintShown) var completionHintShown let code: String let language: String let startLineIndex: Int let scenario: String let firstLineIndent: Double let lineHeight: Double let font: NSFont let proposedForegroundColor: Color? let proposedBackgroundColor: Color? let currentLineBackgroundColor: Color? let dimmedCharacterCount: Int let droppingLeadingSpaces: Bool let isPanelDisplayed: Bool public init( code: String, language: String, startLineIndex: Int, scenario: String, firstLineIndent: Double, lineHeight: Double, font: NSFont, droppingLeadingSpaces: Bool, proposedForegroundColor: Color?, proposedBackgroundColor: Color?, currentLineBackgroundColor: Color?, dimmedCharacterCount: Int, isExpanded: Binding, isPanelDisplayed: Bool ) { self.code = code self.startLineIndex = startLineIndex self.language = language self.scenario = scenario self.firstLineIndent = firstLineIndent self.lineHeight = lineHeight self.font = font self.proposedForegroundColor = proposedForegroundColor self.proposedBackgroundColor = proposedBackgroundColor self.currentLineBackgroundColor = currentLineBackgroundColor self.dimmedCharacterCount = dimmedCharacterCount self.droppingLeadingSpaces = droppingLeadingSpaces self._isExpanded = isExpanded self.isPanelDisplayed = isPanelDisplayed } var foregroundColor: Color { if let proposedForegroundColor = proposedForegroundColor { return proposedForegroundColor } return colorScheme == .light ? .black.opacity(0.85) : .white.opacity(0.85) } var foregroundTextColor: Color { return foregroundColor.opacity(0.6) } var backgroundColor: Color { if let proposedBackgroundColor = proposedBackgroundColor { return proposedBackgroundColor } return colorScheme == .dark ? Color(red: 0.1216, green: 0.1216, blue: 0.1412) : .white } var fontHeight: Double { (font.ascender + abs(font.descender)).rounded(.down) } var lineSpacing: Double { lineHeight - fontHeight } var expandedIndent: Double { let lines = code.splitByNewLine() guard let firstLine = lines.first else { return 0 } let existing = String(firstLine.prefix(dimmedCharacterCount)) let attr = NSAttributedString(string: existing, attributes: [.font: font]) return firstLineIndent - attr.size().width } var hintText: String { if isExpanded { return "Press ⌥⇥ to accept full suggestion" } return "Hold ⌥ for full suggestion" } @ScaledMetric var ellipsisPadding: CGFloat = 5 @ViewBuilder var contentView: some View { let lines = code.splitByNewLine() if let firstLine = lines.first { let firstLineTrimmed = firstLine .dropFirst(dimmedCharacterCount) HStack() { HStack(alignment: .center, spacing: 10) { Text(firstLineTrimmed) .foregroundColor(foregroundTextColor) .lineSpacing(lineSpacing) // This only has effect if a line wraps if lines.count > 1 { Image(systemName: "ellipsis") .renderingMode(.template) .foregroundColor(foregroundTextColor) .padding(.horizontal, ellipsisPadding) .background( RoundedRectangle(cornerRadius: 12) .fill(Color.gray.opacity(isExpanded ? 0.1 : 0.4)) .frame(height: fontHeight * 0.75) ) .popover(isPresented: $isHovering) { Text(hintText) .font(.body) .padding(8) .fixedSize() } .task { isHovering = !completionHintShown completionHintShown = true } } } .background(currentLineBackgroundColor ?? backgroundColor) .padding(.leading, firstLineIndent) .frame(minHeight: lineHeight) .onHover { hovering in guard hovering != isHovering else { return } withAnimation { isHovering = hovering } } Spacer() } } if isExpanded && lines.count > 1 { HStack() { CustomScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(Array(lines.dropFirst()), id: \.self) { line in HStack(alignment: .firstTextBaseline, spacing: 4) { Text(line) .foregroundColor(foregroundTextColor) .lineSpacing(lineSpacing) } .frame(minHeight: lineHeight) } } } .padding(EdgeInsets( top: 0, leading: Constants.paddingLeading, bottom: Constants.paddingBottom, trailing: Constants.paddingTrailing )) .background(backgroundColor) .cornerRadius(10) .overlay(RoundedRectangle(cornerRadius: 10).stroke(foregroundColor.opacity(0.2), lineWidth: 1)) // border .shadow(color: Color.black.opacity(0.2), radius: 8.0, x: 1, y: 1) .onHover { hovering in guard hovering != isHovering else { return } withAnimation { isHovering = hovering } } Spacer() } .padding(.leading, expandedIndent - Constants.paddingLeading) } } public var body: some View { if isPanelDisplayed { WithPerceptionTracking { VStack(spacing: 0) { contentView } .font(.init(font)) .background(Color.clear) } } } }