Fix file open/restore reliability and markdown detection

This commit is contained in:
h3p 2026-02-23 09:02:15 +01:00
parent cb5bf19470
commit 23809800d2
6 changed files with 159 additions and 10 deletions

View file

@ -21,11 +21,13 @@
<string>phtml</string>
<string>csv</string>
<string>tsv</string>
<string>txt</string>
<string>toml</string>
<string>ini</string>
<string>yaml</string>
<string>yml</string>
<string>xml</string>
<string>plist</string>
<string>sql</string>
<string>log</string>
<string>vim</string>
@ -40,6 +42,9 @@
<string>psm1</string>
<string>html</string>
<string>htm</string>
<string>ee</string>
<string>exp</string>
<string>tmpl</string>
<string>css</string>
<string>c</string>
<string>cpp</string>
@ -69,6 +74,13 @@
<string>conf</string>
<string>nginx</string>
</array>
<key>LSItemContentTypes</key>
<array>
<string>public.plain-text</string>
<string>public.text</string>
<string>public.source-code</string>
<string>net.daringfireball.markdown</string>
</array>
<key>CFBundleTypeName</key>
<string>Neon Vision Editor Document</string>
<key>CFBundleTypeRole</key>

View file

@ -358,7 +358,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 323;
CURRENT_PROJECT_VERSION = 324;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -439,7 +439,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 323;
CURRENT_PROJECT_VERSION = 324;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;

View file

@ -22,6 +22,7 @@ public struct LanguageDetector {
"phtml": "php",
"csv": "csv",
"tsv": "csv",
"txt": "plain",
"toml": "toml",
"ini": "ini",
"yaml": "yaml",
@ -126,15 +127,21 @@ public struct LanguageDetector {
}
// Extension/dotfile hint
var extensionHint: String?
if let byURL = preferredLanguage(for: fileURL) {
extensionHint = byURL
bump(byURL, 300)
} else if let name {
let lowerName = name.lowercased()
if let mapped = dotfileMap[lowerName] {
extensionHint = mapped
bump(mapped, 300)
} else {
let ext = URL(fileURLWithPath: lowerName).pathExtension.lowercased()
if let mapped = extensionMap[ext] { bump(mapped, 300) }
if let mapped = extensionMap[ext] {
extensionHint = mapped
bump(mapped, 300)
}
}
}
@ -341,7 +348,11 @@ public struct LanguageDetector {
let second = sorted.dropFirst().first ?? ("plain", 0)
let confidence = max(0, top.value - second.value)
let minScore = 10
let lang = top.value >= minScore ? top.key : "plain"
var lang = top.value >= minScore ? top.key : "plain"
// Keep explicit Markdown filenames stable even if body text contains SQL-like terms.
if extensionHint == "markdown" {
lang = "markdown"
}
return Result(lang: lang, scores: scores, confidence: confidence)
}

View file

@ -383,6 +383,7 @@ class EditorViewModel: ObservableObject {
"phtml": "php",
"csv": "csv",
"tsv": "csv",
"txt": "plain",
"toml": "toml",
"ini": "ini",
"yaml": "yaml",

View file

@ -1686,6 +1686,9 @@ struct ContentView: View {
persistUnsavedDraftSnapshotIfNeeded()
#endif
}
.onOpenURL { url in
viewModel.openFile(url: url)
}
#if os(iOS)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
persistSessionIfReady()
@ -2014,32 +2017,141 @@ struct ContentView: View {
UserDefaults.standard.set(viewModel.selectedTab?.fileURL?.absoluteString, forKey: "LastSessionSelectedFileURL")
#if os(iOS)
persistLastSessionSecurityScopedBookmarks(fileURLs: fileURLs, selectedURL: viewModel.selectedTab?.fileURL)
#elseif os(macOS)
persistLastSessionSecurityScopedBookmarksMac(fileURLs: fileURLs, selectedURL: viewModel.selectedTab?.fileURL)
#endif
}
private func restoredLastSessionFileURLs() -> [URL] {
#if os(iOS)
#if os(macOS)
let bookmarked = restoreSessionURLsFromSecurityScopedBookmarksMac()
if !bookmarked.isEmpty {
return bookmarked
}
#elseif os(iOS)
let bookmarked = restoreSessionURLsFromSecurityScopedBookmarks()
if !bookmarked.isEmpty {
return bookmarked
}
#endif
let paths = UserDefaults.standard.stringArray(forKey: "LastSessionFileURLs") ?? []
return paths.compactMap(URL.init(string:))
let stored = UserDefaults.standard.stringArray(forKey: "LastSessionFileURLs") ?? []
var urls: [URL] = []
var seen: Set<String> = []
for raw in stored {
guard let parsed = restoredSessionURL(from: raw) else { continue }
let standardized = parsed.standardizedFileURL
// Only restore files that still exist; avoids empty placeholder tabs on launch.
guard FileManager.default.fileExists(atPath: standardized.path) else { continue }
let key = standardized.absoluteString
if seen.insert(key).inserted {
urls.append(standardized)
}
}
return urls
}
private func restoredLastSessionSelectedFileURL() -> URL? {
#if os(iOS)
#if os(macOS)
if let bookmarked = restoreSelectedURLFromSecurityScopedBookmarkMac() {
return bookmarked
}
#elseif os(iOS)
if let bookmarked = restoreSelectedURLFromSecurityScopedBookmark() {
return bookmarked
}
#endif
guard let selectedPath = UserDefaults.standard.string(forKey: "LastSessionSelectedFileURL") else {
guard let selectedPath = UserDefaults.standard.string(forKey: "LastSessionSelectedFileURL"),
let selectedURL = restoredSessionURL(from: selectedPath) else {
return nil
}
return URL(string: selectedPath)
let standardized = selectedURL.standardizedFileURL
return FileManager.default.fileExists(atPath: standardized.path) ? standardized : nil
}
private func restoredSessionURL(from raw: String) -> URL? {
// Support both absolute URL strings ("file:///...") and legacy plain paths.
if let url = URL(string: raw), url.isFileURL {
return url
}
if raw.hasPrefix("/") {
return URL(fileURLWithPath: raw)
}
return nil
}
#if os(macOS)
private var macLastSessionBookmarksKey: String { "MacLastSessionFileBookmarks" }
private var macLastSessionSelectedBookmarkKey: String { "MacLastSessionSelectedFileBookmark" }
private func persistLastSessionSecurityScopedBookmarksMac(fileURLs: [URL], selectedURL: URL?) {
let bookmarkData = fileURLs.compactMap { makeSecurityScopedBookmarkDataMac(for: $0) }
UserDefaults.standard.set(bookmarkData, forKey: macLastSessionBookmarksKey)
if let selectedURL, let selectedData = makeSecurityScopedBookmarkDataMac(for: selectedURL) {
UserDefaults.standard.set(selectedData, forKey: macLastSessionSelectedBookmarkKey)
} else {
UserDefaults.standard.removeObject(forKey: macLastSessionSelectedBookmarkKey)
}
}
private func restoreSessionURLsFromSecurityScopedBookmarksMac() -> [URL] {
guard let saved = UserDefaults.standard.array(forKey: macLastSessionBookmarksKey) as? [Data], !saved.isEmpty else {
return []
}
var urls: [URL] = []
var seen: Set<String> = []
for data in saved {
guard let url = resolveSecurityScopedBookmarkMac(data) else { continue }
let standardized = url.standardizedFileURL
guard FileManager.default.fileExists(atPath: standardized.path) else { continue }
let key = standardized.absoluteString
if seen.insert(key).inserted {
urls.append(standardized)
}
}
return urls
}
private func restoreSelectedURLFromSecurityScopedBookmarkMac() -> URL? {
guard let data = UserDefaults.standard.data(forKey: macLastSessionSelectedBookmarkKey),
let resolved = resolveSecurityScopedBookmarkMac(data) else {
return nil
}
let standardized = resolved.standardizedFileURL
return FileManager.default.fileExists(atPath: standardized.path) ? standardized : nil
}
private func makeSecurityScopedBookmarkDataMac(for url: URL) -> Data? {
let didStartScopedAccess = url.startAccessingSecurityScopedResource()
defer {
if didStartScopedAccess {
url.stopAccessingSecurityScopedResource()
}
}
do {
return try url.bookmarkData(
options: [.withSecurityScope],
includingResourceValuesForKeys: nil,
relativeTo: nil
)
} catch {
return nil
}
}
private func resolveSecurityScopedBookmarkMac(_ data: Data) -> URL? {
var isStale = false
guard let resolved = try? URL(
resolvingBookmarkData: data,
options: [.withSecurityScope, .withoutUI],
relativeTo: nil,
bookmarkDataIsStale: &isStale
) else {
return nil
}
return resolved
}
#endif
#if os(iOS)
private var unsavedDraftSnapshotKey: String { "IOSUnsavedDraftSnapshotV1" }
private var lastSessionBookmarksKey: String { "LastSessionFileBookmarks" }

View file

@ -23,6 +23,7 @@ final class LanguageDetectorTests: XCTestCase {
("main.yml", "yaml"),
("main.toml", "toml"),
("main.csv", "csv"),
("main.txt", "plain"),
("main.ini", "ini"),
("main.md", "markdown"),
("main.proto", "proto"),
@ -104,4 +105,16 @@ final class LanguageDetectorTests: XCTestCase {
let result = LanguageDetector.shared.detect(text: "", name: nil, fileURL: nil)
XCTAssertEqual(result.lang, "plain")
}
func testMarkdownExtensionNotOverriddenBySQLHeuristics() {
let text = """
# History vision
Concrete API plan:
SELECT endpoint, method FROM api_catalog WHERE active = 1;
"""
let url = URL(fileURLWithPath: "/tmp/History vision Concrete API plan.md")
let result = LanguageDetector.shared.detect(text: text, name: url.lastPathComponent, fileURL: url)
XCTAssertEqual(result.lang, "markdown")
}
}