mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Fix glyph regression and unify macOS app icon source
This commit is contained in:
parent
c451447d9e
commit
c57c8b8193
7 changed files with 184 additions and 151 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue