diff --git a/CHANGELOG.md b/CHANGELOG.md index bc3bacc..0241ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to **Neon Vision Editor** are documented in this file. The format follows *Keep a Changelog*. Versions use semantic versioning with prerelease tags. +## [v0.4.30] - 2026-02-24 + +### Added +- TODO + +### Improved +- TODO + +### Fixed +- TODO + ## [v0.4.29] - 2026-02-23 ### Added diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj index 2a469c8..6e81a36 100644 --- a/Neon Vision Editor.xcodeproj/project.pbxproj +++ b/Neon Vision Editor.xcodeproj/project.pbxproj @@ -361,7 +361,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 329; + CURRENT_PROJECT_VERSION = 330; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; @@ -442,7 +442,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 329; + CURRENT_PROJECT_VERSION = 330; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; diff --git a/Neon Vision Editor/App/NeonVisionEditorApp.swift b/Neon Vision Editor/App/NeonVisionEditorApp.swift index 86db961..ff6bde5 100644 --- a/Neon Vision Editor/App/NeonVisionEditorApp.swift +++ b/Neon Vision Editor/App/NeonVisionEditorApp.swift @@ -90,6 +90,8 @@ struct NeonVisionEditorApp: App { @State private var useAppleIntelligence: Bool = true @State private var appleAIStatus: String = "Apple Intelligence: Checking…" @State private var appleAIRoundTripMS: Double? = nil + @State private var settingsShortcutMonitorInstalled = false + @State private var settingsShortcutMonitorToken: Any? @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate #endif @State private var showGrokError: Bool = false @@ -262,6 +264,7 @@ struct NeonVisionEditorApp: App { .environmentObject(supportPurchaseManager) .environmentObject(appUpdateManager) .onAppear { + installSettingsShortcutMonitorIfNeeded() appDelegate.viewModel = viewModel appDelegate.appUpdateManager = appUpdateManager } @@ -326,6 +329,30 @@ struct NeonVisionEditorApp: App { .preferredColorScheme(preferredAppearance) } + MenuBarExtra("Welcome Tour", systemImage: "sparkles.rectangle.stack") { + Button { + postWindowCommand(.showWelcomeTourRequested) + } label: { + Label("Show Welcome Tour", systemImage: "sparkles.rectangle.stack") + } + + Divider() + + Button { + showSettingsWindow() + } label: { + Label("Settings…", systemImage: "gearshape") + } + + if ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution { + Button { + postWindowCommand(.showUpdaterRequested, object: true) + } label: { + Label("Check for Updates…", systemImage: "arrow.triangle.2.circlepath.circle") + } + } + } + .commands { CommandGroup(replacing: .appSettings) { if ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution { @@ -337,7 +364,7 @@ struct NeonVisionEditorApp: App { Divider() Button("Settings…") { - postWindowCommand(.showSettingsRequested) + showSettingsWindow() } .keyboardShortcut(",", modifiers: .command) } @@ -607,11 +634,38 @@ struct NeonVisionEditorApp: App { private func showSettingsWindow() { #if os(macOS) + NSApp.activate(ignoringOtherApps: true) if !NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) { - _ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) + if !NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) { + postWindowCommand(.showSettingsRequested) + } } #endif } + +#if os(macOS) + private func installSettingsShortcutMonitorIfNeeded() { + guard !settingsShortcutMonitorInstalled else { return } + settingsShortcutMonitorInstalled = true + settingsShortcutMonitorToken = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + guard event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.command) else { + return event + } + let chars = event.characters ?? "" + let charsIgnoringModifiers = event.charactersIgnoringModifiers ?? "" + if chars == "+" + || chars == "=" + || chars == "," + || charsIgnoringModifiers == "+" + || charsIgnoringModifiers == "=" + || charsIgnoringModifiers == "," { + showSettingsWindow() + return nil + } + return event + } + } +#endif } struct ShowGrokErrorKey: EnvironmentKey { diff --git a/Neon Vision Editor/UI/ContentView+Toolbar.swift b/Neon Vision Editor/UI/ContentView+Toolbar.swift index cc496f5..39e4766 100644 --- a/Neon Vision Editor/UI/ContentView+Toolbar.swift +++ b/Neon Vision Editor/UI/ContentView+Toolbar.swift @@ -10,6 +10,31 @@ extension ContentView { activeProviderName.components(separatedBy: " (").first ?? activeProviderName } + private var providerBadgeLabelText: String { +#if os(macOS) + if compactActiveProviderName == "Apple" { + return "AI Provider \(compactActiveProviderName)" + } +#endif + return compactActiveProviderName + } + + private var providerBadgeIsAppleCompletionActive: Bool { + compactActiveProviderName == "Apple" && isAutoCompletionEnabled + } + + private var providerBadgeForegroundColor: Color { + providerBadgeIsAppleCompletionActive ? .green : .secondary + } + + private var providerBadgeBackgroundColor: Color { + providerBadgeIsAppleCompletionActive ? Color.green.opacity(0.16) : Color.secondary.opacity(0.12) + } + + private var providerBadgeTooltip: String { + "AI Provider for Code Completion" + } + #if os(iOS) private var iOSToolbarChromeStyle: GlassChromeStyle { .single } private var iOSToolbarTintColor: Color { @@ -248,17 +273,17 @@ extension ContentView { @ViewBuilder private var activeProviderBadgeControl: some View { - Text(compactActiveProviderName) + Text(providerBadgeLabelText) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(providerBadgeForegroundColor) .lineLimit(1) .truncationMode(.tail) .minimumScaleFactor(0.9) .fixedSize(horizontal: true, vertical: false) .padding(.horizontal, 6) .padding(.vertical, 2) - .background(Color.secondary.opacity(0.12), in: Capsule()) - .help("Active provider") + .background(providerBadgeBackgroundColor, in: Capsule()) + .help(providerBadgeTooltip) } @ViewBuilder @@ -816,18 +841,18 @@ extension ContentView { .frame(width: 140) .padding(.vertical, 2) - Text(compactActiveProviderName) + Text(providerBadgeLabelText) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(providerBadgeForegroundColor) .lineLimit(1) .truncationMode(.tail) .minimumScaleFactor(0.9) .fixedSize(horizontal: true, vertical: false) .padding(.horizontal, 6) .padding(.vertical, 2) - .background(Color.secondary.opacity(0.12), in: Capsule()) + .background(providerBadgeBackgroundColor, in: Capsule()) .padding(.leading, 6) - .help("Active provider") + .help(providerBadgeTooltip) Button(action: { openSettings() @@ -837,6 +862,30 @@ extension ContentView { } .help("Settings") + #if os(macOS) + Button(action: { + showMarkdownPreviewPane.toggle() + }) { + Label("Markdown Preview", systemImage: showMarkdownPreviewPane ? "doc.richtext.fill" : "doc.richtext") + .foregroundStyle(NeonUIStyle.accentBlue) + } + .disabled(currentLanguage != "markdown") + .help("Toggle Markdown Preview") + + if showMarkdownPreviewPane && currentLanguage == "markdown" { + Menu { + Button("Default") { markdownPreviewTemplateRaw = "default" } + Button("Docs") { markdownPreviewTemplateRaw = "docs" } + Button("Article") { markdownPreviewTemplateRaw = "article" } + Button("Compact") { markdownPreviewTemplateRaw = "compact" } + } label: { + Label("Preview Style", systemImage: "textformat.size") + .foregroundStyle(NeonUIStyle.accentBlue) + } + .help("Markdown Preview Template") + } + #endif + Button(action: { undoFromToolbar() }) { Label("Undo", systemImage: "arrow.uturn.backward") .foregroundStyle(NeonUIStyle.accentBlue) @@ -854,6 +903,37 @@ extension ContentView { .help("Check for Updates") } + Button(action: { openFileFromToolbar() }) { + Label("Open", systemImage: "folder") + .foregroundStyle(NeonUIStyle.accentBlue) + } + .help("Open File… (Cmd+O)") + + Button(action: { + saveCurrentTabFromToolbar() + }) { + Label("Save", systemImage: "square.and.arrow.down") + .foregroundStyle(NeonUIStyle.accentBlue) + } + .disabled(viewModel.selectedTab == nil) + .help("Save File (Cmd+S)") + + Button(action: { viewModel.addNewTab() }) { + Label("New Tab", systemImage: "plus.square.on.square") + .foregroundStyle(NeonUIStyle.accentBlue) + } + .help("New Tab (Cmd+T)") + + #if os(macOS) + Button(action: { + openWindow(id: "blank-window") + }) { + Label("New Window", systemImage: "macwindow.badge.plus") + .foregroundStyle(NeonUIStyle.accentBlue) + } + .help("New Window (Cmd+N)") + #endif + Button(action: { adjustEditorFontSize(-1) }) { Label("Font -", systemImage: "textformat.size.smaller") .foregroundStyle(NeonUIStyle.accentBlue) @@ -882,37 +962,6 @@ extension ContentView { } .help("Insert Template for Current Language") - Button(action: { openFileFromToolbar() }) { - Label("Open", systemImage: "folder") - .foregroundStyle(NeonUIStyle.accentBlue) - } - .help("Open File… (Cmd+O)") - - Button(action: { viewModel.addNewTab() }) { - Label("New Tab", systemImage: "plus.square.on.square") - .foregroundStyle(NeonUIStyle.accentBlue) - } - .help("New Tab (Cmd+T)") - - #if os(macOS) - Button(action: { - openWindow(id: "blank-window") - }) { - Label("New Window", systemImage: "macwindow.badge.plus") - .foregroundStyle(NeonUIStyle.accentBlue) - } - .help("New Window (Cmd+N)") - #endif - - Button(action: { - saveCurrentTabFromToolbar() - }) { - Label("Save", systemImage: "square.and.arrow.down") - .foregroundStyle(NeonUIStyle.accentBlue) - } - .disabled(viewModel.selectedTab == nil) - .help("Save File (Cmd+S)") - Button(action: { toggleSidebarFromToolbar() }) { diff --git a/Neon Vision Editor/UI/ContentView.swift b/Neon Vision Editor/UI/ContentView.swift index a04c2f8..3452720 100644 --- a/Neon Vision Editor/UI/ContentView.swift +++ b/Neon Vision Editor/UI/ContentView.swift @@ -250,6 +250,8 @@ struct ContentView: View { #if os(macOS) @State private var hostWindowNumber: Int? = nil @AppStorage("ShowBracketHelperBarMac") var showBracketHelperBarMac: Bool = false + @State var showMarkdownPreviewPane: Bool = false + @AppStorage("MarkdownPreviewTemplateMac") var markdownPreviewTemplateRaw: String = "default" @State private var windowCloseConfirmationDelegate: WindowCloseConfirmationDelegate? = nil #endif @State private var showLanguageSetupPrompt: Bool = false @@ -2835,6 +2837,14 @@ struct ContentView: View { alignment: brainDumpLayoutEnabled ? .top : .topLeading ) +#if os(macOS) + if showMarkdownPreviewPane && currentLanguage == "markdown" && !brainDumpLayoutEnabled { + Divider() + markdownPreviewPane + .frame(minWidth: 280, idealWidth: 420, maxWidth: 680, maxHeight: .infinity) + } +#endif + if showProjectStructureSidebar && !brainDumpLayoutEnabled { #if os(macOS) VStack(spacing: 0) { @@ -2966,6 +2976,13 @@ struct ContentView: View { .padding(.trailing, 12) } } +#if os(macOS) + .onChange(of: currentLanguage) { _, newLanguage in + if newLanguage != "markdown", showMarkdownPreviewPane { + showMarkdownPreviewPane = false + } + } +#endif #if os(macOS) .toolbarBackground( macChromeBackgroundStyle, @@ -2983,6 +3000,444 @@ struct ContentView: View { #endif } +#if os(macOS) + @ViewBuilder + private var markdownPreviewPane: some View { + VStack(alignment: .leading, spacing: 0) { + HStack { + Text("Markdown Preview") + .font(.headline) + Spacer() + Picker("Template", selection: $markdownPreviewTemplateRaw) { + Text("Default").tag("default") + Text("Docs").tag("docs") + Text("Article").tag("article") + Text("Compact").tag("compact") + } + .labelsHidden() + .pickerStyle(.menu) + .frame(width: 120) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background(editorSurfaceBackgroundStyle) + + MarkdownPreviewWebView(html: markdownPreviewHTML(from: currentContent)) + .accessibilityLabel("Markdown Preview Content") + } + .background(editorSurfaceBackgroundStyle) + } + + private var markdownPreviewTemplate: String { + switch markdownPreviewTemplateRaw { + case "docs", "article", "compact": + return markdownPreviewTemplateRaw + default: + return "default" + } + } + + private func markdownPreviewHTML(from markdownText: String) -> String { + let bodyHTML = renderedMarkdownBodyHTML(from: markdownText) ?? "
\(escapedHTML(markdownText))" + return """ + + + + + + + + +
\(paragraph)
") + paragraphLines.removeAll(keepingCapacity: true) + } + + func closeLists() { + if insideUnorderedList { + result.append("") + insideUnorderedList = false + } + if insideOrderedList { + result.append("") + insideOrderedList = false + } + } + + func closeBlockquote() { + if insideBlockquote { + flushParagraph() + closeLists() + result.append("") + insideBlockquote = false + } + } + + func closeParagraphAndInlineContainers() { + flushParagraph() + closeLists() + } + + for rawLine in lines { + let line = rawLine + let trimmed = line.trimmingCharacters(in: .whitespaces) + + if trimmed.hasPrefix("```") { + if insideCodeFence { + result.append("") + insideCodeFence = false + codeFenceLanguage = nil + } else { + closeBlockquote() + closeParagraphAndInlineContainers() + insideCodeFence = true + let lang = String(trimmed.dropFirst(3)).trimmingCharacters(in: .whitespaces) + codeFenceLanguage = lang.isEmpty ? nil : lang + if let codeFenceLanguage { + result.append("")
+ } else {
+ result.append("")
+ }
+ }
+ continue
+ }
+
+ if insideCodeFence {
+ result.append("\(escapedHTML(line))\n")
+ continue
+ }
+
+ if trimmed.isEmpty {
+ closeParagraphAndInlineContainers()
+ closeBlockquote()
+ continue
+ }
+
+ if let heading = markdownHeading(from: trimmed) {
+ closeBlockquote()
+ closeParagraphAndInlineContainers()
+ result.append("\(inlineMarkdownToHTML(heading.text)) ")
+ continue
+ }
+
+ if isMarkdownHorizontalRule(trimmed) {
+ closeBlockquote()
+ closeParagraphAndInlineContainers()
+ result.append("
")
+ continue
+ }
+
+ var workingLine = trimmed
+ let isBlockquoteLine = workingLine.hasPrefix(">")
+ if isBlockquoteLine {
+ if !insideBlockquote {
+ closeParagraphAndInlineContainers()
+ result.append("")
+ insideBlockquote = true
+ }
+ workingLine = workingLine.dropFirst().trimmingCharacters(in: .whitespaces)
+ } else {
+ closeBlockquote()
+ }
+
+ if let unordered = markdownUnorderedListItem(from: workingLine) {
+ flushParagraph()
+ if insideOrderedList {
+ result.append("")
+ insideOrderedList = false
+ }
+ if !insideUnorderedList {
+ result.append("")
+ insideUnorderedList = true
+ }
+ result.append("- \(inlineMarkdownToHTML(unordered))
")
+ continue
+ }
+
+ if let ordered = markdownOrderedListItem(from: workingLine) {
+ flushParagraph()
+ if insideUnorderedList {
+ result.append("
")
+ insideUnorderedList = false
+ }
+ if !insideOrderedList {
+ result.append("")
+ insideOrderedList = true
+ }
+ result.append("- \(inlineMarkdownToHTML(ordered))
")
+ continue
+ }
+
+ closeLists()
+ paragraphLines.append(workingLine)
+ }
+
+ closeBlockquote()
+ closeParagraphAndInlineContainers()
+ if insideCodeFence {
+ result.append("
")
+ }
+ return result.joined(separator: "\n")
+ }
+
+ private func markdownHeading(from line: String) -> (level: Int, text: String)? {
+ let pattern = "^(#{1,6})\\s+(.+)$"
+ guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
+ let range = NSRange(line.startIndex..., in: line)
+ guard let match = regex.firstMatch(in: line, options: [], range: range),
+ let hashesRange = Range(match.range(at: 1), in: line),
+ let textRange = Range(match.range(at: 2), in: line) else {
+ return nil
+ }
+ return (line[hashesRange].count, String(line[textRange]))
+ }
+
+ private func isMarkdownHorizontalRule(_ line: String) -> Bool {
+ let compact = line.replacingOccurrences(of: " ", with: "")
+ return compact == "***" || compact == "---" || compact == "___"
+ }
+
+ private func markdownUnorderedListItem(from line: String) -> String? {
+ let pattern = "^[-*+]\\s+(.+)$"
+ guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
+ let range = NSRange(line.startIndex..., in: line)
+ guard let match = regex.firstMatch(in: line, options: [], range: range),
+ let textRange = Range(match.range(at: 1), in: line) else {
+ return nil
+ }
+ return String(line[textRange])
+ }
+
+ private func markdownOrderedListItem(from line: String) -> String? {
+ let pattern = "^\\d+[\\.)]\\s+(.+)$"
+ guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
+ let range = NSRange(line.startIndex..., in: line)
+ guard let match = regex.firstMatch(in: line, options: [], range: range),
+ let textRange = Range(match.range(at: 1), in: line) else {
+ return nil
+ }
+ return String(line[textRange])
+ }
+
+ private func inlineMarkdownToHTML(_ text: String) -> String {
+ var html = escapedHTML(text)
+ var codeSpans: [String] = []
+
+ html = replacingRegex(in: html, pattern: "`([^`]+)`") { match in
+ let content = String(match.dropFirst().dropLast())
+ let token = "__CODE_SPAN_\(codeSpans.count)__"
+ codeSpans.append("\(content)")
+ return token
+ }
+
+ html = replacingRegex(in: html, pattern: "!\\[([^\\]]*)\\]\\(([^\\)\\s]+)\\)") { match in
+ let parts = captureGroups(in: match, pattern: "!\\[([^\\]]*)\\]\\(([^\\)\\s]+)\\)")
+ guard parts.count == 2 else { return match }
+ return "
"
+ }
+
+ html = replacingRegex(in: html, pattern: "\\[([^\\]]+)\\]\\(([^\\)\\s]+)\\)") { match in
+ let parts = captureGroups(in: match, pattern: "\\[([^\\]]+)\\]\\(([^\\)\\s]+)\\)")
+ guard parts.count == 2 else { return match }
+ return "\(parts[0])"
+ }
+
+ html = replacingRegex(in: html, pattern: "\\*\\*([^*]+)\\*\\*") { "\(String($0.dropFirst(2).dropLast(2)))" }
+ html = replacingRegex(in: html, pattern: "__([^_]+)__") { "\(String($0.dropFirst(2).dropLast(2)))" }
+ html = replacingRegex(in: html, pattern: "\\*([^*]+)\\*") { "\(String($0.dropFirst().dropLast()))" }
+ html = replacingRegex(in: html, pattern: "_([^_]+)_") { "\(String($0.dropFirst().dropLast()))" }
+
+ for (index, codeHTML) in codeSpans.enumerated() {
+ html = html.replacingOccurrences(of: "__CODE_SPAN_\(index)__", with: codeHTML)
+ }
+ return html
+ }
+
+ private func replacingRegex(in text: String, pattern: String, transform: (String) -> String) -> String {
+ guard let regex = try? NSRegularExpression(pattern: pattern) else { return text }
+ let matches = regex.matches(in: text, options: [], range: NSRange(text.startIndex..., in: text))
+ guard !matches.isEmpty else { return text }
+
+ var output = text
+ for match in matches.reversed() {
+ guard let range = Range(match.range, in: output) else { continue }
+ let segment = String(output[range])
+ output.replaceSubrange(range, with: transform(segment))
+ }
+ return output
+ }
+
+ private func captureGroups(in text: String, pattern: String) -> [String] {
+ guard let regex = try? NSRegularExpression(pattern: pattern),
+ let match = regex.firstMatch(in: text, options: [], range: NSRange(text.startIndex..., in: text)) else {
+ return []
+ }
+ var groups: [String] = []
+ for idx in 1.. String {
+ let basePadding: String
+ let fontSize: String
+ let lineHeight: String
+ let maxWidth: String
+ switch template {
+ case "docs":
+ basePadding = "22px 30px"
+ fontSize = "15px"
+ lineHeight = "1.7"
+ maxWidth = "900px"
+ case "article":
+ basePadding = "32px 48px"
+ fontSize = "17px"
+ lineHeight = "1.8"
+ maxWidth = "760px"
+ case "compact":
+ basePadding = "14px 16px"
+ fontSize = "13px"
+ lineHeight = "1.5"
+ maxWidth = "none"
+ default:
+ basePadding = "18px 22px"
+ fontSize = "14px"
+ lineHeight = "1.6"
+ maxWidth = "860px"
+ }
+
+ return """
+ :root { color-scheme: light dark; }
+ html, body {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
+ font-size: \(fontSize);
+ line-height: \(lineHeight);
+ }
+ .content {
+ max-width: \(maxWidth);
+ padding: \(basePadding);
+ margin: 0 auto;
+ }
+ h1, h2, h3, h4, h5, h6 {
+ line-height: 1.25;
+ margin: 1.1em 0 0.55em;
+ font-weight: 700;
+ }
+ h1 { font-size: 1.85em; border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent); padding-bottom: 0.25em; }
+ h2 { font-size: 1.45em; border-bottom: 1px solid color-mix(in srgb, currentColor 13%, transparent); padding-bottom: 0.2em; }
+ h3 { font-size: 1.2em; }
+ p, ul, ol, blockquote, table, pre { margin: 0.65em 0; }
+ ul, ol { padding-left: 1.3em; }
+ li { margin: 0.2em 0; }
+ blockquote {
+ margin-left: 0;
+ padding: 0.45em 0.9em;
+ border-left: 3px solid color-mix(in srgb, currentColor 30%, transparent);
+ background: color-mix(in srgb, currentColor 6%, transparent);
+ border-radius: 6px;
+ }
+ code {
+ font-family: "SF Mono", "Menlo", "Monaco", monospace;
+ font-size: 0.9em;
+ padding: 0.12em 0.35em;
+ border-radius: 5px;
+ background: color-mix(in srgb, currentColor 10%, transparent);
+ }
+ pre {
+ overflow-x: auto;
+ padding: 0.8em 0.95em;
+ border-radius: 9px;
+ background: color-mix(in srgb, currentColor 8%, transparent);
+ border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
+ line-height: 1.35;
+ white-space: pre;
+ }
+ pre code {
+ display: block;
+ padding: 0;
+ background: transparent;
+ border-radius: 0;
+ font-size: 0.88em;
+ line-height: 1.35;
+ white-space: pre;
+ }
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
+ border-radius: 8px;
+ overflow: hidden;
+ }
+ th, td {
+ text-align: left;
+ padding: 0.45em 0.55em;
+ border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent);
+ }
+ th {
+ background: color-mix(in srgb, currentColor 7%, transparent);
+ font-weight: 600;
+ }
+ a {
+ color: #2f7cf6;
+ text-decoration: none;
+ border-bottom: 1px solid color-mix(in srgb, #2f7cf6 45%, transparent);
+ }
+ img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 8px;
+ }
+ hr {
+ border: 0;
+ border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent);
+ margin: 1.1em 0;
+ }
+ """
+ }
+
+ private func escapedHTML(_ text: String) -> String {
+ text
+ .replacingOccurrences(of: "&", with: "&")
+ .replacingOccurrences(of: "<", with: "<")
+ .replacingOccurrences(of: ">", with: ">")
+ .replacingOccurrences(of: "\"", with: """)
+ .replacingOccurrences(of: "'", with: "'")
+ }
+#endif
+
#if os(iOS)
@ViewBuilder
private var iPhoneUnifiedTopChromeHost: some View {
diff --git a/Neon Vision Editor/UI/EditorTextView.swift b/Neon Vision Editor/UI/EditorTextView.swift
index 61f4f33..c422170 100644
--- a/Neon Vision Editor/UI/EditorTextView.swift
+++ b/Neon Vision Editor/UI/EditorTextView.swift
@@ -16,6 +16,21 @@ private enum EditorRuntimeLimits {
static let bindingDebounceDelay: TimeInterval = 0.18
}
+#if os(macOS)
+private func replaceTextPreservingSelectionAndFocus(_ textView: NSTextView, with newText: String) {
+ let previousSelection = textView.selectedRange()
+ let hadFocus = (textView.window?.firstResponder as? NSTextView) === textView
+ textView.string = newText
+ let length = (newText as NSString).length
+ let safeLocation = min(max(0, previousSelection.location), length)
+ let safeLength = min(max(0, previousSelection.length), max(0, length - safeLocation))
+ textView.setSelectedRange(NSRange(location: safeLocation, length: safeLength))
+ if hadFocus {
+ textView.window?.makeFirstResponder(textView)
+ }
+}
+#endif
+
private enum EmmetExpander {
struct Node {
var tag: String
@@ -1141,8 +1156,8 @@ final class AcceptingTextView: NSTextView {
}
return
}
- // After paste, jump back to the first line.
- pendingPasteCaretLocation = 0
+ // Keep caret anchored at the current insertion location while paste async work settles.
+ pendingPasteCaretLocation = selectedRange().location
if let raw = pasteboardPlainString(from: pasteboard), !raw.isEmpty {
if let pathURL = fileURLFromString(raw) {
@@ -1872,12 +1887,18 @@ struct CustomTextEditor: NSViewRepresentable {
// Sanitize and avoid publishing binding during update
let target = sanitizedForExternalSet(text)
if textView.string != target {
- context.coordinator.cancelPendingBindingSync()
- textView.string = target
- context.coordinator.invalidateHighlightCache()
- DispatchQueue.main.async {
- if self.text != target {
- self.text = target
+ let hasFocus = (textView.window?.firstResponder as? NSTextView) === textView
+ let shouldPreferEditorBuffer = hasFocus && !isTabLoadingContent
+ if shouldPreferEditorBuffer {
+ context.coordinator.syncBindingTextImmediately(textView.string)
+ } else {
+ context.coordinator.cancelPendingBindingSync()
+ replaceTextPreservingSelectionAndFocus(textView, with: target)
+ context.coordinator.invalidateHighlightCache()
+ DispatchQueue.main.async {
+ if self.text != target {
+ self.text = target
+ }
}
}
}
@@ -1911,7 +1932,7 @@ struct CustomTextEditor: NSViewRepresentable {
if currentLength <= 300_000 {
let sanitized = AcceptingTextView.sanitizePlainText(textView.string)
if sanitized != textView.string {
- textView.string = sanitized
+ replaceTextPreservingSelectionAndFocus(textView, with: sanitized)
context.coordinator.invalidateHighlightCache()
DispatchQueue.main.async {
if self.text != sanitized {
@@ -2031,6 +2052,7 @@ struct CustomTextEditor: NSViewRepresentable {
private var pendingEditedRange: NSRange?
private var pendingBindingSync: DispatchWorkItem?
var lastAppliedWrapMode: Bool?
+ var hasPendingBindingSync: Bool { pendingBindingSync != nil }
init(_ parent: CustomTextEditor) {
self.parent = parent
@@ -2057,12 +2079,14 @@ struct CustomTextEditor: NSViewRepresentable {
return
}
pendingBindingSync?.cancel()
+ pendingBindingSync = nil
if immediate || (text as NSString).length < EditorRuntimeLimits.bindingDebounceUTF16Length {
parent.text = text
return
}
let work = DispatchWorkItem { [weak self] in
guard let self else { return }
+ self.pendingBindingSync = nil
if self.textView?.string == text {
self.parent.text = text
}
@@ -2073,6 +2097,11 @@ struct CustomTextEditor: NSViewRepresentable {
func cancelPendingBindingSync() {
pendingBindingSync?.cancel()
+ pendingBindingSync = nil
+ }
+
+ func syncBindingTextImmediately(_ text: String) {
+ syncBindingText(text, immediate: true)
}
func scheduleHighlightIfNeeded(currentText: String? = nil, immediate: Bool = false) {
@@ -2422,7 +2451,7 @@ struct CustomTextEditor: NSViewRepresentable {
sanitized = AcceptingTextView.sanitizePlainText(currentText)
}
if sanitized != currentText {
- textView.string = sanitized
+ replaceTextPreservingSelectionAndFocus(textView, with: sanitized)
}
let normalizedStyle = NSMutableParagraphStyle()
normalizedStyle.lineHeightMultiple = max(0.9, parent.lineHeightMultiple)
@@ -2455,7 +2484,7 @@ struct CustomTextEditor: NSViewRepresentable {
let caretLocation = min(nsText.length, textView.selectedRange().location)
pendingEditedRange = nsText.lineRange(for: NSRange(location: caretLocation, length: 0))
updateCaretStatusAndHighlight(triggerHighlight: false)
- scheduleHighlightIfNeeded(currentText: parent.text)
+ scheduleHighlightIfNeeded(currentText: sanitized)
}
func textViewDidChangeSelection(_ notification: Notification) {
@@ -2934,12 +2963,14 @@ struct CustomTextEditor: UIViewRepresentable {
let textView = uiView.textView
context.coordinator.parent = self
if textView.text != text {
- context.coordinator.cancelPendingBindingSync()
- let priorSelection = textView.selectedRange
- let priorOffset = textView.contentOffset
- let wasFirstResponder = textView.isFirstResponder
- textView.text = text
- if wasFirstResponder {
+ let shouldPreferEditorBuffer = textView.isFirstResponder && !isTabLoadingContent
+ if shouldPreferEditorBuffer {
+ context.coordinator.syncBindingTextImmediately(textView.text)
+ } else {
+ context.coordinator.cancelPendingBindingSync()
+ let priorSelection = textView.selectedRange
+ let priorOffset = textView.contentOffset
+ textView.text = text
let length = (textView.text as NSString).length
let clampedLocation = min(priorSelection.location, length)
let clampedLength = min(priorSelection.length, max(0, length - clampedLocation))
@@ -3003,6 +3034,7 @@ struct CustomTextEditor: UIViewRepresentable {
private var lastTranslucencyEnabled: Bool?
private var isApplyingHighlight = false
private var highlightGeneration: Int = 0
+ var hasPendingBindingSync: Bool { pendingBindingSync != nil }
init(_ parent: CustomTextEditor) {
self.parent = parent
@@ -3027,12 +3059,14 @@ struct CustomTextEditor: UIViewRepresentable {
return
}
pendingBindingSync?.cancel()
+ pendingBindingSync = nil
if immediate || (text as NSString).length < EditorRuntimeLimits.bindingDebounceUTF16Length {
parent.text = text
return
}
let work = DispatchWorkItem { [weak self] in
guard let self else { return }
+ self.pendingBindingSync = nil
if self.textView?.text == text {
self.parent.text = text
}
@@ -3043,6 +3077,11 @@ struct CustomTextEditor: UIViewRepresentable {
func cancelPendingBindingSync() {
pendingBindingSync?.cancel()
+ pendingBindingSync = nil
+ }
+
+ func syncBindingTextImmediately(_ text: String) {
+ syncBindingText(text, immediate: true)
}
@objc private func updateKeyboardAccessoryVisibility(_ notification: Notification) {
diff --git a/Neon Vision Editor/UI/MarkdownPreviewWebView.swift b/Neon Vision Editor/UI/MarkdownPreviewWebView.swift
new file mode 100644
index 0000000..f2845bd
--- /dev/null
+++ b/Neon Vision Editor/UI/MarkdownPreviewWebView.swift
@@ -0,0 +1,33 @@
+#if os(macOS)
+import SwiftUI
+import WebKit
+
+struct MarkdownPreviewWebView: NSViewRepresentable {
+ let html: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator()
+ }
+
+ func makeNSView(context: Context) -> WKWebView {
+ let configuration = WKWebViewConfiguration()
+ configuration.defaultWebpagePreferences.allowsContentJavaScript = false
+ let webView = WKWebView(frame: .zero, configuration: configuration)
+ webView.setValue(false, forKey: "drawsBackground")
+ webView.allowsBackForwardNavigationGestures = false
+ webView.loadHTMLString(html, baseURL: nil)
+ context.coordinator.lastHTML = html
+ return webView
+ }
+
+ func updateNSView(_ webView: WKWebView, context: Context) {
+ guard context.coordinator.lastHTML != html else { return }
+ webView.loadHTMLString(html, baseURL: nil)
+ context.coordinator.lastHTML = html
+ }
+
+ final class Coordinator {
+ var lastHTML: String = ""
+ }
+}
+#endif
diff --git a/Neon Vision Editor/UI/PanelsAndHelpers.swift b/Neon Vision Editor/UI/PanelsAndHelpers.swift
index 75f6d34..5592169 100644
--- a/Neon Vision Editor/UI/PanelsAndHelpers.swift
+++ b/Neon Vision Editor/UI/PanelsAndHelpers.swift
@@ -242,12 +242,11 @@ struct WelcomeTourView: View {
private let pages: [TourPage] = [
TourPage(
title: "What’s New in This Release",
- subtitle: "Major changes since v0.4.28:",
+ subtitle: "Major changes since v0.4.29:",
bullets: [
- "Added explicit English (`en`) and German (`de`) support strings for the Support/IAP settings surface to keep release copy consistent across locales.",
- "Added support-price freshness state with a visible “Last updated” timestamp in Support settings after successful App Store product refreshes.",
- "Improved updater version normalization so release tags with suffix metadata (for example `+build`, `(build 123)`, or prefixed release labels) are compared using the semantic core version.",
- "Improved Support settings refresh UX with a loading spinner on the “Retry App Store” action and clearer status messaging when price data is temporarily unavailable."
+ "TODO",
+ "TODO",
+ "TODO"
],
iconName: "sparkles.rectangle.stack",
colors: [Color(red: 0.40, green: 0.28, blue: 0.90), Color(red: 0.96, green: 0.46, blue: 0.55)],
diff --git a/Neon Vision Editor/de.lproj/Localizable.strings b/Neon Vision Editor/de.lproj/Localizable.strings
index a67ecaa..dbea620 100644
--- a/Neon Vision Editor/de.lproj/Localizable.strings
+++ b/Neon Vision Editor/de.lproj/Localizable.strings
@@ -34,3 +34,126 @@
"Purchase did not complete." = "Der Kauf wurde nicht abgeschlossen.";
"Purchase failed: %@" = "Kauf fehlgeschlagen: %@";
"Transaction verification failed." = "Transaktionsprüfung fehlgeschlagen.";
+
+"General" = "Allgemein";
+"Editor" = "Editor";
+"Templates" = "Vorlagen";
+"Themes" = "Themes";
+"More" = "Mehr";
+"AI" = "KI";
+"Updates" = "Updates";
+"AI Model" = "KI-Modell";
+"Appearance" = "Darstellung";
+"Display" = "Anzeige";
+"Font" = "Schrift";
+"Editor Font" = "Editor-Schrift";
+"Font Size" = "Schriftgröße";
+"Line Height" = "Zeilenhöhe";
+"Language" = "Sprache";
+"Default New File Language" = "Standardsprache für neue Dateien";
+"Startup" = "Start";
+"Open in Tabs" = "In Tabs öffnen";
+"Open with Blank Document" = "Mit leerem Dokument öffnen";
+"Reopen Last Session" = "Letzte Sitzung wiederherstellen";
+"Translucency" = "Transluzenz";
+"Translucency Mode" = "Transluzenzmodus";
+"Translucent Window" = "Transparentes Fenster";
+"Translucent Window Background" = "Transparenter Fensterhintergrund";
+"Follow System" = "System folgen";
+"Always" = "Immer";
+"Never" = "Nie";
+"System" = "System";
+"Light" = "Hell";
+"Dark" = "Dunkel";
+"Completion" = "Vervollständigung";
+"Enable Completion" = "Vervollständigung aktivieren";
+"Include Syntax Keywords" = "Syntax-Schlüsselwörter einbeziehen";
+"Include Words in Document" = "Wörter im Dokument einbeziehen";
+"For lower latency on large files, keep only one completion source enabled." = "Für geringere Latenz bei großen Dateien sollte nur eine Vervollständigungsquelle aktiv sein.";
+"The selected AI model is used for AI-assisted code completion." = "Das ausgewählte KI-Modell wird für KI-gestützte Code-Vervollständigung verwendet.";
+"Apple Intelligence" = "Apple Intelligence";
+"Grok" = "Grok";
+"OpenAI" = "OpenAI";
+"Gemini" = "Gemini";
+"Anthropic" = "Anthropic";
+"AI Provider API Keys" = "API-Schlüssel der KI-Anbieter";
+"Data Disclosure" = "Datentransparenz";
+"AI Data Disclosure" = "KI-Datentransparenz";
+"Close" = "Schließen";
+"Done" = "Fertig";
+"Cancel" = "Abbrechen";
+"Save" = "Speichern";
+"Don't Save" = "Nicht speichern";
+"OK" = "OK";
+"Software Update" = "Software-Update";
+"Checks GitHub releases for Neon Vision Editor updates." = "Prüft GitHub-Releases auf Neon Vision Editor-Updates.";
+"Show Installer Log" = "Installer-Log anzeigen";
+"Checking for updates…" = "Suche nach Updates…";
+"You’re up to date." = "Du bist auf dem neuesten Stand.";
+"Update check failed" = "Update-Prüfung fehlgeschlagen";
+"Unknown error" = "Unbekannter Fehler";
+"%@ is available" = "%@ ist verfügbar";
+"Current version: %@" = "Aktuelle Version: %@";
+"Current version: %@ • New version: %@" = "Aktuelle Version: %@ • Neue Version: %@";
+"Published: %@" = "Veröffentlicht: %@";
+"Installing update…" = "Update wird installiert…";
+"%d%%" = "%d%%";
+"Updates are delivered from GitHub release assets, not App Store updates." = "Updates werden über GitHub-Release-Assets bereitgestellt, nicht über App-Store-Updates.";
+"Later" = "Später";
+"Install and Close App" = "Installieren und App schließen";
+"Restart and Install" = "Neustarten und installieren";
+"View Releases" = "Releases ansehen";
+"Skip This Version" = "Diese Version überspringen";
+"Remind Me Tomorrow" = "Morgen erinnern";
+"Install Update" = "Update installieren";
+"Try Again" = "Erneut versuchen";
+"Check Again" = "Erneut prüfen";
+"Check Now" = "Jetzt prüfen";
+"GitHub Release Updates" = "GitHub-Release-Updates";
+"Automatically check for updates" = "Automatisch nach Updates suchen";
+"Check Interval" = "Prüfintervall";
+"Automatically install updates when available" = "Updates automatisch installieren, wenn verfügbar";
+"Last checked: %@" = "Zuletzt geprüft: %@";
+"Last check result: %@" = "Letztes Prüfergebnis: %@";
+"Auto-check pause active until %@ (%lld consecutive failures)." = "Automatische Prüfung pausiert bis %@ (%lld aufeinanderfolgende Fehler).";
+"Uses GitHub release assets only. App Store Connect releases are not used by this updater." = "Dieser Updater nutzt ausschließlich GitHub-Release-Assets. App-Store-Connect-Releases werden nicht verwendet.";
+"Find & Replace" = "Suchen & Ersetzen";
+"Find" = "Suchen";
+"Replace" = "Ersetzen";
+"Replace All" = "Alle ersetzen";
+"Case Sensitive" = "Groß-/Kleinschreibung beachten";
+"Use Regex" = "Regex verwenden";
+"Quick Open" = "Schnell öffnen";
+"No folder selected" = "Kein Ordner ausgewählt";
+"Folder is empty" = "Ordner ist leer";
+"Project Structure" = "Projektstruktur";
+"Show Line Numbers" = "Zeilennummern anzeigen";
+"Toolbar buttons" = "Toolbar-Schaltflächen";
+"scroll for viewing all toolbar options." = "scrollen, um alle Toolbar-Optionen zu sehen.";
+"This file has unsaved changes." = "Diese Datei enthält ungespeicherte Änderungen.";
+"This will remove all text in the current editor." = "Dadurch wird der gesamte Text im aktuellen Editor entfernt.";
+"Untitled 1" = "Unbenannt 1";
+"Large File Mode" = "Großdatei-Modus";
+"Theme Colors" = "Theme-Farben";
+"Base" = "Basis";
+"Syntax" = "Syntax";
+"Preview" = "Vorschau";
+"Tip: Enable only one startup mode to keep app launch behavior predictable." = "Tipp: Aktiviere nur einen Startmodus, damit das Startverhalten der App vorhersehbar bleibt.";
+"When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts." = "Wenn Zeilenumbruch aktiv ist, werden Scope-Guides/Scope-Bereich deaktiviert, um Layoutkonflikte zu vermeiden.";
+"Scope guides are intended for non-Swift languages. Swift favors matching-token highlight." = "Scope-Guides sind für Nicht-Swift-Sprachen gedacht. Für Swift wird die Hervorhebung passender Tokens bevorzugt.";
+"Invisible character markers are disabled to avoid whitespace glyph artifacts." = "Markierungen für unsichtbare Zeichen sind deaktiviert, um Whitespace-Artefakte zu vermeiden.";
+"Indentation" = "Einrückung";
+"Spaces" = "Leerzeichen";
+"Tabs" = "Tabs";
+"Indent Width: %lld" = "Einrückungsbreite: %lld";
+"Choose a language for code completion" = "Wähle eine Sprache für die Code-Vervollständigung";
+"You can change this later from the Language picker." = "Du kannst dies später im Sprachwähler ändern.";
+"Purchase is available in App Store/TestFlight builds." = "Kauf ist in App-Store-/TestFlight-Builds verfügbar.";
+"Send Support Tip — %@" = "Support-Tipp senden — %@";
+"Send Tip %@" = "Tipp senden %@";
+"The application does not collect analytics data, usage telemetry, advertising identifiers, device fingerprints, or background behavioral metrics. No automatic data transmission to developer-controlled servers occurs." = "Die Anwendung erfasst keine Analysedaten, Nutzungs-Telemetrie, Werbe-IDs, Geräte-Fingerprints oder Verhaltensmetriken im Hintergrund. Es erfolgt keine automatische Datenübertragung an vom Entwickler kontrollierte Server.";
+"AI-assisted code completion is an optional feature. External network communication only occurs when a user explicitly enables AI completion and selects an external AI provider within the application settings." = "KI-gestützte Code-Vervollständigung ist eine optionale Funktion. Externe Netzwerkkommunikation erfolgt nur, wenn ein Benutzer KI-Vervollständigung explizit aktiviert und in den Einstellungen einen externen KI-Anbieter auswählt.";
+"When AI completion is triggered, the application transmits only the minimal contextual text necessary to generate a completion suggestion. This typically includes the code immediately surrounding the cursor position or the active selection." = "Wenn KI-Vervollständigung ausgelöst wird, übermittelt die Anwendung nur den minimal notwendigen Kontexttext zur Erzeugung eines Vorschlags. Dazu gehört typischerweise der Code direkt um die Cursorposition oder die aktive Auswahl.";
+"The application does not automatically transmit full project folders, unrelated files, entire file system contents, contact data, location data, or device-specific identifiers." = "Die Anwendung überträgt nicht automatisch ganze Projektordner, nicht zusammenhängende Dateien, vollständige Dateisysteminhalte, Kontaktdaten, Standortdaten oder gerätespezifische Kennungen.";
+"Authentication credentials (API keys) for external AI providers are stored securely in the system keychain and are transmitted only to the user-selected provider for the purpose of completing the AI request." = "Authentifizierungsdaten (API-Schlüssel) für externe KI-Anbieter werden sicher im System-Schlüsselbund gespeichert und nur an den vom Benutzer gewählten Anbieter zur Bearbeitung der KI-Anfrage übertragen.";
+"All external communication is performed over encrypted HTTPS connections. If AI completion is disabled, the application performs no external AI-related network requests." = "Sämtliche externe Kommunikation erfolgt über verschlüsselte HTTPS-Verbindungen. Wenn KI-Vervollständigung deaktiviert ist, führt die Anwendung keine externen KI-bezogenen Netzwerkanfragen aus.";
diff --git a/Neon Vision Editor/en.lproj/Localizable.strings b/Neon Vision Editor/en.lproj/Localizable.strings
index b349eba..8ec7246 100644
--- a/Neon Vision Editor/en.lproj/Localizable.strings
+++ b/Neon Vision Editor/en.lproj/Localizable.strings
@@ -34,3 +34,126 @@
"Purchase did not complete." = "Purchase did not complete.";
"Purchase failed: %@" = "Purchase failed: %@";
"Transaction verification failed." = "Transaction verification failed.";
+
+"General" = "General";
+"Editor" = "Editor";
+"Templates" = "Templates";
+"Themes" = "Themes";
+"More" = "More";
+"AI" = "AI";
+"Updates" = "Updates";
+"AI Model" = "AI Model";
+"Appearance" = "Appearance";
+"Display" = "Display";
+"Font" = "Font";
+"Editor Font" = "Editor Font";
+"Font Size" = "Font Size";
+"Line Height" = "Line Height";
+"Language" = "Language";
+"Default New File Language" = "Default New File Language";
+"Startup" = "Startup";
+"Open in Tabs" = "Open in Tabs";
+"Open with Blank Document" = "Open with Blank Document";
+"Reopen Last Session" = "Reopen Last Session";
+"Translucency" = "Translucency";
+"Translucency Mode" = "Translucency Mode";
+"Translucent Window" = "Translucent Window";
+"Translucent Window Background" = "Translucent Window Background";
+"Follow System" = "Follow System";
+"Always" = "Always";
+"Never" = "Never";
+"System" = "System";
+"Light" = "Light";
+"Dark" = "Dark";
+"Completion" = "Completion";
+"Enable Completion" = "Enable Completion";
+"Include Syntax Keywords" = "Include Syntax Keywords";
+"Include Words in Document" = "Include Words in Document";
+"For lower latency on large files, keep only one completion source enabled." = "For lower latency on large files, keep only one completion source enabled.";
+"The selected AI model is used for AI-assisted code completion." = "The selected AI model is used for AI-assisted code completion.";
+"Apple Intelligence" = "Apple Intelligence";
+"Grok" = "Grok";
+"OpenAI" = "OpenAI";
+"Gemini" = "Gemini";
+"Anthropic" = "Anthropic";
+"AI Provider API Keys" = "AI Provider API Keys";
+"Data Disclosure" = "Data Disclosure";
+"AI Data Disclosure" = "AI Data Disclosure";
+"Close" = "Close";
+"Done" = "Done";
+"Cancel" = "Cancel";
+"Save" = "Save";
+"Don't Save" = "Don't Save";
+"OK" = "OK";
+"Software Update" = "Software Update";
+"Checks GitHub releases for Neon Vision Editor updates." = "Checks GitHub releases for Neon Vision Editor updates.";
+"Show Installer Log" = "Show Installer Log";
+"Checking for updates…" = "Checking for updates…";
+"You’re up to date." = "You’re up to date.";
+"Update check failed" = "Update check failed";
+"Unknown error" = "Unknown error";
+"%@ is available" = "%@ is available";
+"Current version: %@" = "Current version: %@";
+"Current version: %@ • New version: %@" = "Current version: %@ • New version: %@";
+"Published: %@" = "Published: %@";
+"Installing update…" = "Installing update…";
+"%d%%" = "%d%%";
+"Updates are delivered from GitHub release assets, not App Store updates." = "Updates are delivered from GitHub release assets, not App Store updates.";
+"Later" = "Later";
+"Install and Close App" = "Install and Close App";
+"Restart and Install" = "Restart and Install";
+"View Releases" = "View Releases";
+"Skip This Version" = "Skip This Version";
+"Remind Me Tomorrow" = "Remind Me Tomorrow";
+"Install Update" = "Install Update";
+"Try Again" = "Try Again";
+"Check Again" = "Check Again";
+"Check Now" = "Check Now";
+"GitHub Release Updates" = "GitHub Release Updates";
+"Automatically check for updates" = "Automatically check for updates";
+"Check Interval" = "Check Interval";
+"Automatically install updates when available" = "Automatically install updates when available";
+"Last checked: %@" = "Last checked: %@";
+"Last check result: %@" = "Last check result: %@";
+"Auto-check pause active until %@ (%lld consecutive failures)." = "Auto-check pause active until %@ (%lld consecutive failures).";
+"Uses GitHub release assets only. App Store Connect releases are not used by this updater." = "Uses GitHub release assets only. App Store Connect releases are not used by this updater.";
+"Find & Replace" = "Find & Replace";
+"Find" = "Find";
+"Replace" = "Replace";
+"Replace All" = "Replace All";
+"Case Sensitive" = "Case Sensitive";
+"Use Regex" = "Use Regex";
+"Quick Open" = "Quick Open";
+"No folder selected" = "No folder selected";
+"Folder is empty" = "Folder is empty";
+"Project Structure" = "Project Structure";
+"Show Line Numbers" = "Show Line Numbers";
+"Toolbar buttons" = "Toolbar buttons";
+"scroll for viewing all toolbar options." = "scroll for viewing all toolbar options.";
+"This file has unsaved changes." = "This file has unsaved changes.";
+"This will remove all text in the current editor." = "This will remove all text in the current editor.";
+"Untitled 1" = "Untitled 1";
+"Large File Mode" = "Large File Mode";
+"Theme Colors" = "Theme Colors";
+"Base" = "Base";
+"Syntax" = "Syntax";
+"Preview" = "Preview";
+"Tip: Enable only one startup mode to keep app launch behavior predictable." = "Tip: Enable only one startup mode to keep app launch behavior predictable.";
+"When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts." = "When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts.";
+"Scope guides are intended for non-Swift languages. Swift favors matching-token highlight." = "Scope guides are intended for non-Swift languages. Swift favors matching-token highlight.";
+"Invisible character markers are disabled to avoid whitespace glyph artifacts." = "Invisible character markers are disabled to avoid whitespace glyph artifacts.";
+"Indentation" = "Indentation";
+"Spaces" = "Spaces";
+"Tabs" = "Tabs";
+"Indent Width: %lld" = "Indent Width: %lld";
+"Choose a language for code completion" = "Choose a language for code completion";
+"You can change this later from the Language picker." = "You can change this later from the Language picker.";
+"Purchase is available in App Store/TestFlight builds." = "Purchase is available in App Store/TestFlight builds.";
+"Send Support Tip — %@" = "Send Support Tip — %@";
+"Send Tip %@" = "Send Tip %@";
+"The application does not collect analytics data, usage telemetry, advertising identifiers, device fingerprints, or background behavioral metrics. No automatic data transmission to developer-controlled servers occurs." = "The application does not collect analytics data, usage telemetry, advertising identifiers, device fingerprints, or background behavioral metrics. No automatic data transmission to developer-controlled servers occurs.";
+"AI-assisted code completion is an optional feature. External network communication only occurs when a user explicitly enables AI completion and selects an external AI provider within the application settings." = "AI-assisted code completion is an optional feature. External network communication only occurs when a user explicitly enables AI completion and selects an external AI provider within the application settings.";
+"When AI completion is triggered, the application transmits only the minimal contextual text necessary to generate a completion suggestion. This typically includes the code immediately surrounding the cursor position or the active selection." = "When AI completion is triggered, the application transmits only the minimal contextual text necessary to generate a completion suggestion. This typically includes the code immediately surrounding the cursor position or the active selection.";
+"The application does not automatically transmit full project folders, unrelated files, entire file system contents, contact data, location data, or device-specific identifiers." = "The application does not automatically transmit full project folders, unrelated files, entire file system contents, contact data, location data, or device-specific identifiers.";
+"Authentication credentials (API keys) for external AI providers are stored securely in the system keychain and are transmitted only to the user-selected provider for the purpose of completing the AI request." = "Authentication credentials (API keys) for external AI providers are stored securely in the system keychain and are transmitted only to the user-selected provider for the purpose of completing the AI request.";
+"All external communication is performed over encrypted HTTPS connections. If AI completion is disabled, the application performs no external AI-related network requests." = "All external communication is performed over encrypted HTTPS connections. If AI completion is disabled, the application performs no external AI-related network requests.";
diff --git a/README.md b/README.md
index 76eb987..f95cc02 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@
> Status: **active release**
-> Latest release: **v0.4.29**
+> Latest release: **v0.4.30**
> Platform target: **macOS 26 (Tahoe)** compatible with **macOS Sequoia**
> Apple Silicon: tested / Intel: not tested
@@ -30,7 +30,7 @@
Prebuilt binaries are available on [GitHub Releases](https://github.com/h3pdesign/Neon-Vision-Editor/releases).
-- Latest release: **v0.4.29**
+- Latest release: **v0.4.30**
- Apple AppStore [On the AppStore](https://apps.apple.com/de/app/neon-vision-editor/id6758950965)
- TestFlight beta: [Join here](https://testflight.apple.com/join/YWB2fGAP)
- Architecture: Apple Silicon (Intel not tested)
@@ -128,6 +128,12 @@ If macOS blocks first launch:
## Changelog
+### v0.4.30 (summary)
+
+- TODO
+- TODO
+- TODO
+
### v0.4.29 (summary)
- Added explicit English (`en`) and German (`de`) support strings for the Support/IAP settings surface to keep release copy consistent across locales.
@@ -144,13 +150,6 @@ If macOS blocks first launch:
- Improved macOS Settings UX with smoother tab-to-tab size transitions and tighter dynamic window sizing.
- Improved iOS/iPadOS toolbar language picker sizing so compact labels remain single-line and avoid clipping.
-### v0.4.27 (summary)
-
-- Added compact iOS/iPadOS toolbar language labels and tightened picker widths to free toolbar space on smaller screens.
-- Improved iPad toolbar density/alignment so more actions are visible before overflow and controls start further left.
-- Improved macOS translucent chrome consistency between toolbar, tab strip, and project-sidebar header surfaces.
-- Fixed macOS project-sidebar top/header transparency bleed when unified translucent toolbar backgrounds are enabled.
-
Full release history: [`CHANGELOG.md`](CHANGELOG.md)
## Known Limitations
@@ -170,12 +169,12 @@ Full release history: [`CHANGELOG.md`](CHANGELOG.md)
## Release Integrity
-- Tag: `v0.4.29`
+- Tag: `v0.4.30`
- Tagged commit: `1c31306`
- Verify local tag target:
```bash
-git rev-parse --verify v0.4.29
+git rev-parse --verify v0.4.30
```
- Verify downloaded artifact checksum locally: