diff --git a/Info-macOS.plist b/Info-macOS.plist
index b720fb9..f53789e 100644
--- a/Info-macOS.plist
+++ b/Info-macOS.plist
@@ -21,11 +21,13 @@
phtml
csv
tsv
+ txt
toml
ini
yaml
yml
xml
+ plist
sql
log
vim
@@ -40,6 +42,9 @@
psm1
html
htm
+ ee
+ exp
+ tmpl
css
c
cpp
@@ -69,6 +74,13 @@
conf
nginx
+ LSItemContentTypes
+
+ public.plain-text
+ public.text
+ public.source-code
+ net.daringfireball.markdown
+
CFBundleTypeName
Neon Vision Editor Document
CFBundleTypeRole
diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj
index 9c586ad..68a5dd5 100644
--- a/Neon Vision Editor.xcodeproj/project.pbxproj
+++ b/Neon Vision Editor.xcodeproj/project.pbxproj
@@ -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;
diff --git a/Neon Vision Editor/Core/LanguageDetector.swift b/Neon Vision Editor/Core/LanguageDetector.swift
index 8e38492..f8dba80 100644
--- a/Neon Vision Editor/Core/LanguageDetector.swift
+++ b/Neon Vision Editor/Core/LanguageDetector.swift
@@ -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)
}
diff --git a/Neon Vision Editor/Data/EditorViewModel.swift b/Neon Vision Editor/Data/EditorViewModel.swift
index 4f9507b..d4c3698 100644
--- a/Neon Vision Editor/Data/EditorViewModel.swift
+++ b/Neon Vision Editor/Data/EditorViewModel.swift
@@ -383,6 +383,7 @@ class EditorViewModel: ObservableObject {
"phtml": "php",
"csv": "csv",
"tsv": "csv",
+ "txt": "plain",
"toml": "toml",
"ini": "ini",
"yaml": "yaml",
diff --git a/Neon Vision Editor/UI/ContentView.swift b/Neon Vision Editor/UI/ContentView.swift
index 5f25e79..a04c2f8 100644
--- a/Neon Vision Editor/UI/ContentView.swift
+++ b/Neon Vision Editor/UI/ContentView.swift
@@ -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 = []
+ 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 = []
+ 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" }
diff --git a/Neon Vision EditorTests/LanguageDetectorTests.swift b/Neon Vision EditorTests/LanguageDetectorTests.swift
index 51b5490..0763336 100644
--- a/Neon Vision EditorTests/LanguageDetectorTests.swift
+++ b/Neon Vision EditorTests/LanguageDetectorTests.swift
@@ -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")
+ }
}