1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-30 16:30:09 +02:00

🐛 Fix issue with code blocks

This commit is contained in:
2026-03-03 13:39:59 +01:00
parent fae5e5c4fb
commit 818246b0e5
4 changed files with 62 additions and 26 deletions

View File

@@ -12,11 +12,46 @@ import AppKit
/// Note: Written with the help of an LLM.
class CodeBlockTextView: NSTextView {
private let codePaddingX: CGFloat = 4
private let codePaddingY: CGFloat = 1
private let codeCornerRadius: CGFloat = 4
private let codePaddingY: CGFloat = 0
private let codeCornerRadius: CGFloat = 2
private lazy var appColor: NSColor = NSColor(named: "AppColor") ?? .systemBlue
/**
When we have selected text in a code block, we need to sanitize the output that will be copied to the pasteboard.
This means stripping thin spaces, no-break spaces and using Markdown's annotation for code blocks.
*/
override func copy(_ sender: Any?) {
guard let textStorage, selectedRange().length > 0 else {
super.copy(sender)
return
}
let selected = textStorage.attributedSubstring(from: selectedRange())
let codeSpanKey = MarkdownTextViewRepresentable.codeSpanKey
// Rebuild the string, wrapping code spans in backticks and cleaning up special characters
let result = NSMutableString()
selected.enumerateAttribute(codeSpanKey, in: NSRange(location: 0, length: selected.length)) { value, range, _ in
var fragment = (selected.string as NSString).substring(with: range)
fragment = fragment
.replacingOccurrences(of: "\u{2009}", with: "") // Remove thin spaces (leading padding)
.replacingOccurrences(of: "\u{202F}", with: "") // Remove narrow no-break spaces (trailing padding)
.replacingOccurrences(of: "\u{2060}", with: "") // Remove word joiners
.replacingOccurrences(of: "\u{00A0}", with: " ") // Restore regular spaces
if value != nil {
result.append("`\(fragment)`")
} else {
result.append(fragment)
}
}
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(result as String, forType: .string)
}
override func draw(_ dirtyRect: NSRect) {
drawCodeBackgrounds()
super.draw(dirtyRect)
@@ -39,25 +74,21 @@ class CodeBlockTextView: NSTextView {
textStorage.enumerateAttribute(codeSpanKey, in: NSRange(location: 0, length: textStorage.length)) { value, range, _ in
guard value != nil else { return }
// Get the glyph range and bounding rect for this code span
let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
var textRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// Trim line spacing from the rect height so the background fits the text tightly
let font = textStorage.attribute(.font, at: range.location, effectiveRange: nil) as? NSFont
let lineHeight = font?.ascender ?? 0 + abs(font?.descender ?? 0) + (font?.leading ?? 0)
if lineHeight > 0 && textRect.height > lineHeight {
textRect.size.height = lineHeight + 5 // added 5px for optimal size
}
// Enumerate per-line rects so wrapped code spans get individual backgrounds
layoutManager.enumerateEnclosingRects(
forGlyphRange: glyphRange,
withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0),
in: textContainer
) { lineRect, _ in
let rect = lineRect.offsetBy(dx: self.textContainerInset.width, dy: self.textContainerInset.height)
let paddedRect = rect.insetBy(dx: -self.codePaddingX, dy: -self.codePaddingY)
let path = NSBezierPath(roundedRect: paddedRect, xRadius: self.codeCornerRadius, yRadius: self.codeCornerRadius)
// Offset by text container inset
let rect = textRect.offsetBy(dx: textContainerInset.width, dy: textContainerInset.height)
let paddedRect = rect.insetBy(dx: -codePaddingX, dy: -codePaddingY)
let path = NSBezierPath(roundedRect: paddedRect, xRadius: codeCornerRadius, yRadius: codeCornerRadius)
// Fill
appColor.withAlphaComponent(0.15).setFill()
self.appColor.withAlphaComponent(0.15).setFill()
path.fill()
}
}
}
}

View File

@@ -71,7 +71,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
let font = NSFont.systemFont(ofSize: fontSize)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 3
paragraphStyle.lineSpacing = 2
paragraphStyle.paragraphSpacing = -4
let defaultAttributes: [NSAttributedString.Key: Any] = [
@@ -103,14 +103,19 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
paragraphStyle: NSParagraphStyle
) {
let codeFont = NSFont.monospacedSystemFont(ofSize: fontSize - 1, weight: .regular)
let thinSpace = "\u{2009}" // Thin space for visual padding around code spans
let leadingSpace = "\u{2009}" // Thin space (breakable) so the code span can shift to the next line
let trailingSpace = "\u{202F}" // Narrow no-break space to stay attached to the code span
let fullRange = NSRange(location: 0, length: result.length)
let matches = codeRegex.matches(in: result.string, range: fullRange).reversed()
for match in matches {
let innerRange = match.range(at: 1)
// Replace spaces with non-breaking spaces and insert word joiners after hyphens
// to prevent line breaks anywhere inside code spans
let innerText = (result.string as NSString).substring(with: innerRange)
.replacingOccurrences(of: " ", with: "\u{00A0}")
.replacingOccurrences(of: "-", with: "-\u{2060}")
let spaceAttributes: [NSAttributedString.Key: Any] = [
.font: codeFont,
@@ -118,14 +123,14 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
.paragraphStyle: paragraphStyle
]
// Build: thin space + code span (with marker) + thin space
// Build: leading space + code span (with marker) + trailing space
let replacement = NSMutableAttributedString()
replacement.append(NSAttributedString(string: thinSpace, attributes: spaceAttributes))
replacement.append(NSAttributedString(string: leadingSpace, attributes: spaceAttributes))
replacement.append(NSAttributedString(
string: innerText,
attributes: spaceAttributes.merging([Self.codeSpanKey: true]) { _, new in new }
))
replacement.append(NSAttributedString(string: thinSpace, attributes: spaceAttributes))
replacement.append(NSAttributedString(string: trailingSpace, attributes: spaceAttributes))
result.replaceCharacters(in: match.range, with: replacement)
}

View File

@@ -23,7 +23,7 @@ struct StartupAlertView: View {
titleText: viewModel.check.titleText,
subtitleText: viewModel.check.subtitleText
)
.padding(.top, 5)
.padding(.top, 4)
.padding(.bottom, 8)
// Fix command description: only shown in idle state when a fix is available
@@ -66,7 +66,7 @@ struct StartupAlertView: View {
onFix: { viewModel.runFix() }
)
}
.frame(width: 550)
.frame(width: 520)
}
}

View File

@@ -712,7 +712,7 @@ If you are seeing this message but are confused why this folder has gone missing
// Valet version too new or old
"startup.errors.valet_version_not_supported.title" = "This version of Valet is not supported";
"startup.errors.valet_version_not_supported.subtitle" = "You are running a version of Valet that is currently not supported. In order to avoid causing issues on your system, PHP Monitor cannot start.";
"startup.errors.valet_version_not_supported.desc" = "You can find out what version you are running by running `valet --version`. PHP Monitor will start if a compatible version of Valet is installed.
"startup.errors.valet_version_not_supported.desc" = "Run `valet --version` to find out what version is currently installed. PHP Monitor will start if a compatible version of Valet is installed.
**Note:** If this message is unexpected, you may also need to upgrade to a newer version of PHP Monitor. A newer version of PHP Monitor may include compatibility for newer versions of Valet.";