Fix glyph regression and unify macOS app icon source

This commit is contained in:
h3p 2026-02-11 13:56:57 +01:00
parent c451447d9e
commit c57c8b8193
7 changed files with 184 additions and 151 deletions

View file

@ -15,12 +15,15 @@ The format follows *Keep a Changelog*. Versions use semantic versioning with pre
- Settings architecture cleanup: editor options consolidated into Settings dialog/sheet and aligned with toolbar actions.
- Language detection and syntax highlighting stability for newly opened tabs and ongoing edits.
- Sequoia/Tahoe compatibility guards and cross-platform settings presentation behavior.
- Consolidated macOS app icon source to a single `Resources/AppIcon.icon` catalog (removed duplicate `Assets.xcassets/AppIcon.icon`).
### Fixed
- iOS build break caused by missing settings sheet state binding in `ContentView`.
- Find panel behavior (`Cmd+F`, initial focus, Enter-to-find-next) and highlight-current-line setting application.
- Line number ruler rendering overlap/flicker issues from previous fragment enumeration logic.
- Editor text sanitization paths around paste/tab/open flows to reduce injected visible whitespace glyph artifacts.
- Reintroduced whitespace/control glyph artifacts (`U+2581`, `U+2400``U+243F`) during typing/paste by hardening sanitizer checks in editor update paths.
- macOS line-number gutter redraw/background mismatch so the ruler keeps the same window/editor tone without white striping.
## [v0.4.4-beta] - 2026-02-09

View file

@ -101,6 +101,7 @@ struct NeonVisionEditorApp: App {
// Force-disable invisible/control character rendering.
defaults.set(false, forKey: "NSShowAllInvisibles")
defaults.set(false, forKey: "NSShowControlCharacters")
defaults.set(false, forKey: "SettingsShowInvisibleCharacters")
// Default editor behavior:
// - keep line numbers on
// - keep style/space visualization toggles off unless user enables them in Settings

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

View file

@ -1,64 +0,0 @@
{
"fill-specializations" : [
{
"value" : {
"linear-gradient" : [
"srgb:1.00000,0.41748,0.93330,1.00000",
"srgb:0.49593,0.00401,0.64443,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 0
},
"stop" : {
"x" : 0.5,
"y" : 0.7
}
}
}
},
{
"appearance" : "dark",
"value" : "automatic"
}
],
"groups" : [
{
"blur-material" : null,
"layers" : [
{
"blend-mode" : "darken",
"fill" : "none",
"hidden" : false,
"image-name" : "foreground.png",
"name" : "foreground",
"position" : {
"scale" : 1.38,
"translation-in-points" : [
8.279999999999973,
0.6900000000000546
]
}
}
],
"lighting" : "individual",
"opacity" : 1,
"shadow" : {
"kind" : "layer-color",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View file

@ -757,7 +757,7 @@ struct ContentView: View {
#endif
private func withBaseEditorEvents<Content: View>(_ view: Content) -> some View {
view
let viewWithClipboardEvents = view
.onReceive(NotificationCenter.default.publisher(for: .caretPositionDidChange)) { notif in
if let line = notif.userInfo?["line"] as? Int, let col = notif.userInfo?["column"] as? Int {
if line <= 0 {
@ -767,99 +767,135 @@ struct ContentView: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: .pastedText)) { notif in
if let pasted = notif.object as? String {
let result = LanguageDetector.shared.detect(text: pasted, name: nil, fileURL: nil)
currentLanguageBinding.wrappedValue = result.lang
.onReceive(NotificationCenter.default.publisher(for: .pastedText)) { notif in
handlePastedTextNotification(notif)
}
.onReceive(NotificationCenter.default.publisher(for: .pastedFileURL)) { notif in
handlePastedFileNotification(notif)
}
.onReceive(NotificationCenter.default.publisher(for: .zoomEditorFontRequested)) { notif in
let delta: Double = {
if let d = notif.object as? Double { return d }
if let n = notif.object as? NSNumber { return n.doubleValue }
return 1
}()
adjustEditorFontSize(delta)
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileURL)) { notif in
handleDroppedFileNotification(notif)
}
return viewWithClipboardEvents
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadStarted)) { notif in
droppedFileLoadInProgress = true
droppedFileProgressDeterminate = (notif.userInfo?["isDeterminate"] as? Bool) ?? true
droppedFileLoadProgress = 0
droppedFileLoadLabel = "Reading file"
largeFileModeEnabled = (notif.userInfo?["largeFileMode"] as? Bool) ?? false
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadProgress)) { notif in
// Recover even if "started" was missed.
droppedFileLoadInProgress = true
if let determinate = notif.userInfo?["isDeterminate"] as? Bool {
droppedFileProgressDeterminate = determinate
}
let fraction: Double = {
if let v = notif.userInfo?["fraction"] as? Double { return v }
if let v = notif.userInfo?["fraction"] as? NSNumber { return v.doubleValue }
if let v = notif.userInfo?["fraction"] as? Float { return Double(v) }
if let v = notif.userInfo?["fraction"] as? CGFloat { return Double(v) }
return droppedFileLoadProgress
}()
droppedFileLoadProgress = min(max(fraction, 0), 1)
if (notif.userInfo?["largeFileMode"] as? Bool) == true {
largeFileModeEnabled = true
}
if let name = notif.userInfo?["fileName"] as? String, !name.isEmpty {
droppedFileLoadLabel = name
}
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadFinished)) { notif in
let success = (notif.userInfo?["success"] as? Bool) ?? true
droppedFileLoadProgress = success ? 1 : 0
droppedFileProgressDeterminate = true
if (notif.userInfo?["largeFileMode"] as? Bool) == true {
largeFileModeEnabled = true
}
if !success, let message = notif.userInfo?["message"] as? String, !message.isEmpty {
findStatusMessage = "Drop failed: \(message)"
droppedFileLoadLabel = "Import failed"
}
DispatchQueue.main.asyncAfter(deadline: .now() + (success ? 0.35 : 2.5)) {
droppedFileLoadInProgress = false
}
}
.onChange(of: viewModel.selectedTab?.id) { _, _ in
updateLargeFileMode(for: currentContentBinding.wrappedValue)
highlightRefreshToken &+= 1
}
.onChange(of: currentLanguage) { _, newValue in
settingsTemplateLanguage = newValue
}
}
private func handlePastedTextNotification(_ notif: Notification) {
guard let pasted = notif.object as? String else {
DispatchQueue.main.async {
updateLargeFileMode(for: currentContentBinding.wrappedValue)
highlightRefreshToken &+= 1
}
return
}
.onReceive(NotificationCenter.default.publisher(for: .pastedFileURL)) { notif in
var urls: [URL] = []
if let url = notif.object as? URL {
urls = [url]
} else if let list = notif.object as? [URL] {
urls = list
}
guard !urls.isEmpty else { return }
for url in urls {
viewModel.openFile(url: url)
}
DispatchQueue.main.async {
updateLargeFileMode(for: currentContentBinding.wrappedValue)
highlightRefreshToken &+= 1
let result = LanguageDetector.shared.detect(text: pasted, name: nil, fileURL: nil)
if let tab = viewModel.selectedTab {
if let idx = viewModel.tabs.firstIndex(where: { $0.id == tab.id }),
!viewModel.tabs[idx].languageLocked,
viewModel.tabs[idx].language == "plain",
result.lang != "plain" {
viewModel.tabs[idx].language = result.lang
}
} else if singleLanguage == "plain", result.lang != "plain" {
singleLanguage = result.lang
}
.onReceive(NotificationCenter.default.publisher(for: .zoomEditorFontRequested)) { notif in
let delta: Double = {
if let d = notif.object as? Double { return d }
if let n = notif.object as? NSNumber { return n.doubleValue }
return 1
}()
adjustEditorFontSize(delta)
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileURL)) { notif in
guard let fileURL = notif.object as? URL else { return }
if let preferred = LanguageDetector.shared.preferredLanguage(for: fileURL) {
currentLanguageBinding.wrappedValue = preferred
}
DispatchQueue.main.async {
updateLargeFileMode(for: currentContentBinding.wrappedValue)
highlightRefreshToken &+= 1
}
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadStarted)) { notif in
droppedFileLoadInProgress = true
droppedFileProgressDeterminate = (notif.userInfo?["isDeterminate"] as? Bool) ?? true
droppedFileLoadProgress = 0
droppedFileLoadLabel = "Reading file"
largeFileModeEnabled = (notif.userInfo?["largeFileMode"] as? Bool) ?? false
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadProgress)) { notif in
// Recover even if "started" was missed.
droppedFileLoadInProgress = true
if let determinate = notif.userInfo?["isDeterminate"] as? Bool {
droppedFileProgressDeterminate = determinate
}
let fraction: Double = {
if let v = notif.userInfo?["fraction"] as? Double { return v }
if let v = notif.userInfo?["fraction"] as? NSNumber { return v.doubleValue }
if let v = notif.userInfo?["fraction"] as? Float { return Double(v) }
if let v = notif.userInfo?["fraction"] as? CGFloat { return Double(v) }
return droppedFileLoadProgress
}()
droppedFileLoadProgress = min(max(fraction, 0), 1)
if (notif.userInfo?["largeFileMode"] as? Bool) == true {
largeFileModeEnabled = true
}
if let name = notif.userInfo?["fileName"] as? String, !name.isEmpty {
droppedFileLoadLabel = name
}
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadFinished)) { notif in
let success = (notif.userInfo?["success"] as? Bool) ?? true
droppedFileLoadProgress = success ? 1 : 0
droppedFileProgressDeterminate = true
if (notif.userInfo?["largeFileMode"] as? Bool) == true {
largeFileModeEnabled = true
}
if !success, let message = notif.userInfo?["message"] as? String, !message.isEmpty {
findStatusMessage = "Drop failed: \(message)"
droppedFileLoadLabel = "Import failed"
}
DispatchQueue.main.asyncAfter(deadline: .now() + (success ? 0.35 : 2.5)) {
droppedFileLoadInProgress = false
}
}
.onChange(of: viewModel.selectedTab?.id) { _, _ in
DispatchQueue.main.async {
updateLargeFileMode(for: currentContentBinding.wrappedValue)
highlightRefreshToken &+= 1
}
.onChange(of: currentLanguage) { _, newValue in
settingsTemplateLanguage = newValue
}
private func handlePastedFileNotification(_ notif: Notification) {
var urls: [URL] = []
if let url = notif.object as? URL {
urls = [url]
} else if let list = notif.object as? [URL] {
urls = list
}
guard !urls.isEmpty else { return }
for url in urls {
viewModel.openFile(url: url)
}
DispatchQueue.main.async {
updateLargeFileMode(for: currentContentBinding.wrappedValue)
highlightRefreshToken &+= 1
}
}
private func handleDroppedFileNotification(_ notif: Notification) {
guard let fileURL = notif.object as? URL else { return }
if let preferred = LanguageDetector.shared.preferredLanguage(for: fileURL) {
if let tab = viewModel.selectedTab {
if let idx = viewModel.tabs.firstIndex(where: { $0.id == tab.id }),
!viewModel.tabs[idx].languageLocked,
viewModel.tabs[idx].language == "plain" {
viewModel.tabs[idx].language = preferred
}
} else if singleLanguage == "plain" {
singleLanguage = preferred
}
}
DispatchQueue.main.async {
updateLargeFileMode(for: currentContentBinding.wrappedValue)
highlightRefreshToken &+= 1
}
}

View file

@ -363,6 +363,16 @@ final class AcceptingTextView: NSTextView {
EditorTextSanitizer.sanitize(input)
}
private static func containsGlyphArtifacts(_ input: String) -> Bool {
for scalar in input.unicodeScalars {
let value = scalar.value
if value == 0x2581 || (0x2400...0x243F).contains(value) {
return true
}
}
return false
}
private func sanitizedPlainText(_ input: String) -> String {
Self.sanitizePlainText(input)
}
@ -566,7 +576,18 @@ final class AcceptingTextView: NSTextView {
override func didChangeText() {
super.didChangeText()
forceDisableInvisibleGlyphRendering()
forceDisableInvisibleGlyphRendering(deep: true)
if let storage = textStorage {
let raw = storage.string
if Self.containsGlyphArtifacts(raw) {
let sanitized = Self.sanitizePlainText(raw)
let sel = selectedRange()
storage.beginEditing()
storage.replaceCharacters(in: NSRange(location: 0, length: (raw as NSString).length), with: sanitized)
storage.endEditing()
setSelectedRange(NSRange(location: min(sel.location, (sanitized as NSString).length), length: 0))
}
}
if !isApplyingInlineSuggestion {
clearInlineSuggestion()
}

View file

@ -5,26 +5,44 @@ final class LineNumberRulerView: NSRulerView {
weak var textView: NSTextView?
private let font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular)
private let textColor = NSColor.secondaryLabelColor
private let textColor = NSColor.tertiaryLabelColor.withAlphaComponent(0.92)
private let inset: CGFloat = 6
private var observers: [NSObjectProtocol] = []
init(textView: NSTextView) {
self.textView = textView
super.init(scrollView: textView.enclosingScrollView, orientation: .verticalRuler)
self.clientView = textView
self.ruleThickness = 48
installObservers(textView: textView)
}
required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
deinit {
for observer in observers {
NotificationCenter.default.removeObserver(observer)
}
}
override var isOpaque: Bool { true }
override func draw(_ dirtyRect: NSRect) {
let bg: NSColor = textView?.backgroundColor ?? .clear
let bg: NSColor = {
guard let tv = textView else { return .windowBackgroundColor }
let color = tv.backgroundColor
if color.alphaComponent >= 0.99 {
return color
}
if let windowColor = tv.window?.backgroundColor {
return windowColor
}
return .windowBackgroundColor
}()
bg.setFill()
bounds.fill()
NSColor.separatorColor.withAlphaComponent(0.35).setFill()
NSColor.separatorColor.withAlphaComponent(0.09).setFill()
NSRect(x: bounds.maxX - 1, y: bounds.minY, width: 1, height: bounds.height).fill()
drawHashMarksAndLabels(in: dirtyRect)
@ -91,5 +109,23 @@ final class LineNumberRulerView: NSRulerView {
numberString.draw(at: drawPoint, withAttributes: attributes)
}
}
private func installObservers(textView: NSTextView) {
let center = NotificationCenter.default
observers.append(center.addObserver(
forName: NSText.didChangeNotification,
object: textView,
queue: .main
) { [weak self] _ in
self?.needsDisplay = true
})
observers.append(center.addObserver(
forName: NSView.boundsDidChangeNotification,
object: textView.enclosingScrollView?.contentView,
queue: .main
) { [weak self] _ in
self?.needsDisplay = true
})
}
}
#endif