mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
v0.4.30: cursor stability and markdown preview fixes
This commit is contained in:
parent
5b1a887b50
commit
c5afcc6801
11 changed files with 961 additions and 76 deletions
11
CHANGELOG.md
11
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -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: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: "\"", with: """)
|
||||
.replacingOccurrences(of: "'", with: "'")
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@ViewBuilder
|
||||
private var iPhoneUnifiedTopChromeHost: some View {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
33
Neon Vision Editor/UI/MarkdownPreviewWebView.swift
Normal file
33
Neon Vision Editor/UI/MarkdownPreviewWebView.swift
Normal 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
|
||||
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
21
README.md
21
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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue