mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Fix file open/restore reliability and markdown detection
This commit is contained in:
parent
cb5bf19470
commit
23809800d2
6 changed files with 159 additions and 10 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -383,6 +383,7 @@ class EditorViewModel: ObservableObject {
|
|||
"phtml": "php",
|
||||
"csv": "csv",
|
||||
"tsv": "csv",
|
||||
"txt": "plain",
|
||||
"toml": "toml",
|
||||
"ini": "ini",
|
||||
"yaml": "yaml",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue