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