1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-02 17:40:08 +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. /// Note: Written with the help of an LLM.
class CodeBlockTextView: NSTextView { class CodeBlockTextView: NSTextView {
private let codePaddingX: CGFloat = 4 private let codePaddingX: CGFloat = 4
private let codePaddingY: CGFloat = 1 private let codePaddingY: CGFloat = 0
private let codeCornerRadius: CGFloat = 4 private let codeCornerRadius: CGFloat = 2
private lazy var appColor: NSColor = NSColor(named: "AppColor") ?? .systemBlue 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) { override func draw(_ dirtyRect: NSRect) {
drawCodeBackgrounds() drawCodeBackgrounds()
super.draw(dirtyRect) super.draw(dirtyRect)
@@ -39,25 +74,21 @@ class CodeBlockTextView: NSTextView {
textStorage.enumerateAttribute(codeSpanKey, in: NSRange(location: 0, length: textStorage.length)) { value, range, _ in textStorage.enumerateAttribute(codeSpanKey, in: NSRange(location: 0, length: textStorage.length)) { value, range, _ in
guard value != nil else { return } guard value != nil else { return }
// Get the glyph range and bounding rect for this code span
let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) 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 // Enumerate per-line rects so wrapped code spans get individual backgrounds
let font = textStorage.attribute(.font, at: range.location, effectiveRange: nil) as? NSFont layoutManager.enumerateEnclosingRects(
let lineHeight = font?.ascender ?? 0 + abs(font?.descender ?? 0) + (font?.leading ?? 0) forGlyphRange: glyphRange,
if lineHeight > 0 && textRect.height > lineHeight { withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0),
textRect.size.height = lineHeight + 5 // added 5px for optimal size 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 self.appColor.withAlphaComponent(0.15).setFill()
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()
path.fill() path.fill()
} }
} }
} }
}

View File

@@ -71,7 +71,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
let font = NSFont.systemFont(ofSize: fontSize) let font = NSFont.systemFont(ofSize: fontSize)
let paragraphStyle = NSMutableParagraphStyle() let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 3 paragraphStyle.lineSpacing = 2
paragraphStyle.paragraphSpacing = -4 paragraphStyle.paragraphSpacing = -4
let defaultAttributes: [NSAttributedString.Key: Any] = [ let defaultAttributes: [NSAttributedString.Key: Any] = [
@@ -103,14 +103,19 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
paragraphStyle: NSParagraphStyle paragraphStyle: NSParagraphStyle
) { ) {
let codeFont = NSFont.monospacedSystemFont(ofSize: fontSize - 1, weight: .regular) 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 fullRange = NSRange(location: 0, length: result.length)
let matches = codeRegex.matches(in: result.string, range: fullRange).reversed() let matches = codeRegex.matches(in: result.string, range: fullRange).reversed()
for match in matches { for match in matches {
let innerRange = match.range(at: 1) 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) let innerText = (result.string as NSString).substring(with: innerRange)
.replacingOccurrences(of: " ", with: "\u{00A0}")
.replacingOccurrences(of: "-", with: "-\u{2060}")
let spaceAttributes: [NSAttributedString.Key: Any] = [ let spaceAttributes: [NSAttributedString.Key: Any] = [
.font: codeFont, .font: codeFont,
@@ -118,14 +123,14 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
.paragraphStyle: paragraphStyle .paragraphStyle: paragraphStyle
] ]
// Build: thin space + code span (with marker) + thin space // Build: leading space + code span (with marker) + trailing space
let replacement = NSMutableAttributedString() let replacement = NSMutableAttributedString()
replacement.append(NSAttributedString(string: thinSpace, attributes: spaceAttributes)) replacement.append(NSAttributedString(string: leadingSpace, attributes: spaceAttributes))
replacement.append(NSAttributedString( replacement.append(NSAttributedString(
string: innerText, string: innerText,
attributes: spaceAttributes.merging([Self.codeSpanKey: true]) { _, new in new } 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) result.replaceCharacters(in: match.range, with: replacement)
} }

View File

@@ -23,7 +23,7 @@ struct StartupAlertView: View {
titleText: viewModel.check.titleText, titleText: viewModel.check.titleText,
subtitleText: viewModel.check.subtitleText subtitleText: viewModel.check.subtitleText
) )
.padding(.top, 5) .padding(.top, 4)
.padding(.bottom, 8) .padding(.bottom, 8)
// Fix command description: only shown in idle state when a fix is available // Fix command description: only shown in idle state when a fix is available
@@ -66,7 +66,7 @@ struct StartupAlertView: View {
onFix: { viewModel.runFix() } 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 // 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.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.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."; **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.";