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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user