mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Prepare 0.5.0 quality milestone: updater, CSV safety, a11y, release gates
This commit is contained in:
parent
fc05bed461
commit
e1c0f1f617
7 changed files with 115 additions and 18 deletions
|
|
@ -361,7 +361,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 403;
|
||||
CURRENT_PROJECT_VERSION = 404;
|
||||
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 = 403;
|
||||
CURRENT_PROJECT_VERSION = 404;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
|
|||
|
|
@ -855,17 +855,63 @@ final class AppUpdateManager: ObservableObject {
|
|||
let stagedDir = root.appendingPathComponent("v\(safeVersion)-\(UUID().uuidString)", isDirectory: true)
|
||||
try fm.createDirectory(at: stagedDir, withIntermediateDirectories: true)
|
||||
let stagedAppURL = stagedDir.appendingPathComponent("Neon Vision Editor.app", isDirectory: true)
|
||||
do {
|
||||
if fm.fileExists(atPath: stagedAppURL.path) {
|
||||
try fm.removeItem(at: stagedAppURL)
|
||||
let expectedVersion = readBundleShortVersionString(of: appBundle)
|
||||
var lastError: Error?
|
||||
|
||||
for attempt in 1...2 {
|
||||
do {
|
||||
if fm.fileExists(atPath: stagedAppURL.path) {
|
||||
try fm.removeItem(at: stagedAppURL)
|
||||
}
|
||||
let (dittoStatus, dittoStderr) = runDittoCopy(from: appBundle, to: stagedAppURL)
|
||||
if dittoStatus == 0 {
|
||||
appendUpdaterLog("Staging via ditto succeeded (attempt \(attempt)).")
|
||||
} else {
|
||||
appendUpdaterLog("Staging via ditto failed (attempt \(attempt), exit \(dittoStatus)). \(dittoStderr)")
|
||||
try fm.copyItem(at: appBundle, to: stagedAppURL)
|
||||
appendUpdaterLog("Staging fallback via FileManager.copyItem succeeded (attempt \(attempt)).")
|
||||
}
|
||||
|
||||
guard try verifyCodeSignatureStrictCLI(of: stagedAppURL) else {
|
||||
throw UpdateError.installUnsupported("Staged app failed signature validation.")
|
||||
}
|
||||
if let expectedVersion {
|
||||
let stagedVersion = readBundleShortVersionString(of: stagedAppURL)
|
||||
guard stagedVersion == expectedVersion else {
|
||||
throw UpdateError.installUnsupported("Staged app version mismatch.")
|
||||
}
|
||||
}
|
||||
return stagedAppURL
|
||||
} catch {
|
||||
lastError = error
|
||||
appendUpdaterLog("Staging attempt \(attempt) failed: \(error.localizedDescription)")
|
||||
try? fm.removeItem(at: stagedAppURL)
|
||||
}
|
||||
try fm.copyItem(at: appBundle, to: stagedAppURL)
|
||||
appendUpdaterLog("Staging via FileManager.copyItem succeeded.")
|
||||
} catch {
|
||||
appendUpdaterLog("Staging copy failed: \(error.localizedDescription). Source: \(appBundle.path)")
|
||||
throw UpdateError.installUnsupported("Failed to stage downloaded app for background install.")
|
||||
}
|
||||
return stagedAppURL
|
||||
|
||||
appendUpdaterLog("Staging copy failed after retries. Source: \(appBundle.path)")
|
||||
if let lastError {
|
||||
throw lastError
|
||||
}
|
||||
throw UpdateError.installUnsupported("Failed to stage downloaded app for background install.")
|
||||
}
|
||||
|
||||
private nonisolated static func runDittoCopy(from sourceURL: URL, to destinationURL: URL) -> (Int32, String) {
|
||||
let process = Process()
|
||||
let stderrPipe = Pipe()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
|
||||
process.arguments = [sourceURL.path, destinationURL.path]
|
||||
process.standardError = stderrPipe
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let stderrText = String(data: stderrData, encoding: .utf8)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return (process.terminationStatus, stderrText)
|
||||
} catch {
|
||||
return (-1, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func writeInstallerScript(
|
||||
|
|
@ -906,7 +952,10 @@ final class AppUpdateManager: ObservableObject {
|
|||
done
|
||||
|
||||
/bin/rm -rf "$TMP"
|
||||
/usr/bin/ditto "$SRC" "$TMP"
|
||||
if ! /usr/bin/ditto "$SRC" "$TMP"; then
|
||||
echo "ditto failed; trying fallback copy."
|
||||
/bin/cp -R "$SRC" "$TMP"
|
||||
fi
|
||||
/bin/rm -rf "$OLD"
|
||||
if [ -e "$DST" ]; then
|
||||
/bin/mv "$DST" "$OLD"
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ struct AppUpdaterDialog: View {
|
|||
appUpdateManager.openUpdaterLog()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.accessibilityLabel("Show installer log")
|
||||
.accessibilityHint("Opens the updater log file for troubleshooting")
|
||||
#endif
|
||||
}
|
||||
.padding(14)
|
||||
|
|
@ -150,6 +152,9 @@ struct AppUpdaterDialog: View {
|
|||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("Update install progress")
|
||||
.accessibilityValue("\(Int((appUpdateManager.installProgress * 100).rounded())) percent")
|
||||
}
|
||||
|
||||
if let installMessage = appUpdateManager.installMessage {
|
||||
|
|
|
|||
|
|
@ -215,6 +215,8 @@ extension ContentView {
|
|||
Image(systemName: "plus.square.on.square")
|
||||
}
|
||||
.help("New Tab (Cmd+T)")
|
||||
.accessibilityLabel("New tab")
|
||||
.accessibilityHint("Creates a new editor tab")
|
||||
.keyboardShortcut("t", modifiers: .command)
|
||||
}
|
||||
|
||||
|
|
@ -224,6 +226,8 @@ extension ContentView {
|
|||
Image(systemName: "gearshape")
|
||||
}
|
||||
.help("Settings (Cmd+,)")
|
||||
.accessibilityLabel("Settings")
|
||||
.accessibilityHint("Opens app settings")
|
||||
.keyboardShortcut(",", modifiers: .command)
|
||||
}
|
||||
|
||||
|
|
@ -253,6 +257,8 @@ extension ContentView {
|
|||
}
|
||||
.labelsHidden()
|
||||
.help("Language")
|
||||
.accessibilityLabel("Language picker")
|
||||
.accessibilityHint("Choose syntax language for the current tab")
|
||||
.frame(width: isIPadToolbarLayout ? 112 : iPhoneLanguagePickerWidth)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.layoutPriority(2)
|
||||
|
|
@ -309,6 +315,8 @@ extension ContentView {
|
|||
Image(systemName: "folder")
|
||||
}
|
||||
.help("Open File… (Cmd+O)")
|
||||
.accessibilityLabel("Open file")
|
||||
.accessibilityHint("Opens a file picker")
|
||||
.keyboardShortcut("o", modifiers: .command)
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +336,8 @@ extension ContentView {
|
|||
}
|
||||
.disabled(viewModel.selectedTab == nil)
|
||||
.help("Save File (Cmd+S)")
|
||||
.accessibilityLabel("Save file")
|
||||
.accessibilityHint("Saves the current tab")
|
||||
.keyboardShortcut("s", modifiers: .command)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ private enum EditorRuntimeLimits {
|
|||
// Above this, keep editing responsive by skipping regex-heavy syntax passes.
|
||||
static let syntaxMinimalUTF16Length = 1_200_000
|
||||
static let htmlFastProfileUTF16Length = 250_000
|
||||
static let csvFastProfileUTF16Length = 180_000
|
||||
static let csvFastProfileUTF16Length = 120_000
|
||||
static let csvFastProfileLongLineUTF16 = 4_000
|
||||
static let csvFastProfileScanLimitUTF16 = 120_000
|
||||
static let scopeComputationMaxUTF16Length = 300_000
|
||||
static let cursorRehighlightMaxUTF16Length = 220_000
|
||||
static let nonImmediateHighlightMaxUTF16Length = 220_000
|
||||
|
|
@ -22,6 +24,27 @@ private enum EditorRuntimeLimits {
|
|||
static let bindingDebounceDelay: TimeInterval = 0.18
|
||||
}
|
||||
|
||||
private func shouldUseCSVFastProfile(_ nsText: NSString) -> Bool {
|
||||
if nsText.length >= EditorRuntimeLimits.csvFastProfileUTF16Length {
|
||||
return true
|
||||
}
|
||||
let scanLimit = min(nsText.length, EditorRuntimeLimits.csvFastProfileScanLimitUTF16)
|
||||
guard scanLimit > 0 else { return false }
|
||||
var currentLineLength = 0
|
||||
for idx in 0..<scanLimit {
|
||||
let codeUnit = nsText.character(at: idx)
|
||||
if codeUnit == 10 { // '\n'
|
||||
currentLineLength = 0
|
||||
continue
|
||||
}
|
||||
currentLineLength += 1
|
||||
if currentLineLength >= EditorRuntimeLimits.csvFastProfileLongLineUTF16 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
struct EditorTextMutation {
|
||||
let documentID: UUID
|
||||
let range: NSRange
|
||||
|
|
@ -2501,7 +2524,7 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
if lower == "html" && nsText.length >= EditorRuntimeLimits.htmlFastProfileUTF16Length {
|
||||
return .htmlFast
|
||||
}
|
||||
if lower == "csv" && nsText.length >= EditorRuntimeLimits.csvFastProfileUTF16Length {
|
||||
if lower == "csv" && shouldUseCSVFastProfile(nsText) {
|
||||
return .csvFast
|
||||
}
|
||||
return .full
|
||||
|
|
@ -3726,7 +3749,7 @@ struct CustomTextEditor: UIViewRepresentable {
|
|||
if lower == "html" && nsText.length >= EditorRuntimeLimits.htmlFastProfileUTF16Length {
|
||||
return .htmlFast
|
||||
}
|
||||
if lower == "csv" && nsText.length >= EditorRuntimeLimits.csvFastProfileUTF16Length {
|
||||
if lower == "csv" && shouldUseCSVFastProfile(nsText) {
|
||||
return .csvFast
|
||||
}
|
||||
return .full
|
||||
|
|
|
|||
|
|
@ -199,9 +199,12 @@ Availability legend: `Full` = complete support, `Partial` = available with platf
|
|||
|
||||
## Roadmap (Near Term)
|
||||
|
||||
- Improve iPad settings layout density and reduce scrolling friction. Tracking: [#12](https://github.com/h3pdesign/Neon-Vision-Editor/issues/12)
|
||||
- Expand Markdown preview parity and interaction stability across Apple platforms. Tracking: [#13](https://github.com/h3pdesign/Neon-Vision-Editor/issues/13)
|
||||
- Improve toolbar consistency and action discoverability across window sizes. Tracking: [#14](https://github.com/h3pdesign/Neon-Vision-Editor/issues/14)
|
||||
- 0.5.0 milestone: quality + trust release (updater reliability, CSV safety, cross-platform polish, accessibility, release gating). Tracking: [Milestone 0.5.0](https://github.com/h3pdesign/Neon-Vision-Editor/milestone/1)
|
||||
- Auto-update reliability hardening. Tracking: [#36](https://github.com/h3pdesign/Neon-Vision-Editor/issues/36)
|
||||
- CSV/large-file safety and table-mode path. Tracking: [#25](https://github.com/h3pdesign/Neon-Vision-Editor/issues/25), [#26](https://github.com/h3pdesign/Neon-Vision-Editor/issues/26)
|
||||
- Toolbar consistency and action discoverability across sizes. Tracking: [#14](https://github.com/h3pdesign/Neon-Vision-Editor/issues/14)
|
||||
- Accessibility completion pass (VoiceOver + keyboard focus). Tracking: [#37](https://github.com/h3pdesign/Neon-Vision-Editor/issues/37)
|
||||
- Release engineering lock-in checks for 0.5.0. Tracking: [#38](https://github.com/h3pdesign/Neon-Vision-Editor/issues/38)
|
||||
|
||||
## Known Issues
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,17 @@ if grep -nE "^- TODO$" /tmp/release-notes-"$TAG".md >/dev/null; then
|
|||
echo "CHANGELOG section for ${TAG} still contains TODO entries." >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -nEi "\bTODO\b" /tmp/release-notes-"$TAG".md >/dev/null; then
|
||||
echo "CHANGELOG section for ${TAG} still contains unresolved TODO markers." >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -nE "^> Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null
|
||||
grep -nE "^- Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null
|
||||
grep -nE "^### ${TAG} \\(summary\\)\\r?$" README.md >/dev/null
|
||||
|
||||
echo "Validating README download metrics freshness..."
|
||||
scripts/update_download_metrics.py --check
|
||||
|
||||
SAFE_TAG="$(echo "$TAG" | tr -c 'A-Za-z0-9_' '_')"
|
||||
WORK_DIR="/tmp/nve_release_preflight_${SAFE_TAG}"
|
||||
rm -rf "$WORK_DIR"
|
||||
|
|
|
|||
Loading…
Reference in a new issue