mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Implement core 0.5.2 milestone items
This commit is contained in:
parent
111dc5fed4
commit
9d8cc44922
7 changed files with 236 additions and 39 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -6,6 +6,18 @@ The format follows *Keep a Changelog*. Versions use semantic versioning with pre
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Added editor performance presets in Settings (`Balanced`, `Large Files`, `Battery`) with shared runtime mapping.
|
||||
- Added configurable project navigator placement (`Left`/`Right`) for project-structure sidebar layout.
|
||||
- Added richer updater diagnostics details in Settings: staged update summary, last install-attempt summary, and recent sanitized log snippet.
|
||||
|
||||
### Improved
|
||||
- Improved iOS/iPadOS large-file responsiveness by lowering automatic large-file thresholds and applying preset-based tuning.
|
||||
- Improved project-sidebar open flow by short-circuiting redundant opens when the selected file is already active.
|
||||
|
||||
### Fixed
|
||||
- Fixed missing diagnostics reset workflow by adding a dedicated `Clear Diagnostics` action that also clears file-open timing snapshots.
|
||||
|
||||
## [v0.5.1] - 2026-03-08
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 444;
|
||||
CURRENT_PROJECT_VERSION = 445;
|
||||
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 = 444;
|
||||
CURRENT_PROJECT_VERSION = 445;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
|
|||
|
|
@ -388,6 +388,49 @@ final class AppUpdateManager: ObservableObject {
|
|||
installDispatchScheduled = false
|
||||
}
|
||||
|
||||
var stagedUpdateVersionSummary: String {
|
||||
let stagedURL = preparedUpdateAppURL ?? defaults.string(forKey: Self.stagedUpdatePathKey).map(URL.init(fileURLWithPath:))
|
||||
guard let stagedURL else { return "None" }
|
||||
let version = Self.readBundleShortVersionString(of: stagedURL) ?? "unknown"
|
||||
return "v\(version)"
|
||||
}
|
||||
|
||||
var lastInstallAttemptSummary: String {
|
||||
if let installMessage, !installMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return Self.sanitizedDiagnosticSummary(installMessage)
|
||||
}
|
||||
return "No install attempt yet."
|
||||
}
|
||||
|
||||
var recentLogSnippet: String {
|
||||
let fm = FileManager.default
|
||||
guard let existing = updaterLogFileCandidates.first(where: { fm.fileExists(atPath: $0.path) }),
|
||||
let raw = try? String(contentsOf: existing, encoding: .utf8) else {
|
||||
return "No updater log available yet."
|
||||
}
|
||||
let lines = raw
|
||||
.split(whereSeparator: \.isNewline)
|
||||
.suffix(8)
|
||||
.map(String.init)
|
||||
.map(Self.sanitizedDiagnosticSummary)
|
||||
if lines.isEmpty {
|
||||
return "No updater log entries yet."
|
||||
}
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
func resetDiagnostics() {
|
||||
clearInstallMessage()
|
||||
errorMessage = nil
|
||||
lastCheckedAt = nil
|
||||
lastCheckResultSummary = "Never checked"
|
||||
defaults.removeObject(forKey: Self.lastCheckedAtKey)
|
||||
defaults.removeObject(forKey: Self.lastCheckSummaryKey)
|
||||
defaults.removeObject(forKey: Self.pauseUntilKey)
|
||||
defaults.removeObject(forKey: Self.consecutiveFailuresKey)
|
||||
defaults.removeObject(forKey: Self.stagedUpdatePathKey)
|
||||
}
|
||||
|
||||
func installUpdateNow() async {
|
||||
if let reason = installNowDisabledReason {
|
||||
installMessage = reason
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ final class EditorPerformanceMonitor {
|
|||
return Array(decoded.suffix(clamped))
|
||||
}
|
||||
|
||||
func clearRecentFileOpenEvents() {
|
||||
defaults.removeObject(forKey: eventsDefaultsKey)
|
||||
}
|
||||
|
||||
private func storeFileOpenEvent(_ event: FileOpenEvent) {
|
||||
var existing = recentFileOpenEvents(limit: maxEvents)
|
||||
existing.append(event)
|
||||
|
|
|
|||
|
|
@ -679,6 +679,9 @@ extension ContentView {
|
|||
presentUnsupportedFileAlert(for: url)
|
||||
return
|
||||
}
|
||||
if viewModel.selectedTab?.fileURL?.standardizedFileURL == url.standardizedFileURL {
|
||||
return
|
||||
}
|
||||
if let existing = viewModel.tabs.first(where: { $0.fileURL?.standardizedFileURL == url.standardizedFileURL }) {
|
||||
viewModel.selectTab(id: existing.id)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -104,6 +104,21 @@ struct ContentView: View {
|
|||
case forceBlankDocument
|
||||
}
|
||||
|
||||
enum ProjectNavigatorPlacement: String, CaseIterable, Identifiable {
|
||||
case leading
|
||||
case trailing
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
enum PerformancePreset: String, CaseIterable, Identifiable {
|
||||
case balanced
|
||||
case largeFiles
|
||||
case battery
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
let startupBehavior: StartupBehavior
|
||||
|
||||
init(startupBehavior: StartupBehavior = .standard) {
|
||||
|
|
@ -113,9 +128,13 @@ struct ContentView: View {
|
|||
private enum EditorPerformanceThresholds {
|
||||
static let largeFileBytes = 12_000_000
|
||||
static let largeFileBytesHTMLCSV = 4_000_000
|
||||
static let largeFileBytesMobile = 8_000_000
|
||||
static let largeFileBytesHTMLCSVMobile = 3_000_000
|
||||
static let heavyFeatureUTF16Length = 450_000
|
||||
static let largeFileLineBreaks = 40_000
|
||||
static let largeFileLineBreaksHTMLCSV = 15_000
|
||||
static let largeFileLineBreaksMobile = 25_000
|
||||
static let largeFileLineBreaksHTMLCSVMobile = 10_000
|
||||
}
|
||||
private static let completionSignposter = OSSignposter(subsystem: "h3p.Neon-Vision-Editor", category: "InlineCompletion")
|
||||
|
||||
|
|
@ -266,6 +285,8 @@ struct ContentView: View {
|
|||
@State var droppedFileLoadProgress: Double = 0
|
||||
@State var droppedFileLoadLabel: String = ""
|
||||
@State var largeFileModeEnabled: Bool = false
|
||||
@AppStorage("SettingsProjectNavigatorPlacement") var projectNavigatorPlacementRaw: String = ProjectNavigatorPlacement.trailing.rawValue
|
||||
@AppStorage("SettingsPerformancePreset") var performancePresetRaw: String = PerformancePreset.balanced.rawValue
|
||||
#if os(iOS)
|
||||
@AppStorage("SettingsForceLargeFileMode") var forceLargeFileMode: Bool = false
|
||||
@AppStorage("SettingsShowKeyboardAccessoryBarIOS") var showKeyboardAccessoryBarIOS: Bool = false
|
||||
|
|
@ -314,6 +335,14 @@ struct ContentView: View {
|
|||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
private var projectNavigatorPlacement: ProjectNavigatorPlacement {
|
||||
ProjectNavigatorPlacement(rawValue: projectNavigatorPlacementRaw) ?? .trailing
|
||||
}
|
||||
|
||||
private var performancePreset: PerformancePreset {
|
||||
PerformancePreset(rawValue: performancePresetRaw) ?? .balanced
|
||||
}
|
||||
#if os(macOS)
|
||||
private enum MacTranslucencyMode: String {
|
||||
case subtle
|
||||
|
|
@ -1484,12 +1513,31 @@ struct ContentView: View {
|
|||
let isHTMLLike = ["html", "htm", "xml", "svg", "xhtml"].contains(lowerLanguage)
|
||||
let isCSVLike = ["csv", "tsv"].contains(lowerLanguage)
|
||||
let useAggressiveThresholds = isHTMLLike || isCSVLike
|
||||
let byteThreshold = useAggressiveThresholds
|
||||
#if os(iOS)
|
||||
var byteThreshold = useAggressiveThresholds
|
||||
? EditorPerformanceThresholds.largeFileBytesHTMLCSVMobile
|
||||
: EditorPerformanceThresholds.largeFileBytesMobile
|
||||
var lineThreshold = useAggressiveThresholds
|
||||
? EditorPerformanceThresholds.largeFileLineBreaksHTMLCSVMobile
|
||||
: EditorPerformanceThresholds.largeFileLineBreaksMobile
|
||||
#else
|
||||
var byteThreshold = useAggressiveThresholds
|
||||
? EditorPerformanceThresholds.largeFileBytesHTMLCSV
|
||||
: EditorPerformanceThresholds.largeFileBytes
|
||||
let lineThreshold = useAggressiveThresholds
|
||||
var lineThreshold = useAggressiveThresholds
|
||||
? EditorPerformanceThresholds.largeFileLineBreaksHTMLCSV
|
||||
: EditorPerformanceThresholds.largeFileLineBreaks
|
||||
#endif
|
||||
switch performancePreset {
|
||||
case .balanced:
|
||||
break
|
||||
case .largeFiles:
|
||||
byteThreshold = max(1_000_000, Int(Double(byteThreshold) * 0.75))
|
||||
lineThreshold = max(5_000, Int(Double(lineThreshold) * 0.75))
|
||||
case .battery:
|
||||
byteThreshold = max(750_000, Int(Double(byteThreshold) * 0.55))
|
||||
lineThreshold = max(3_000, Int(Double(lineThreshold) * 0.55))
|
||||
}
|
||||
let byteCount = text.utf8.count
|
||||
let exceedsByteThreshold = byteCount >= byteThreshold
|
||||
let exceedsLineThreshold: Bool = {
|
||||
|
|
@ -3323,6 +3371,37 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
///MARK: - Main Editor Stack
|
||||
@ViewBuilder
|
||||
private var projectStructureSidebarPanel: some View {
|
||||
#if os(macOS)
|
||||
VStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.fill(macChromeBackgroundStyle)
|
||||
.frame(height: macTabBarStripHeight)
|
||||
projectStructureSidebarBody
|
||||
}
|
||||
.frame(minWidth: 220, idealWidth: 260, maxWidth: 340)
|
||||
#else
|
||||
projectStructureSidebarBody
|
||||
.frame(minWidth: 220, idealWidth: 260, maxWidth: 340)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var projectStructureSidebarBody: some View {
|
||||
ProjectStructureSidebarView(
|
||||
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() }
|
||||
)
|
||||
}
|
||||
|
||||
var editorView: some View {
|
||||
@Bindable var bindableViewModel = viewModel
|
||||
let shouldThrottleFeatures = shouldThrottleHeavyEditorFeatures()
|
||||
|
|
@ -3330,6 +3409,10 @@ struct ContentView: View {
|
|||
let effectiveScopeGuides = showScopeGuides && !shouldThrottleFeatures
|
||||
let effectiveScopeBackground = highlightScopeBackground && !shouldThrottleFeatures
|
||||
let content = HStack(spacing: 0) {
|
||||
if showProjectStructureSidebar && projectNavigatorPlacement == .leading && !brainDumpLayoutEnabled {
|
||||
projectStructureSidebarPanel
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
if !useIPhoneUnifiedTopHost && !brainDumpLayoutEnabled {
|
||||
tabBarView
|
||||
|
|
@ -3415,41 +3498,8 @@ struct ContentView: View {
|
|||
.frame(minWidth: 280, idealWidth: 420, maxWidth: 680, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
if showProjectStructureSidebar && !brainDumpLayoutEnabled {
|
||||
#if os(macOS)
|
||||
VStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.fill(macChromeBackgroundStyle)
|
||||
.frame(height: macTabBarStripHeight)
|
||||
ProjectStructureSidebarView(
|
||||
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() }
|
||||
)
|
||||
}
|
||||
.frame(minWidth: 220, idealWidth: 260, maxWidth: 340)
|
||||
#else
|
||||
ProjectStructureSidebarView(
|
||||
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() }
|
||||
)
|
||||
.frame(minWidth: 220, idealWidth: 260, maxWidth: 340)
|
||||
#endif
|
||||
if showProjectStructureSidebar && projectNavigatorPlacement == .trailing && !brainDumpLayoutEnabled {
|
||||
projectStructureSidebarPanel
|
||||
}
|
||||
}
|
||||
.background(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ struct NeonSettingsView: View {
|
|||
@AppStorage("SettingsAutoCloseBrackets") private var autoCloseBrackets: Bool = false
|
||||
@AppStorage("SettingsTrimTrailingWhitespace") private var trimTrailingWhitespace: Bool = false
|
||||
@AppStorage("SettingsTrimWhitespaceForSyntaxDetection") private var trimWhitespaceForSyntaxDetection: Bool = false
|
||||
@AppStorage("SettingsProjectNavigatorPlacement") private var projectNavigatorPlacementRaw: String = ContentView.ProjectNavigatorPlacement.trailing.rawValue
|
||||
@AppStorage("SettingsPerformancePreset") private var performancePresetRaw: String = ContentView.PerformancePreset.balanced.rawValue
|
||||
|
||||
@AppStorage("SettingsCompletionEnabled") private var completionEnabled: Bool = false
|
||||
@AppStorage("SettingsCompletionFromDocument") private var completionFromDocument: Bool = false
|
||||
|
|
@ -990,6 +992,34 @@ struct NeonSettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
settingsCardSection(
|
||||
title: "Layout",
|
||||
icon: "sidebar.left",
|
||||
emphasis: .secondary
|
||||
) {
|
||||
Picker("Project Navigator Position", selection: $projectNavigatorPlacementRaw) {
|
||||
Text("Left").tag(ContentView.ProjectNavigatorPlacement.leading.rawValue)
|
||||
Text("Right").tag(ContentView.ProjectNavigatorPlacement.trailing.rawValue)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
settingsCardSection(
|
||||
title: "Performance",
|
||||
icon: "speedometer",
|
||||
emphasis: .secondary
|
||||
) {
|
||||
Picker("Preset", selection: $performancePresetRaw) {
|
||||
Text("Balanced").tag(ContentView.PerformancePreset.balanced.rawValue)
|
||||
Text("Large Files").tag(ContentView.PerformancePreset.largeFiles.rawValue)
|
||||
Text("Battery").tag(ContentView.PerformancePreset.battery.rawValue)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Text("Balanced keeps default behavior. Large Files and Battery enter performance mode earlier.")
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
settingsCardSection(
|
||||
title: "Editing",
|
||||
icon: "keyboard",
|
||||
|
|
@ -1057,6 +1087,36 @@ struct NeonSettingsView: View {
|
|||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: UI.space10) {
|
||||
Text("Layout")
|
||||
.font(Typography.sectionHeadline)
|
||||
Picker("Project Navigator Position", selection: $projectNavigatorPlacementRaw) {
|
||||
Text("Left").tag(ContentView.ProjectNavigatorPlacement.leading.rawValue)
|
||||
Text("Right").tag(ContentView.ProjectNavigatorPlacement.trailing.rawValue)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: UI.space10) {
|
||||
Text("Performance")
|
||||
.font(Typography.sectionHeadline)
|
||||
Picker("Preset", selection: $performancePresetRaw) {
|
||||
Text("Balanced").tag(ContentView.PerformancePreset.balanced.rawValue)
|
||||
Text("Large Files").tag(ContentView.PerformancePreset.largeFiles.rawValue)
|
||||
Text("Battery").tag(ContentView.PerformancePreset.battery.rawValue)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Text("Balanced keeps default behavior. Large Files and Battery enter performance mode earlier.")
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: UI.space10) {
|
||||
Text("Editing")
|
||||
.font(Typography.sectionHeadline)
|
||||
|
|
@ -1702,6 +1762,22 @@ struct NeonSettingsView: View {
|
|||
.font(Typography.footnote)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
Text("Staged update: \(appUpdateManager.stagedUpdateVersionSummary)")
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Last install attempt: \(appUpdateManager.lastInstallAttemptSummary)")
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("Recent updater log")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
ScrollView {
|
||||
Text(appUpdateManager.recentLogSnippet)
|
||||
.font(Typography.monoBody)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.frame(maxHeight: 140)
|
||||
|
||||
Divider()
|
||||
|
||||
|
|
@ -1738,6 +1814,11 @@ struct NeonSettingsView: View {
|
|||
copyDiagnosticsToClipboard()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
Button("Clear Diagnostics") {
|
||||
appUpdateManager.resetDiagnostics()
|
||||
EditorPerformanceMonitor.shared.clearRecentFileOpenEvents()
|
||||
diagnosticsCopyStatus = "Cleared"
|
||||
}
|
||||
if !diagnosticsCopyStatus.isEmpty {
|
||||
Text(diagnosticsCopyStatus)
|
||||
.font(Typography.footnote)
|
||||
|
|
@ -1754,10 +1835,14 @@ struct NeonSettingsView: View {
|
|||
lines.append("Timestamp: \(Date().formatted(date: .abbreviated, time: .shortened))")
|
||||
lines.append("Updater.lastCheckResult: \(AppUpdateManager.sanitizedDiagnosticSummary(appUpdateManager.lastCheckResultSummary))")
|
||||
lines.append("Updater.lastCheckedAt: \(appUpdateManager.lastCheckedAt?.formatted(date: .abbreviated, time: .shortened) ?? "never")")
|
||||
lines.append("Updater.stagedVersion: \(appUpdateManager.stagedUpdateVersionSummary)")
|
||||
lines.append("Updater.lastInstallAttempt: \(AppUpdateManager.sanitizedDiagnosticSummary(appUpdateManager.lastInstallAttemptSummary))")
|
||||
if let pausedUntil = appUpdateManager.pausedUntil, pausedUntil > Date() {
|
||||
lines.append("Updater.pauseUntil: \(pausedUntil.formatted(date: .abbreviated, time: .shortened))")
|
||||
}
|
||||
lines.append("Updater.consecutiveFailures: \(appUpdateManager.consecutiveFailureCount)")
|
||||
lines.append("Updater.logSnippet:")
|
||||
lines.append(appUpdateManager.recentLogSnippet)
|
||||
lines.append("FileOpenEvents.count: \(events.count)")
|
||||
for event in events {
|
||||
lines.append(
|
||||
|
|
|
|||
Loading…
Reference in a new issue