mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Prepare v0.5.1 release updates
This commit is contained in:
parent
41a3d1798c
commit
3a8c13806c
14 changed files with 575 additions and 39 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -6,13 +6,30 @@ The format follows *Keep a Changelog*. Versions use semantic versioning with pre
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.5.1] - 2026-03-08
|
||||
|
||||
### Added
|
||||
- Added bulk `Close All Tabs` actions to toolbar surfaces (macOS, iOS, iPadOS), including a confirmation step before closing.
|
||||
- Added project-structure quick actions to expand all folders or collapse all folders in one step.
|
||||
- Added six vivid neon syntax themes with distinct color profiles: `Neon Voltage`, `Laserwave`, `Cyber Lime`, `Plasma Storm`, `Inferno Neon`, and `Ultraviolet Flux`.
|
||||
- Added a lock-safe cross-platform build matrix helper script (`scripts/ci/build_platform_matrix.sh`) to run macOS + iOS Simulator + iPad Simulator builds sequentially.
|
||||
- Added iPhone Markdown preview as a bottom sheet with toolbar toggle and resizable detents for Apple-guideline-compliant height control.
|
||||
- Added unsupported-file safety handling across project sidebar, open/import flows, and user-facing unsupported-file alerts instead of crash paths.
|
||||
- Added a project-sidebar switch to show only supported files (enabled by default).
|
||||
- Added SVG (`.svg`) editor file support with XML language mapping and syntax-highlighting path reuse.
|
||||
|
||||
### Improved
|
||||
- Improved Markdown preview stability by preserving relative scroll position during preview refreshes.
|
||||
- Improved Markdown preview behavior for very large files by using a safe plain-text fallback with explicit status messaging instead of full HTML conversion.
|
||||
- Improved neon syntax vibrancy consistency by extending the raw-neon adjustment profile to additional high-intensity neon themes.
|
||||
- Improved contributor guidance with a documented lock-safe platform build verification command in `README.md`.
|
||||
- Improved iPhone markdown preview sheet header density by using inline navigation-title mode to align title height with the `Done/Fertig` action row.
|
||||
|
||||
### Fixed
|
||||
- Fixed diagnostics export safety by redacting token-like updater status fragments before copying.
|
||||
- Fixed Markdown regression coverage with new tests for Claude-style mixed-content Markdown and code-fence matching behavior.
|
||||
- Fixed accidental destructive tab-bulk-close behavior by requiring explicit user confirmation before closing all tabs.
|
||||
- Fixed missing localization for new close-all confirmation/actions by adding English and German strings.
|
||||
|
||||
## [v0.5.0] - 2026-03-06
|
||||
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 433;
|
||||
CURRENT_PROJECT_VERSION = 434;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
@ -444,7 +444,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 433;
|
||||
CURRENT_PROJECT_VERSION = 434;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public struct LanguageDetector {
|
|||
"yaml": "yaml",
|
||||
"yml": "yaml",
|
||||
"xml": "xml",
|
||||
"svg": "xml",
|
||||
"plist": "xml",
|
||||
"sql": "sql",
|
||||
"log": "log",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import Observation
|
|||
import UniformTypeIdentifiers
|
||||
import Foundation
|
||||
import OSLog
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
|
@ -802,6 +805,7 @@ class EditorViewModel {
|
|||
"yaml": "yaml",
|
||||
"yml": "yaml",
|
||||
"xml": "xml",
|
||||
"svg": "xml",
|
||||
"sql": "sql",
|
||||
"log": "log",
|
||||
"vim": "vim",
|
||||
|
|
@ -1182,7 +1186,9 @@ class EditorViewModel {
|
|||
if panel.runModal() == .OK {
|
||||
let urls = panel.urls
|
||||
for url in urls {
|
||||
openFile(url: url)
|
||||
if !openFile(url: url) {
|
||||
presentUnsupportedFileAlertOnMac(for: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
|
|
@ -1192,8 +1198,13 @@ class EditorViewModel {
|
|||
}
|
||||
|
||||
// Loads a file into a new tab unless the file is already open.
|
||||
func openFile(url: URL) {
|
||||
if focusTabIfOpen(for: url) { return }
|
||||
@discardableResult
|
||||
func openFile(url: URL) -> Bool {
|
||||
guard Self.isSupportedEditorFileURL(url) else {
|
||||
debugLog("Unsupported file type skipped: \(url.lastPathComponent)")
|
||||
return false
|
||||
}
|
||||
if focusTabIfOpen(for: url) { return true }
|
||||
let extLangHint = LanguageDetector.shared.preferredLanguage(for: url) ?? languageMap[url.pathExtension.lowercased()]
|
||||
let fileSize = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
|
||||
let isLargeCandidate = fileSize >= EditorLoadHelper.largeFileCandidateByteThreshold
|
||||
|
|
@ -1222,8 +1233,55 @@ class EditorViewModel {
|
|||
await self.markTabLoadFailed(tabID: tabID)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
nonisolated static func isSupportedEditorFileURL(_ url: URL) -> Bool {
|
||||
if url.hasDirectoryPath { return false }
|
||||
let fileName = url.lastPathComponent.lowercased()
|
||||
let ext = url.pathExtension.lowercased()
|
||||
|
||||
if ext.isEmpty {
|
||||
let supportedDotfiles: Set<String> = [
|
||||
".zshrc", ".zprofile", ".zlogin", ".zlogout",
|
||||
".bashrc", ".bash_profile", ".bash_login", ".bash_logout",
|
||||
".profile", ".vimrc", ".env", ".envrc", ".gitconfig"
|
||||
]
|
||||
return supportedDotfiles.contains(fileName) || fileName.hasPrefix(".env")
|
||||
}
|
||||
|
||||
let knownSupportedExtensions: Set<String> = [
|
||||
"swift", "py", "pyi", "js", "mjs", "cjs", "ts", "tsx", "php", "phtml",
|
||||
"csv", "tsv", "txt", "toml", "ini", "yaml", "yml", "xml", "svg", "plist", "sql",
|
||||
"log", "vim", "ipynb", "java", "kt", "kts", "go", "rb", "rs", "ps1", "psm1",
|
||||
"html", "htm", "ee", "exp", "tmpl", "css", "c", "cpp", "cc", "hpp", "hh", "h",
|
||||
"m", "mm", "cs", "json", "jsonc", "json5", "md", "markdown", "env", "proto",
|
||||
"graphql", "gql", "rst", "conf", "nginx", "cob", "cbl", "cobol", "sh", "bash", "zsh"
|
||||
]
|
||||
if knownSupportedExtensions.contains(ext) {
|
||||
return true
|
||||
}
|
||||
|
||||
guard let type = UTType(filenameExtension: ext) else { return false }
|
||||
if type.conforms(to: .text) || type.conforms(to: .plainText) || type.conforms(to: .sourceCode) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private func presentUnsupportedFileAlertOnMac(for url: URL) {
|
||||
let title = NSLocalizedString("Can’t Open File", comment: "Unsupported file alert title")
|
||||
let format = NSLocalizedString("The file \"%@\" is not supported and can’t be opened.", comment: "Unsupported file alert message")
|
||||
let alert = NSAlert()
|
||||
alert.messageText = title
|
||||
alert.informativeText = String(format: format, url.lastPathComponent)
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: "Alert confirmation button"))
|
||||
alert.runModal()
|
||||
}
|
||||
#endif
|
||||
|
||||
private nonisolated static func contentFingerprintValue(_ text: String) -> UInt64 {
|
||||
var hasher = Hasher()
|
||||
hasher.combine(text)
|
||||
|
|
|
|||
|
|
@ -112,24 +112,41 @@ extension ContentView {
|
|||
case .success(let urls):
|
||||
guard !urls.isEmpty else { return }
|
||||
var openedCount = 0
|
||||
var unsupportedCount = 0
|
||||
var openedNames: [String] = []
|
||||
|
||||
for url in urls {
|
||||
viewModel.openFile(url: url)
|
||||
openedCount += 1
|
||||
openedNames.append(url.lastPathComponent)
|
||||
if viewModel.openFile(url: url) {
|
||||
openedCount += 1
|
||||
openedNames.append(url.lastPathComponent)
|
||||
} else {
|
||||
unsupportedCount += 1
|
||||
presentUnsupportedFileAlert(for: url)
|
||||
}
|
||||
}
|
||||
|
||||
guard openedCount > 0 else {
|
||||
findStatusMessage = "Open failed: selected files are no longer available."
|
||||
if unsupportedCount > 0 {
|
||||
findStatusMessage = "Open failed: unsupported file type."
|
||||
} else {
|
||||
findStatusMessage = "Open failed: selected files are no longer available."
|
||||
}
|
||||
recordDiagnostic("iOS import failed: no valid files in selection")
|
||||
return
|
||||
}
|
||||
|
||||
if openedCount == 1, let name = openedNames.first {
|
||||
findStatusMessage = "Opened \(name)"
|
||||
if unsupportedCount > 0 {
|
||||
findStatusMessage = "Opened \(name) (\(unsupportedCount) unsupported ignored)"
|
||||
} else {
|
||||
findStatusMessage = "Opened \(name)"
|
||||
}
|
||||
} else {
|
||||
findStatusMessage = "Opened \(openedCount) files"
|
||||
if unsupportedCount > 0 {
|
||||
findStatusMessage = "Opened \(openedCount) files (\(unsupportedCount) unsupported ignored)"
|
||||
} else {
|
||||
findStatusMessage = "Opened \(openedCount) files"
|
||||
}
|
||||
}
|
||||
recordDiagnostic("iOS import success count: \(openedCount)")
|
||||
case .failure(let error):
|
||||
|
|
@ -242,10 +259,31 @@ extension ContentView {
|
|||
if UIDevice.current.userInterfaceIdiom == .pad && nextValue {
|
||||
showProjectStructureSidebar = false
|
||||
showCompactProjectSidebarSheet = false
|
||||
} else if UIDevice.current.userInterfaceIdiom == .phone && nextValue {
|
||||
dismissKeyboard()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func closeAllTabsFromToolbar() {
|
||||
let dirtyTabIDs = viewModel.tabs.filter(\.isDirty).map(\.id)
|
||||
for tabID in dirtyTabIDs {
|
||||
guard viewModel.tabs.contains(where: { $0.id == tabID }) else { continue }
|
||||
viewModel.saveFile(tabID: tabID)
|
||||
}
|
||||
|
||||
let tabIDsToClose = viewModel.tabs.map(\.id)
|
||||
for tabID in tabIDsToClose {
|
||||
guard viewModel.tabs.contains(where: { $0.id == tabID }) else { continue }
|
||||
viewModel.closeTab(tabID: tabID)
|
||||
}
|
||||
}
|
||||
|
||||
func requestCloseAllTabsFromToolbar() {
|
||||
guard !viewModel.tabs.isEmpty else { return }
|
||||
showCloseAllTabsDialog = true
|
||||
}
|
||||
|
||||
func dismissKeyboard() {
|
||||
#if os(iOS)
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
|
|
@ -624,8 +662,9 @@ extension ContentView {
|
|||
guard let root = projectRootFolderURL else { return }
|
||||
projectTreeRefreshGeneration &+= 1
|
||||
let generation = projectTreeRefreshGeneration
|
||||
let supportedOnly = showSupportedProjectFilesOnly
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let nodes = Self.buildProjectTree(at: root)
|
||||
let nodes = Self.buildProjectTree(at: root, supportedOnly: supportedOnly)
|
||||
DispatchQueue.main.async {
|
||||
guard generation == projectTreeRefreshGeneration else { return }
|
||||
guard projectRootFolderURL?.standardizedFileURL == root.standardizedFileURL else { return }
|
||||
|
|
@ -636,21 +675,27 @@ extension ContentView {
|
|||
}
|
||||
|
||||
func openProjectFile(url: URL) {
|
||||
guard EditorViewModel.isSupportedEditorFileURL(url) else {
|
||||
presentUnsupportedFileAlert(for: url)
|
||||
return
|
||||
}
|
||||
if let existing = viewModel.tabs.first(where: { $0.fileURL?.standardizedFileURL == url.standardizedFileURL }) {
|
||||
viewModel.selectTab(id: existing.id)
|
||||
return
|
||||
}
|
||||
viewModel.openFile(url: url)
|
||||
if !viewModel.openFile(url: url) {
|
||||
presentUnsupportedFileAlert(for: url)
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func buildProjectTree(at root: URL) -> [ProjectTreeNode] {
|
||||
private nonisolated static func buildProjectTree(at root: URL, supportedOnly: Bool) -> [ProjectTreeNode] {
|
||||
var isDir: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: root.path, isDirectory: &isDir), isDir.boolValue else { return [] }
|
||||
return readChildren(of: root, recursive: true)
|
||||
return readChildren(of: root, recursive: true, supportedOnly: supportedOnly)
|
||||
}
|
||||
|
||||
func loadProjectTreeChildren(for directory: URL) -> [ProjectTreeNode] {
|
||||
Self.readChildren(of: directory, recursive: false)
|
||||
Self.readChildren(of: directory, recursive: false, supportedOnly: showSupportedProjectFilesOnly)
|
||||
}
|
||||
|
||||
func setProjectFolder(_ folderURL: URL) {
|
||||
|
|
@ -704,7 +749,7 @@ extension ContentView {
|
|||
}
|
||||
}
|
||||
|
||||
private nonisolated static func readChildren(of directory: URL, recursive: Bool) -> [ProjectTreeNode] {
|
||||
private nonisolated static func readChildren(of directory: URL, recursive: Bool, supportedOnly: Bool) -> [ProjectTreeNode] {
|
||||
if Task.isCancelled { return [] }
|
||||
let fm = FileManager.default
|
||||
let keys: [URLResourceKey] = [.isDirectoryKey, .isHiddenKey, .nameKey]
|
||||
|
|
@ -719,7 +764,10 @@ extension ContentView {
|
|||
guard let values = try? url.resourceValues(forKeys: Set(keys)) else { continue }
|
||||
if values.isHidden == true { continue }
|
||||
let isDirectory = values.isDirectory == true
|
||||
let children = (isDirectory && recursive) ? readChildren(of: url, recursive: true) : []
|
||||
if !isDirectory && supportedOnly && !EditorViewModel.isSupportedEditorFileURL(url) {
|
||||
continue
|
||||
}
|
||||
let children = (isDirectory && recursive) ? readChildren(of: url, recursive: true, supportedOnly: supportedOnly) : []
|
||||
nodes.append(
|
||||
ProjectTreeNode(
|
||||
url: url,
|
||||
|
|
@ -731,6 +779,12 @@ extension ContentView {
|
|||
return nodes
|
||||
}
|
||||
|
||||
private func presentUnsupportedFileAlert(for url: URL) {
|
||||
unsupportedFileName = url.lastPathComponent
|
||||
findStatusMessage = "Unsupported file type."
|
||||
showUnsupportedFileAlert = true
|
||||
}
|
||||
|
||||
static func projectFileURLs(from nodes: [ProjectTreeNode]) -> [URL] {
|
||||
var results: [URL] = []
|
||||
var stack = nodes
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ extension ContentView {
|
|||
case openFile
|
||||
case undo
|
||||
case newTab
|
||||
case closeAllTabs
|
||||
case saveFile
|
||||
case markdownPreview
|
||||
case fontDecrease
|
||||
|
|
@ -130,6 +131,7 @@ extension ContentView {
|
|||
.openFile,
|
||||
.undo,
|
||||
.newTab,
|
||||
.closeAllTabs,
|
||||
.saveFile,
|
||||
.markdownPreview,
|
||||
.fontDecrease,
|
||||
|
|
@ -178,7 +180,7 @@ extension ContentView {
|
|||
}
|
||||
|
||||
private var iPadAlwaysVisibleActions: [IPadToolbarAction] {
|
||||
[.openFile, .newTab, .saveFile, .findReplace, .settings]
|
||||
[.openFile, .newTab, .closeAllTabs, .saveFile, .findReplace, .settings]
|
||||
}
|
||||
|
||||
private var iPadPromotedActionSlotCount: Int {
|
||||
|
|
@ -341,6 +343,17 @@ extension ContentView {
|
|||
.keyboardShortcut("s", modifiers: .command)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var closeAllTabsControl: some View {
|
||||
Button(action: { requestCloseAllTabsFromToolbar() }) {
|
||||
Image(systemName: "xmark.square")
|
||||
}
|
||||
.disabled(viewModel.tabs.isEmpty)
|
||||
.help("Close All Tabs")
|
||||
.accessibilityLabel("Close all tabs")
|
||||
.accessibilityHint("Closes every open tab")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var fontDecreaseControl: some View {
|
||||
Button(action: { adjustEditorFontSize(-1) }) {
|
||||
|
|
@ -473,6 +486,7 @@ extension ContentView {
|
|||
case .openFile: openFileControl
|
||||
case .undo: undoControl
|
||||
case .newTab: newTabControl
|
||||
case .closeAllTabs: closeAllTabsControl
|
||||
case .saveFile: saveFileControl
|
||||
case .markdownPreview: markdownPreviewControl
|
||||
case .fontDecrease: fontDecreaseControl
|
||||
|
|
@ -512,6 +526,11 @@ extension ContentView {
|
|||
Button(action: { viewModel.addNewTab() }) {
|
||||
Label("New Tab", systemImage: "plus.square.on.square")
|
||||
}
|
||||
case .closeAllTabs:
|
||||
Button(action: { requestCloseAllTabsFromToolbar() }) {
|
||||
Label("Close All Tabs", systemImage: "xmark.square")
|
||||
}
|
||||
.disabled(viewModel.tabs.isEmpty)
|
||||
case .saveFile:
|
||||
Button(action: { saveCurrentTabFromToolbar() }) {
|
||||
Label("Save File", systemImage: "square.and.arrow.down")
|
||||
|
|
@ -660,6 +679,19 @@ extension ContentView {
|
|||
.disabled(viewModel.selectedTab == nil)
|
||||
.keyboardShortcut("s", modifiers: .command)
|
||||
|
||||
Button(action: { toggleMarkdownPreviewFromToolbar() }) {
|
||||
Label(
|
||||
"Markdown Preview",
|
||||
systemImage: showMarkdownPreviewPane ? "doc.richtext.fill" : "doc.richtext"
|
||||
)
|
||||
}
|
||||
.disabled(currentLanguage != "markdown")
|
||||
|
||||
Button(action: { requestCloseAllTabsFromToolbar() }) {
|
||||
Label("Close All Tabs", systemImage: "xmark.square")
|
||||
}
|
||||
.disabled(viewModel.tabs.isEmpty)
|
||||
|
||||
Button(action: { saveCurrentTabAsFromToolbar() }) {
|
||||
Label("Save As…", systemImage: "square.and.arrow.down.on.square")
|
||||
}
|
||||
|
|
@ -915,6 +947,12 @@ extension ContentView {
|
|||
}
|
||||
.help("New Tab (Cmd+T)")
|
||||
|
||||
Button(action: { requestCloseAllTabsFromToolbar() }) {
|
||||
Label("Close All Tabs", systemImage: "xmark.square")
|
||||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||||
}
|
||||
.help("Close All Tabs")
|
||||
|
||||
Button(action: {
|
||||
saveCurrentTabFromToolbar()
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ struct ContentView: View {
|
|||
#endif
|
||||
#if os(iOS)
|
||||
@State private var previousKeyboardAccessoryVisibility: Bool? = nil
|
||||
@State private var markdownPreviewSheetDetent: PresentationDetent = .medium
|
||||
#endif
|
||||
#if os(macOS)
|
||||
@AppStorage("SettingsMacTranslucencyMode") private var macTranslucencyModeRaw: String = "balanced"
|
||||
|
|
@ -227,18 +228,22 @@ struct ContentView: View {
|
|||
@State var projectRootFolderURL: URL? = nil
|
||||
@State var projectTreeNodes: [ProjectTreeNode] = []
|
||||
@State var projectTreeRefreshGeneration: Int = 0
|
||||
@AppStorage("SettingsShowSupportedProjectFilesOnly") var showSupportedProjectFilesOnly: Bool = true
|
||||
@State var projectOverrideIndentWidth: Int? = nil
|
||||
@State var projectOverrideLineWrapEnabled: Bool? = nil
|
||||
@State var showProjectFolderPicker: Bool = false
|
||||
@State var projectFolderSecurityURL: URL? = nil
|
||||
@State var pendingCloseTabID: UUID? = nil
|
||||
@State var showUnsavedCloseDialog: Bool = false
|
||||
@State var showCloseAllTabsDialog: Bool = false
|
||||
@State private var showExternalConflictDialog: Bool = false
|
||||
@State private var showExternalConflictCompareSheet: Bool = false
|
||||
@State private var externalConflictCompareSnapshot: EditorViewModel.ExternalFileComparisonSnapshot?
|
||||
@State var showClearEditorConfirmDialog: Bool = false
|
||||
@State var showIOSFileImporter: Bool = false
|
||||
@State var showIOSFileExporter: Bool = false
|
||||
@State var showUnsupportedFileAlert: Bool = false
|
||||
@State var unsupportedFileName: String = ""
|
||||
@State var iosExportDocument: PlainTextDocument = PlainTextDocument(text: "")
|
||||
@State var iosExportFilename: String = "Untitled.txt"
|
||||
@State var iosExportTabID: UUID? = nil
|
||||
|
|
@ -381,6 +386,14 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
var canShowMarkdownPreviewPane: Bool {
|
||||
#if os(iOS)
|
||||
true
|
||||
#else
|
||||
true
|
||||
#endif
|
||||
}
|
||||
|
||||
private var canShowMarkdownPreviewSplitPane: Bool {
|
||||
#if os(iOS)
|
||||
canShowMarkdownPreviewOnCurrentDevice
|
||||
#else
|
||||
|
|
@ -388,6 +401,26 @@ struct ContentView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
private var shouldPresentMarkdownPreviewSheetOnIPhone: Bool {
|
||||
UIDevice.current.userInterfaceIdiom == .phone &&
|
||||
showMarkdownPreviewPane &&
|
||||
currentLanguage == "markdown" &&
|
||||
!brainDumpLayoutEnabled
|
||||
}
|
||||
|
||||
private var markdownPreviewSheetPresentationBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { shouldPresentMarkdownPreviewSheetOnIPhone },
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
showMarkdownPreviewPane = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
private var settingsSheetDetents: Set<PresentationDetent> {
|
||||
#if os(iOS)
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
|
|
@ -1836,6 +1869,9 @@ struct ContentView: View {
|
|||
.onChange(of: showProjectStructureSidebar) { _, _ in
|
||||
persistSessionIfReady()
|
||||
}
|
||||
.onChange(of: showSupportedProjectFilesOnly) { _, _ in
|
||||
refreshProjectTree()
|
||||
}
|
||||
.onChange(of: showMarkdownPreviewPane) { _, _ in
|
||||
persistSessionIfReady()
|
||||
}
|
||||
|
|
@ -2031,9 +2067,11 @@ struct ContentView: View {
|
|||
rootFolderURL: contentView.projectRootFolderURL,
|
||||
nodes: contentView.projectTreeNodes,
|
||||
selectedFileURL: contentView.viewModel.selectedTab?.fileURL,
|
||||
showSupportedFilesOnly: contentView.showSupportedProjectFilesOnly,
|
||||
translucentBackgroundEnabled: false,
|
||||
onOpenFile: { contentView.openFileFromToolbar() },
|
||||
onOpenFolder: { contentView.openProjectFolder() },
|
||||
onToggleSupportedFilesOnly: { contentView.showSupportedProjectFilesOnly = $0 },
|
||||
onOpenProjectFile: { contentView.openProjectFile(url: $0) },
|
||||
onRefreshTree: { contentView.refreshProjectTree() }
|
||||
)
|
||||
|
|
@ -2048,6 +2086,23 @@ struct ContentView: View {
|
|||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
}
|
||||
.sheet(isPresented: contentView.markdownPreviewSheetPresentationBinding) {
|
||||
NavigationStack {
|
||||
contentView.markdownPreviewPane
|
||||
.navigationTitle("Markdown Preview")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") {
|
||||
contentView.showMarkdownPreviewPane = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.35), .medium, .large], selection: contentView.$markdownPreviewSheetDetent)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationContentInteraction(.scrolls)
|
||||
}
|
||||
#endif
|
||||
#if canImport(UIKit)
|
||||
.sheet(isPresented: contentView.$showProjectFolderPicker) {
|
||||
|
|
@ -2124,6 +2179,12 @@ struct ContentView: View {
|
|||
Text("This file has unsaved changes.")
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Are you sure you want to close all tabs?", isPresented: contentView.$showCloseAllTabsDialog, titleVisibility: .visible) {
|
||||
Button("Close All Tabs", role: .destructive) {
|
||||
contentView.closeAllTabsFromToolbar()
|
||||
}
|
||||
Button("Cancel", role: .cancel) { }
|
||||
}
|
||||
.confirmationDialog("File changed on disk", isPresented: contentView.$showExternalConflictDialog, titleVisibility: .visible) {
|
||||
if let conflict = contentView.viewModel.pendingExternalFileConflict {
|
||||
Button("Reload from Disk", role: .destructive) {
|
||||
|
|
@ -2207,6 +2268,17 @@ struct ContentView: View {
|
|||
} message: {
|
||||
Text("This will remove all text in the current editor.")
|
||||
}
|
||||
.alert("Can’t Open File", isPresented: contentView.$showUnsupportedFileAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text(String(
|
||||
format: NSLocalizedString(
|
||||
"The file \"%@\" is not supported and can’t be opened.",
|
||||
comment: "Unsupported file alert message"
|
||||
),
|
||||
contentView.unsupportedFileName
|
||||
))
|
||||
}
|
||||
#if canImport(UIKit)
|
||||
.fileImporter(
|
||||
isPresented: contentView.$showIOSFileImporter,
|
||||
|
|
@ -3337,7 +3409,7 @@ struct ContentView: View {
|
|||
alignment: brainDumpLayoutEnabled ? .top : .topLeading
|
||||
)
|
||||
|
||||
if canShowMarkdownPreviewPane && showMarkdownPreviewPane && currentLanguage == "markdown" && !brainDumpLayoutEnabled {
|
||||
if canShowMarkdownPreviewSplitPane && showMarkdownPreviewPane && currentLanguage == "markdown" && !brainDumpLayoutEnabled {
|
||||
Divider()
|
||||
markdownPreviewPane
|
||||
.frame(minWidth: 280, idealWidth: 420, maxWidth: 680, maxHeight: .infinity)
|
||||
|
|
@ -3353,9 +3425,11 @@ struct ContentView: View {
|
|||
rootFolderURL: projectRootFolderURL,
|
||||
nodes: projectTreeNodes,
|
||||
selectedFileURL: viewModel.selectedTab?.fileURL,
|
||||
showSupportedFilesOnly: showSupportedProjectFilesOnly,
|
||||
translucentBackgroundEnabled: enableTranslucentWindow,
|
||||
onOpenFile: { openFileFromToolbar() },
|
||||
onOpenFolder: { openProjectFolder() },
|
||||
onToggleSupportedFilesOnly: { showSupportedProjectFilesOnly = $0 },
|
||||
onOpenProjectFile: { openProjectFile(url: $0) },
|
||||
onRefreshTree: { refreshProjectTree() }
|
||||
)
|
||||
|
|
@ -3366,9 +3440,11 @@ struct ContentView: View {
|
|||
rootFolderURL: projectRootFolderURL,
|
||||
nodes: projectTreeNodes,
|
||||
selectedFileURL: viewModel.selectedTab?.fileURL,
|
||||
showSupportedFilesOnly: showSupportedProjectFilesOnly,
|
||||
translucentBackgroundEnabled: enableTranslucentWindow,
|
||||
onOpenFile: { openFileFromToolbar() },
|
||||
onOpenFolder: { openProjectFolder() },
|
||||
onToggleSupportedFilesOnly: { showSupportedProjectFilesOnly = $0 },
|
||||
onOpenProjectFile: { openProjectFile(url: $0) },
|
||||
onRefreshTree: { refreshProjectTree() }
|
||||
)
|
||||
|
|
@ -3432,7 +3508,7 @@ struct ContentView: View {
|
|||
)
|
||||
}
|
||||
.onChange(of: horizontalSizeClass) { _, newClass in
|
||||
if newClass != .regular && showMarkdownPreviewPane {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad && newClass != .regular && showMarkdownPreviewPane {
|
||||
showMarkdownPreviewPane = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -337,12 +337,12 @@ struct WelcomeTourView: View {
|
|||
private let pages: [TourPage] = [
|
||||
TourPage(
|
||||
title: "What’s New in This Release",
|
||||
subtitle: "Major changes since v0.4.34:",
|
||||
subtitle: "Major changes since v0.5.0:",
|
||||
bullets: [
|
||||
"Added updater staging hardening with retry/fallback behavior and staged-bundle integrity checks.",
|
||||
"Added explicit accessibility labels/hints for key toolbar actions and updater log/progress controls.",
|
||||
"Added a 0.5.0 quality roadmap milestone with focused issues for updater reliability, accessibility, and release gating.",
|
||||
"Improved CSV handling by enabling fast syntax profile earlier and for long-line CSV files to reduce freeze risk."
|
||||
"Added bulk `Close All Tabs` actions to toolbar surfaces (macOS, iOS, iPadOS), including a confirmation step before closing.",
|
||||
"Added project-structure quick actions to expand all folders or collapse all folders in one step.",
|
||||
"Added six vivid neon syntax themes with distinct color profiles: `Neon Voltage`, `Laserwave`, `Cyber Lime`, `Plasma Storm`, `Inferno Neon`, and `Ultraviolet Flux`.",
|
||||
"Added a lock-safe cross-platform build matrix helper script (`scripts/ci/build_platform_matrix.sh`) to run macOS + iOS Simulator + iPad Simulator builds sequentially."
|
||||
],
|
||||
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)],
|
||||
|
|
|
|||
|
|
@ -358,9 +358,11 @@ struct ProjectStructureSidebarView: View {
|
|||
let rootFolderURL: URL?
|
||||
let nodes: [ProjectTreeNode]
|
||||
let selectedFileURL: URL?
|
||||
let showSupportedFilesOnly: Bool
|
||||
let translucentBackgroundEnabled: Bool
|
||||
let onOpenFile: () -> Void
|
||||
let onOpenFolder: () -> Void
|
||||
let onToggleSupportedFilesOnly: (Bool) -> Void
|
||||
let onOpenProjectFile: (URL) -> Void
|
||||
let onRefreshTree: () -> Void
|
||||
@State private var expandedDirectories: Set<String> = []
|
||||
|
|
@ -392,6 +394,30 @@ struct ProjectStructureSidebarView: View {
|
|||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Refresh Folder Tree")
|
||||
|
||||
Menu {
|
||||
Button {
|
||||
onToggleSupportedFilesOnly(!showSupportedFilesOnly)
|
||||
} label: {
|
||||
Label(
|
||||
"Show Supported Files Only",
|
||||
systemImage: showSupportedFilesOnly ? "checkmark.circle.fill" : "circle"
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
Button("Expand All") {
|
||||
expandAllDirectories()
|
||||
}
|
||||
Button("Collapse All") {
|
||||
collapseAllDirectories()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.up.arrow.down.circle")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Expand or Collapse All")
|
||||
.accessibilityLabel("Expand or collapse all folders")
|
||||
.accessibilityHint("Expands or collapses all folders in the project tree")
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 10)
|
||||
|
|
@ -533,6 +559,23 @@ struct ProjectStructureSidebarView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
private func expandAllDirectories() {
|
||||
expandedDirectories = allDirectoryNodeIDs(in: nodes)
|
||||
}
|
||||
|
||||
private func collapseAllDirectories() {
|
||||
expandedDirectories.removeAll()
|
||||
}
|
||||
|
||||
private func allDirectoryNodeIDs(in treeNodes: [ProjectTreeNode]) -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
for node in treeNodes where node.isDirectory {
|
||||
result.insert(node.id)
|
||||
result.formUnion(allDirectoryNodeIDs(in: node.children))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func projectNodeView(_ node: ProjectTreeNode, level: Int) -> AnyView {
|
||||
if node.isDirectory {
|
||||
return AnyView(
|
||||
|
|
|
|||
|
|
@ -155,6 +155,12 @@ private func adjustedSyntaxColor(
|
|||
let editorThemeNames: [String] = [
|
||||
"Neon Glow",
|
||||
"Neon Flow",
|
||||
"Neon Voltage",
|
||||
"Laserwave",
|
||||
"Cyber Lime",
|
||||
"Plasma Storm",
|
||||
"Inferno Neon",
|
||||
"Ultraviolet Flux",
|
||||
"Custom",
|
||||
"Dracula",
|
||||
"One Dark Pro",
|
||||
|
|
@ -221,6 +227,90 @@ private func paletteForThemeName(_ name: String, defaults: UserDefaults) -> Them
|
|||
property: Color(red: 0.92, green: 0.05, blue: 0.91),
|
||||
builtin: Color(red: 0.96, green: 0.20, blue: 0.04)
|
||||
)
|
||||
case "Neon Voltage":
|
||||
return ThemePalette(
|
||||
text: Color(red: 0.95, green: 0.98, blue: 1.00),
|
||||
background: Color(red: 0.04, green: 0.06, blue: 0.09),
|
||||
cursor: Color(red: 0.00, green: 0.92, blue: 1.00),
|
||||
selection: Color(red: 0.12, green: 0.20, blue: 0.30),
|
||||
keyword: Color(red: 0.00, green: 0.92, blue: 1.00),
|
||||
string: Color(red: 0.35, green: 1.00, blue: 0.72),
|
||||
number: Color(red: 1.00, green: 0.72, blue: 0.10),
|
||||
comment: Color(red: 0.48, green: 0.56, blue: 0.68),
|
||||
type: Color(red: 0.74, green: 0.52, blue: 1.00),
|
||||
property: Color(red: 0.32, green: 0.88, blue: 1.00),
|
||||
builtin: Color(red: 1.00, green: 0.34, blue: 0.74)
|
||||
)
|
||||
case "Laserwave":
|
||||
return ThemePalette(
|
||||
text: Color(red: 0.96, green: 0.93, blue: 1.00),
|
||||
background: Color(red: 0.10, green: 0.05, blue: 0.14),
|
||||
cursor: Color(red: 0.98, green: 0.24, blue: 0.88),
|
||||
selection: Color(red: 0.25, green: 0.15, blue: 0.34),
|
||||
keyword: Color(red: 1.00, green: 0.28, blue: 0.86),
|
||||
string: Color(red: 0.22, green: 0.94, blue: 1.00),
|
||||
number: Color(red: 1.00, green: 0.60, blue: 0.30),
|
||||
comment: Color(red: 0.60, green: 0.52, blue: 0.72),
|
||||
type: Color(red: 0.65, green: 0.72, blue: 1.00),
|
||||
property: Color(red: 0.36, green: 0.98, blue: 0.84),
|
||||
builtin: Color(red: 1.00, green: 0.42, blue: 0.56)
|
||||
)
|
||||
case "Cyber Lime":
|
||||
return ThemePalette(
|
||||
text: Color(red: 0.94, green: 0.98, blue: 0.92),
|
||||
background: Color(red: 0.07, green: 0.09, blue: 0.06),
|
||||
cursor: Color(red: 0.68, green: 1.00, blue: 0.20),
|
||||
selection: Color(red: 0.18, green: 0.25, blue: 0.14),
|
||||
keyword: Color(red: 0.68, green: 1.00, blue: 0.20),
|
||||
string: Color(red: 0.20, green: 1.00, blue: 0.78),
|
||||
number: Color(red: 1.00, green: 0.86, blue: 0.22),
|
||||
comment: Color(red: 0.54, green: 0.62, blue: 0.50),
|
||||
type: Color(red: 0.48, green: 0.86, blue: 1.00),
|
||||
property: Color(red: 0.88, green: 1.00, blue: 0.36),
|
||||
builtin: Color(red: 1.00, green: 0.48, blue: 0.40)
|
||||
)
|
||||
case "Plasma Storm":
|
||||
return ThemePalette(
|
||||
text: Color(red: 0.95, green: 0.95, blue: 1.00),
|
||||
background: Color(red: 0.06, green: 0.05, blue: 0.10),
|
||||
cursor: Color(red: 0.54, green: 0.44, blue: 1.00),
|
||||
selection: Color(red: 0.16, green: 0.12, blue: 0.30),
|
||||
keyword: Color(red: 0.58, green: 0.46, blue: 1.00),
|
||||
string: Color(red: 0.24, green: 0.90, blue: 1.00),
|
||||
number: Color(red: 1.00, green: 0.52, blue: 0.20),
|
||||
comment: Color(red: 0.54, green: 0.50, blue: 0.68),
|
||||
type: Color(red: 0.80, green: 0.54, blue: 1.00),
|
||||
property: Color(red: 0.66, green: 0.78, blue: 1.00),
|
||||
builtin: Color(red: 1.00, green: 0.30, blue: 0.66)
|
||||
)
|
||||
case "Inferno Neon":
|
||||
return ThemePalette(
|
||||
text: Color(red: 1.00, green: 0.94, blue: 0.90),
|
||||
background: Color(red: 0.11, green: 0.05, blue: 0.03),
|
||||
cursor: Color(red: 1.00, green: 0.46, blue: 0.08),
|
||||
selection: Color(red: 0.28, green: 0.12, blue: 0.08),
|
||||
keyword: Color(red: 1.00, green: 0.38, blue: 0.16),
|
||||
string: Color(red: 1.00, green: 0.74, blue: 0.24),
|
||||
number: Color(red: 1.00, green: 0.26, blue: 0.48),
|
||||
comment: Color(red: 0.70, green: 0.54, blue: 0.46),
|
||||
type: Color(red: 1.00, green: 0.56, blue: 0.20),
|
||||
property: Color(red: 1.00, green: 0.70, blue: 0.32),
|
||||
builtin: Color(red: 1.00, green: 0.18, blue: 0.30)
|
||||
)
|
||||
case "Ultraviolet Flux":
|
||||
return ThemePalette(
|
||||
text: Color(red: 0.96, green: 0.93, blue: 1.00),
|
||||
background: Color(red: 0.08, green: 0.04, blue: 0.12),
|
||||
cursor: Color(red: 0.84, green: 0.36, blue: 1.00),
|
||||
selection: Color(red: 0.21, green: 0.12, blue: 0.30),
|
||||
keyword: Color(red: 0.84, green: 0.36, blue: 1.00),
|
||||
string: Color(red: 0.32, green: 0.98, blue: 0.96),
|
||||
number: Color(red: 1.00, green: 0.62, blue: 0.24),
|
||||
comment: Color(red: 0.60, green: 0.52, blue: 0.72),
|
||||
type: Color(red: 1.00, green: 0.38, blue: 0.86),
|
||||
property: Color(red: 0.68, green: 0.72, blue: 1.00),
|
||||
builtin: Color(red: 1.00, green: 0.28, blue: 0.56)
|
||||
)
|
||||
case "Dracula":
|
||||
return ThemePalette(
|
||||
text: Color(red: 0.97, green: 0.97, blue: 0.95),
|
||||
|
|
@ -508,7 +598,18 @@ func currentEditorTheme(colorScheme: ColorScheme) -> EditorTheme {
|
|||
return Color(red: 0.90, green: 0.90, blue: 0.90)
|
||||
}()
|
||||
|
||||
let profile: SyntaxAdjustmentProfile = (name == "Neon Glow") ? .neonRaw : .standard
|
||||
let profile: SyntaxAdjustmentProfile = {
|
||||
let vividNeonThemes: Set<String> = [
|
||||
"Neon Glow",
|
||||
"Neon Voltage",
|
||||
"Laserwave",
|
||||
"Cyber Lime",
|
||||
"Plasma Storm",
|
||||
"Inferno Neon",
|
||||
"Ultraviolet Flux"
|
||||
]
|
||||
return vividNeonThemes.contains(name) ? .neonRaw : .standard
|
||||
}()
|
||||
let keyword = adjustedSyntaxColor(
|
||||
palette.keyword,
|
||||
colorScheme: colorScheme,
|
||||
|
|
|
|||
|
|
@ -262,3 +262,8 @@
|
|||
"Refresh Folder Tree" = "Ordnerbaum aktualisieren";
|
||||
"Reset to Default" = "Auf Standard zurücksetzen";
|
||||
"Use Default Template" = "Standardvorlage verwenden";
|
||||
"Close All Tabs" = "Alle Tabs schließen";
|
||||
"Are you sure you want to close all tabs?" = "Möchtest du wirklich alle Tabs schließen?";
|
||||
"Can’t Open File" = "Datei kann nicht geöffnet werden";
|
||||
"The file \"%@\" is not supported and can’t be opened." = "Die Datei \"%@\" wird nicht unterstützt und kann nicht geöffnet werden.";
|
||||
"Show Supported Files Only" = "Nur unterstützte Dateien anzeigen";
|
||||
|
|
|
|||
|
|
@ -163,3 +163,8 @@
|
|||
"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.";
|
||||
"Close All Tabs" = "Close All Tabs";
|
||||
"Are you sure you want to close all tabs?" = "Are you sure you want to close all tabs?";
|
||||
"Can’t Open File" = "Can’t Open File";
|
||||
"The file \"%@\" is not supported and can’t be opened." = "The file \"%@\" is not supported and can’t be opened.";
|
||||
"Show Supported Files Only" = "Show Supported Files Only";
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
|
||||
> Status: **active release**
|
||||
> Latest release: **v0.5.0**
|
||||
> Latest release: **v0.5.1**
|
||||
> Platform target: **macOS 26 (Tahoe)** compatible with **macOS Sequoia**
|
||||
> Apple Silicon: tested / Intel: not tested
|
||||
> Last updated (README): **2026-03-08** for release line **v0.5.0**
|
||||
|
|
@ -125,7 +125,7 @@ Prebuilt binaries are available on [GitHub Releases](https://github.com/h3pdesig
|
|||
Best for direct notarized builds and fastest access to new stable versions.
|
||||
|
||||
- Download: [GitHub Releases](https://github.com/h3pdesign/Neon-Vision-Editor/releases)
|
||||
- Latest release: **v0.5.0**
|
||||
- Latest release: **v0.5.1**
|
||||
- Channel: **Stable**
|
||||
- Architecture: Apple Silicon (Intel not tested)
|
||||
|
||||
|
|
@ -398,6 +398,14 @@ All shortcuts use `Cmd` (`⌘`). iPad/iOS require a hardware keyboard.
|
|||
|
||||
## Changelog
|
||||
|
||||
### v0.5.1 (summary)
|
||||
|
||||
- Added bulk `Close All Tabs` actions to toolbar surfaces (macOS, iOS, iPadOS), including a confirmation step before closing.
|
||||
- Added project-structure quick actions to expand all folders or collapse all folders in one step.
|
||||
- Added six vivid neon syntax themes with distinct color profiles: `Neon Voltage`, `Laserwave`, `Cyber Lime`, `Plasma Storm`, `Inferno Neon`, and `Ultraviolet Flux`.
|
||||
- Added a lock-safe cross-platform build matrix helper script (`scripts/ci/build_platform_matrix.sh`) to run macOS + iOS Simulator + iPad Simulator builds sequentially.
|
||||
- Added iPhone Markdown preview as a bottom sheet with toolbar toggle and resizable detents for Apple-guideline-compliant height control.
|
||||
|
||||
### v0.5.0 (summary)
|
||||
|
||||
- Added updater staging hardening with retry/fallback behavior and staged-bundle integrity checks.
|
||||
|
|
@ -414,14 +422,6 @@ All shortcuts use `Cmd` (`⌘`). iPad/iOS require a hardware keyboard.
|
|||
- iOS/iPad settings cards were visually simplified by removing accent stripe lines across tabs.
|
||||
- Wrapped-line numbering on iOS/iPad now uses sticky logical line numbers instead of repeating on every visual wrap row.
|
||||
|
||||
### v0.4.33 (summary)
|
||||
|
||||
- Added performance instrumentation for startup first-paint/first-keystroke and file-open latency in debug builds.
|
||||
- Added iPad hardware-keyboard shortcut bridging for New Tab, Open, Save, Find, Find in Files, and Command Palette.
|
||||
- Added local runtime reliability monitoring with previous-run crash bucketing and main-thread stall watchdog logging in debug.
|
||||
- Improved command palette behavior with fuzzy matching, command entries, and recent-selection ranking.
|
||||
- Improved large-file responsiveness by forcing throttle mode during load/import and reevaluating after idle.
|
||||
|
||||
Full release history: [`CHANGELOG.md`](CHANGELOG.md)
|
||||
|
||||
## Known Limitations
|
||||
|
|
@ -441,12 +441,12 @@ Full release history: [`CHANGELOG.md`](CHANGELOG.md)
|
|||
|
||||
## Release Integrity
|
||||
|
||||
- Tag: `v0.5.0`
|
||||
- Tag: `v0.5.1`
|
||||
- Tagged commit: `1c31306`
|
||||
- Verify local tag target:
|
||||
|
||||
```bash
|
||||
git rev-parse --verify v0.5.0
|
||||
git rev-parse --verify v0.5.1
|
||||
```
|
||||
|
||||
- Verify downloaded artifact checksum locally:
|
||||
|
|
@ -485,6 +485,12 @@ cd Neon-Vision-Editor
|
|||
xcodebuild -project "Neon Vision Editor.xcodeproj" -scheme "Neon Vision Editor" -destination 'platform=macOS,name=My Mac' build
|
||||
```
|
||||
|
||||
Lock-safe cross-platform verification (sequential macOS + iOS Simulator + iPad Simulator):
|
||||
|
||||
```bash
|
||||
scripts/ci/build_platform_matrix.sh
|
||||
```
|
||||
|
||||
## Support & Feedback
|
||||
|
||||
- Questions and ideas: [GitHub Discussions](https://github.com/h3pdesign/Neon-Vision-Editor/discussions)
|
||||
|
|
|
|||
132
scripts/ci/build_platform_matrix.sh
Executable file
132
scripts/ci/build_platform_matrix.sh
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
PROJECT="${PROJECT:-Neon Vision Editor.xcodeproj}"
|
||||
SCHEME="${SCHEME:-Neon Vision Editor}"
|
||||
CONFIGURATION="${CONFIGURATION:-Debug}"
|
||||
CODE_SIGNING_ALLOWED="${CODE_SIGNING_ALLOWED:-NO}"
|
||||
LOCK_DIR="${LOCK_DIR:-/tmp/nve_xcodebuild.lock}"
|
||||
DERIVED_DATA_ROOT="${DERIVED_DATA_ROOT:-$ROOT/.DerivedDataMatrix}"
|
||||
RETRIES="${RETRIES:-2}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/ci/build_platform_matrix.sh [--keep-derived-data]
|
||||
|
||||
Runs build verification sequentially for:
|
||||
1) macOS
|
||||
2) iOS Simulator
|
||||
3) iPad Simulator target family
|
||||
|
||||
Environment overrides:
|
||||
PROJECT, SCHEME, CONFIGURATION, CODE_SIGNING_ALLOWED
|
||||
LOCK_DIR, DERIVED_DATA_ROOT, RETRIES
|
||||
EOF
|
||||
}
|
||||
|
||||
KEEP_DERIVED_DATA=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--keep-derived-data) KEEP_DERIVED_DATA=1 ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $arg" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
wait_for_lock() {
|
||||
local attempts=0
|
||||
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
||||
attempts=$((attempts + 1))
|
||||
if [[ "$attempts" -ge 120 ]]; then
|
||||
echo "Timed out waiting for build lock: $LOCK_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
release_lock() {
|
||||
rmdir "$LOCK_DIR" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
release_lock
|
||||
if [[ "$KEEP_DERIVED_DATA" -eq 0 ]]; then
|
||||
rm -rf "$DERIVED_DATA_ROOT"
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
is_lock_error() {
|
||||
local log_file="$1"
|
||||
rg -q "database is locked|build system has crashed|unable to attach DB|unexpected service error" "$log_file"
|
||||
}
|
||||
|
||||
run_build() {
|
||||
local name="$1"
|
||||
shift
|
||||
local platform_slug
|
||||
platform_slug="$(echo "$name" | tr '[:upper:] ' '[:lower:]_')"
|
||||
local derived_data_path="${DERIVED_DATA_ROOT}/${platform_slug}"
|
||||
local log_file="/tmp/nve_build_${platform_slug}.log"
|
||||
local attempt=0
|
||||
|
||||
rm -rf "$derived_data_path"
|
||||
|
||||
while :; do
|
||||
attempt=$((attempt + 1))
|
||||
echo "[$name] Attempt ${attempt}/${RETRIES}"
|
||||
|
||||
if xcodebuild \
|
||||
-project "$PROJECT" \
|
||||
-scheme "$SCHEME" \
|
||||
-configuration "$CONFIGURATION" \
|
||||
-derivedDataPath "$derived_data_path" \
|
||||
CODE_SIGNING_ALLOWED="$CODE_SIGNING_ALLOWED" \
|
||||
"$@" >"$log_file" 2>&1; then
|
||||
echo "[$name] BUILD SUCCEEDED"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$attempt" -lt "$RETRIES" ]] && is_lock_error "$log_file"; then
|
||||
echo "[$name] Detected lock/build-system transient. Retrying..."
|
||||
sleep 2
|
||||
rm -rf "$derived_data_path"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$name] BUILD FAILED (see $log_file)" >&2
|
||||
tail -n 40 "$log_file" >&2 || true
|
||||
return 1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_lock
|
||||
|
||||
run_build "macOS" \
|
||||
-destination "generic/platform=macOS" \
|
||||
build
|
||||
|
||||
run_build "iOS Simulator" \
|
||||
-sdk iphonesimulator \
|
||||
-destination "generic/platform=iOS Simulator" \
|
||||
build
|
||||
|
||||
run_build "iPad Simulator" \
|
||||
-sdk iphonesimulator \
|
||||
-destination "generic/platform=iOS Simulator" \
|
||||
TARGETED_DEVICE_FAMILY=2 \
|
||||
build
|
||||
|
||||
echo "Build matrix completed successfully."
|
||||
Loading…
Reference in a new issue