v0.4.30: cursor stability and markdown preview fixes

This commit is contained in:
h3p 2026-02-24 15:44:43 +01:00
parent 5b1a887b50
commit c5afcc6801
11 changed files with 961 additions and 76 deletions

View file

@ -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

View file

@ -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;

View file

@ -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 {

View file

@ -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()
}) {

View file

@ -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) ?? "<pre>\(escapedHTML(markdownText))</pre>"
return """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
\(markdownPreviewCSS(template: markdownPreviewTemplate))
</style>
</head>
<body class="\(markdownPreviewTemplate)">
<main class="content">
\(bodyHTML)
</main>
</body>
</html>
"""
}
private func renderedMarkdownBodyHTML(from markdownText: String) -> String? {
let html = simpleMarkdownToHTML(markdownText).trimmingCharacters(in: .whitespacesAndNewlines)
return html.isEmpty ? nil : html
}
private func simpleMarkdownToHTML(_ markdown: String) -> String {
let lines = markdown.replacingOccurrences(of: "\r\n", with: "\n").components(separatedBy: "\n")
var result: [String] = []
var paragraphLines: [String] = []
var insideCodeFence = false
var codeFenceLanguage: String?
var insideUnorderedList = false
var insideOrderedList = false
var insideBlockquote = false
func flushParagraph() {
guard !paragraphLines.isEmpty else { return }
let paragraph = paragraphLines.map { inlineMarkdownToHTML($0) }.joined(separator: "<br/>")
result.append("<p>\(paragraph)</p>")
paragraphLines.removeAll(keepingCapacity: true)
}
func closeLists() {
if insideUnorderedList {
result.append("</ul>")
insideUnorderedList = false
}
if insideOrderedList {
result.append("</ol>")
insideOrderedList = false
}
}
func closeBlockquote() {
if insideBlockquote {
flushParagraph()
closeLists()
result.append("</blockquote>")
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("</code></pre>")
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("<pre><code class=\"language-\(escapedHTML(codeFenceLanguage))\">")
} else {
result.append("<pre><code>")
}
}
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("<h\(heading.level)>\(inlineMarkdownToHTML(heading.text))</h\(heading.level)>")
continue
}
if isMarkdownHorizontalRule(trimmed) {
closeBlockquote()
closeParagraphAndInlineContainers()
result.append("<hr/>")
continue
}
var workingLine = trimmed
let isBlockquoteLine = workingLine.hasPrefix(">")
if isBlockquoteLine {
if !insideBlockquote {
closeParagraphAndInlineContainers()
result.append("<blockquote>")
insideBlockquote = true
}
workingLine = workingLine.dropFirst().trimmingCharacters(in: .whitespaces)
} else {
closeBlockquote()
}
if let unordered = markdownUnorderedListItem(from: workingLine) {
flushParagraph()
if insideOrderedList {
result.append("</ol>")
insideOrderedList = false
}
if !insideUnorderedList {
result.append("<ul>")
insideUnorderedList = true
}
result.append("<li>\(inlineMarkdownToHTML(unordered))</li>")
continue
}
if let ordered = markdownOrderedListItem(from: workingLine) {
flushParagraph()
if insideUnorderedList {
result.append("</ul>")
insideUnorderedList = false
}
if !insideOrderedList {
result.append("<ol>")
insideOrderedList = true
}
result.append("<li>\(inlineMarkdownToHTML(ordered))</li>")
continue
}
closeLists()
paragraphLines.append(workingLine)
}
closeBlockquote()
closeParagraphAndInlineContainers()
if insideCodeFence {
result.append("</code></pre>")
}
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("<code>\(content)</code>")
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 "<img src=\"\(parts[1])\" alt=\"\(parts[0])\"/>"
}
html = replacingRegex(in: html, pattern: "\\[([^\\]]+)\\]\\(([^\\)\\s]+)\\)") { match in
let parts = captureGroups(in: match, pattern: "\\[([^\\]]+)\\]\\(([^\\)\\s]+)\\)")
guard parts.count == 2 else { return match }
return "<a href=\"\(parts[1])\">\(parts[0])</a>"
}
html = replacingRegex(in: html, pattern: "\\*\\*([^*]+)\\*\\*") { "<strong>\(String($0.dropFirst(2).dropLast(2)))</strong>" }
html = replacingRegex(in: html, pattern: "__([^_]+)__") { "<strong>\(String($0.dropFirst(2).dropLast(2)))</strong>" }
html = replacingRegex(in: html, pattern: "\\*([^*]+)\\*") { "<em>\(String($0.dropFirst().dropLast()))</em>" }
html = replacingRegex(in: html, pattern: "_([^_]+)_") { "<em>\(String($0.dropFirst().dropLast()))</em>" }
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..<match.numberOfRanges {
if let range = Range(match.range(at: idx), in: text) {
groups.append(String(text[range]))
}
}
return groups
}
private func markdownPreviewCSS(template: String) -> 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: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "\"", with: "&quot;")
.replacingOccurrences(of: "'", with: "&#39;")
}
#endif
#if os(iOS)
@ViewBuilder
private var iPhoneUnifiedTopChromeHost: some View {

View file

@ -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) {

View file

@ -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

View file

@ -242,12 +242,11 @@ struct WelcomeTourView: View {
private let pages: [TourPage] = [
TourPage(
title: "Whats 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)],

View file

@ -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…";
"Youre 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.";

View file

@ -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…";
"Youre up to date." = "Youre 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.";

View file

@ -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: