diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj index 8d805e8..ffadb95 100644 --- a/Neon Vision Editor.xcodeproj/project.pbxproj +++ b/Neon Vision Editor.xcodeproj/project.pbxproj @@ -361,7 +361,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 517; + CURRENT_PROJECT_VERSION = 518; 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 = 517; + CURRENT_PROJECT_VERSION = 518; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; diff --git a/Neon Vision Editor/App/AppMenus.swift b/Neon Vision Editor/App/AppMenus.swift index 192b8d1..5650a27 100644 --- a/Neon Vision Editor/App/AppMenus.swift +++ b/Neon Vision Editor/App/AppMenus.swift @@ -15,6 +15,8 @@ struct NeonVisionMacAppCommands: Commands { let openAIDiagnosticsWindow: () -> Void let postWindowCommand: (_ name: Notification.Name, _ object: Any?) -> Void let isUpdaterEnabled: Bool + let recentFilesProvider: () -> [RecentFilesStore.Item] + let clearRecentFiles: () -> Void @Binding var useAppleIntelligence: Bool @Binding var appleAIStatus: String @@ -60,6 +62,10 @@ struct NeonVisionMacAppCommands: Commands { postWindowCommand(name, object) } + private var recentFiles: [RecentFilesStore.Item] { + recentFilesProvider() + } + @CommandsBuilder private var appSettingsCommands: some Commands { CommandGroup(before: .appSettings) { @@ -98,6 +104,31 @@ struct NeonVisionMacAppCommands: Commands { .keyboardShortcut("o", modifiers: [.command, .shift]) } + CommandMenu("Open Recent") { + if recentFiles.isEmpty { + Button("No Recent Files") {} + .disabled(true) + } else { + ForEach(Array(recentFiles.prefix(10))) { item in + Button { + post(.openRecentFileRequested, object: item.url) + } label: { + if item.isPinned { + Label(item.title, systemImage: "star.fill") + } else { + Text(item.title) + } + } + } + + Divider() + + Button("Clear Unpinned Recents") { + clearRecentFiles() + } + } + } + CommandGroup(replacing: .saveItem) { Button("Save") { let current = activeEditorViewModel() diff --git a/Neon Vision Editor/App/NeonVisionEditorApp.swift b/Neon Vision Editor/App/NeonVisionEditorApp.swift index 4c64177..067127e 100644 --- a/Neon Vision Editor/App/NeonVisionEditorApp.swift +++ b/Neon Vision Editor/App/NeonVisionEditorApp.swift @@ -393,6 +393,8 @@ struct NeonVisionEditorApp: App { postWindowCommand(name, object: object) }, isUpdaterEnabled: ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution, + recentFilesProvider: { RecentFilesStore.items(limit: 10) }, + clearRecentFiles: { RecentFilesStore.clearUnpinned() }, useAppleIntelligence: $useAppleIntelligence, appleAIStatus: $appleAIStatus, appleAIRoundTripMS: $appleAIRoundTripMS, diff --git a/Neon Vision Editor/Core/RecentFilesStore.swift b/Neon Vision Editor/Core/RecentFilesStore.swift new file mode 100644 index 0000000..2ab0d96 --- /dev/null +++ b/Neon Vision Editor/Core/RecentFilesStore.swift @@ -0,0 +1,96 @@ +import Foundation + +struct RecentFilesStore { + struct Item: Identifiable, Equatable { + let url: URL + let isPinned: Bool + + var id: String { url.standardizedFileURL.path } + var title: String { url.lastPathComponent } + var subtitle: String { url.standardizedFileURL.path } + } + + private static let recentPathsKey = "RecentFilesPathsV1" + private static let pinnedPathsKey = "PinnedRecentFilesPathsV1" + private static let maximumItemCount = 30 + + static func items(limit: Int = maximumItemCount) -> [Item] { + let defaults = UserDefaults.standard + let recentPaths = sanitizedPaths(from: defaults.stringArray(forKey: recentPathsKey) ?? []) + let pinnedPaths = sanitizedPaths(from: defaults.stringArray(forKey: pinnedPathsKey) ?? []) + let pinnedSet = Set(pinnedPaths) + + let orderedPaths = pinnedPaths + recentPaths.filter { !pinnedSet.contains($0) } + let urls = orderedPaths.prefix(limit).map { URL(fileURLWithPath: $0) } + return urls.map { Item(url: $0, isPinned: pinnedSet.contains($0.standardizedFileURL.path)) } + } + + static func remember(_ url: URL) { + let standardizedPath = url.standardizedFileURL.path + let defaults = UserDefaults.standard + var recentPaths = sanitizedPaths(from: defaults.stringArray(forKey: recentPathsKey) ?? []) + recentPaths.removeAll { $0 == standardizedPath } + recentPaths.insert(standardizedPath, at: 0) + + let pinnedPaths = sanitizedPaths(from: defaults.stringArray(forKey: pinnedPathsKey) ?? []) + let pinnedSet = Set(pinnedPaths) + let retainedUnpinned = recentPaths.filter { !pinnedSet.contains($0) } + let availableUnpinnedSlots = max(0, maximumItemCount - pinnedPaths.count) + let trimmedRecent = Array(retainedUnpinned.prefix(availableUnpinnedSlots)) + + defaults.set(trimmedRecent, forKey: recentPathsKey) + defaults.set(pinnedPaths, forKey: pinnedPathsKey) + postDidChange() + } + + static func togglePinned(_ url: URL) { + let standardizedPath = url.standardizedFileURL.path + let defaults = UserDefaults.standard + var pinnedPaths = sanitizedPaths(from: defaults.stringArray(forKey: pinnedPathsKey) ?? []) + var recentPaths = sanitizedPaths(from: defaults.stringArray(forKey: recentPathsKey) ?? []) + + if let existingIndex = pinnedPaths.firstIndex(of: standardizedPath) { + pinnedPaths.remove(at: existingIndex) + recentPaths.removeAll { $0 == standardizedPath } + recentPaths.insert(standardizedPath, at: 0) + } else { + pinnedPaths.removeAll { $0 == standardizedPath } + pinnedPaths.insert(standardizedPath, at: 0) + pinnedPaths = Array(pinnedPaths.prefix(maximumItemCount)) + recentPaths.removeAll { $0 == standardizedPath } + } + + let pinnedSet = Set(pinnedPaths) + let availableUnpinnedSlots = max(0, maximumItemCount - pinnedPaths.count) + recentPaths = Array(recentPaths.filter { !pinnedSet.contains($0) }.prefix(availableUnpinnedSlots)) + + defaults.set(recentPaths, forKey: recentPathsKey) + defaults.set(pinnedPaths, forKey: pinnedPathsKey) + postDidChange() + } + + static func clearUnpinned() { + let defaults = UserDefaults.standard + defaults.removeObject(forKey: recentPathsKey) + postDidChange() + } + + private static func sanitizedPaths(from rawPaths: [String]) -> [String] { + var seen: Set = [] + return rawPaths.compactMap { rawPath in + let path = URL(fileURLWithPath: rawPath).standardizedFileURL.path + guard !path.isEmpty else { return nil } + guard FileManager.default.fileExists(atPath: path) else { return nil } + guard !seen.contains(path) else { return nil } + seen.insert(path) + return path + } + } + + private static func postDidChange() { + guard NSClassFromString("XCTestCase") == nil else { return } + DispatchQueue.main.async { + NotificationCenter.default.post(name: .recentFilesDidChange, object: nil) + } + } +} diff --git a/Neon Vision Editor/Data/EditorViewModel.swift b/Neon Vision Editor/Data/EditorViewModel.swift index 4b1d1e7..95ffd66 100644 --- a/Neon Vision Editor/Data/EditorViewModel.swift +++ b/Neon Vision Editor/Data/EditorViewModel.swift @@ -1461,6 +1461,9 @@ class EditorViewModel { isLargeCandidate: result.isLargeCandidate ) ) + if let fileURL = tabs.first(where: { $0.id == tabID })?.fileURL { + RecentFilesStore.remember(fileURL) + } EditorPerformanceMonitor.shared.endFileOpen( tabID: tabID, success: true, diff --git a/Neon Vision Editor/UI/ContentView.swift b/Neon Vision Editor/UI/ContentView.swift index 6fa69dc..c474d12 100644 --- a/Neon Vision Editor/UI/ContentView.swift +++ b/Neon Vision Editor/UI/ContentView.swift @@ -290,6 +290,7 @@ struct ContentView: View { @State var quickSwitcherQuery: String = "" @State var quickSwitcherProjectFileURLs: [URL] = [] @State private var quickSwitcherRecentItemIDs: [String] = [] + @State private var recentFilesRefreshToken: UUID = UUID() @State var showFindInFiles: Bool = false @State var findInFilesQuery: String = "" @State var findInFilesCaseSensitive: Bool = false @@ -1806,7 +1807,7 @@ struct ContentView: View { } } - let viewWithPanels = viewWithEditorActions + let viewWithPanelTriggers = viewWithEditorActions .onReceive(NotificationCenter.default.publisher(for: .showFindReplaceRequested)) { notif in guard matchesCurrentWindow(notif) else { return } showFindReplace = true @@ -1820,6 +1821,14 @@ struct ContentView: View { quickSwitcherQuery = "" showQuickSwitcher = true } + .onReceive(NotificationCenter.default.publisher(for: .openRecentFileRequested)) { notif in + guard matchesCurrentWindow(notif) else { return } + guard let url = notif.object as? URL else { return } + _ = viewModel.openFile(url: url) + } + .onReceive(NotificationCenter.default.publisher(for: .recentFilesDidChange)) { _ in + recentFilesRefreshToken = UUID() + } .onReceive(NotificationCenter.default.publisher(for: .addNextMatchRequested)) { notif in guard matchesCurrentWindow(notif) else { return } addNextMatchSelection() @@ -1843,10 +1852,12 @@ struct ContentView: View { guard matchesCurrentWindow(notif) else { return } showSupportPromptSheet = true } - .onReceive(NotificationCenter.default.publisher(for: .toggleProjectStructureSidebarRequested)) { notif in - guard matchesCurrentWindow(notif) else { return } - toggleProjectSidebarFromToolbar() - } + + let viewWithPanels = viewWithPanelTriggers + .onReceive(NotificationCenter.default.publisher(for: .toggleProjectStructureSidebarRequested)) { notif in + guard matchesCurrentWindow(notif) else { return } + toggleProjectSidebarFromToolbar() + } .onReceive(NotificationCenter.default.publisher(for: .openProjectFolderRequested)) { notif in guard matchesCurrentWindow(notif) else { return } openProjectFolder() @@ -2329,7 +2340,8 @@ struct ContentView: View { QuickFileSwitcherPanel( query: contentView.$quickSwitcherQuery, items: contentView.quickSwitcherItems, - onSelect: { contentView.selectQuickSwitcherItem($0) } + onSelect: { contentView.selectQuickSwitcherItem($0) }, + onTogglePin: { contentView.toggleQuickSwitcherPin($0) } ) } .sheet(isPresented: contentView.$showFindInFiles) { @@ -3996,6 +4008,11 @@ struct ContentView: View { } ) .id(currentLanguage) + .overlay { + if shouldShowStartupRecentFilesCard { + startupRecentFilesCard + } + } } } .frame(maxWidth: brainDumpLayoutEnabled ? 920 : .infinity) @@ -4979,16 +4996,17 @@ struct ContentView: View { } private var quickSwitcherItems: [QuickFileSwitcherPanel.Item] { + _ = recentFilesRefreshToken var items: [QuickFileSwitcherPanel.Item] = [] let fileURLSet = Set(viewModel.tabs.compactMap { $0.fileURL?.standardizedFileURL.path }) let commandItems: [QuickFileSwitcherPanel.Item] = [ - .init(id: "cmd:new_tab", title: "New Tab", subtitle: "Create a new empty tab"), - .init(id: "cmd:open_file", title: "Open File", subtitle: "Open files from disk"), - .init(id: "cmd:save_file", title: "Save", subtitle: "Save current tab"), - .init(id: "cmd:save_as", title: "Save As", subtitle: "Save current tab to a new file"), - .init(id: "cmd:find_replace", title: "Find and Replace", subtitle: "Search and replace in current document"), - .init(id: "cmd:find_in_files", title: "Find in Files", subtitle: "Search across project files"), - .init(id: "cmd:toggle_sidebar", title: "Toggle Sidebar", subtitle: "Show or hide the outline sidebar") + .init(id: "cmd:new_tab", title: "New Tab", subtitle: "Create a new empty tab", isPinned: false, canTogglePin: false), + .init(id: "cmd:open_file", title: "Open File", subtitle: "Open files from disk", isPinned: false, canTogglePin: false), + .init(id: "cmd:save_file", title: "Save", subtitle: "Save current tab", isPinned: false, canTogglePin: false), + .init(id: "cmd:save_as", title: "Save As", subtitle: "Save current tab to a new file", isPinned: false, canTogglePin: false), + .init(id: "cmd:find_replace", title: "Find and Replace", subtitle: "Search and replace in current document", isPinned: false, canTogglePin: false), + .init(id: "cmd:find_in_files", title: "Find in Files", subtitle: "Search across project files", isPinned: false, canTogglePin: false), + .init(id: "cmd:toggle_sidebar", title: "Toggle Sidebar", subtitle: "Show or hide the outline sidebar", isPinned: false, canTogglePin: false) ] items.append(contentsOf: commandItems) @@ -4998,7 +5016,23 @@ struct ContentView: View { QuickFileSwitcherPanel.Item( id: "tab:\(tab.id.uuidString)", title: tab.name, - subtitle: subtitle + subtitle: subtitle, + isPinned: false, + canTogglePin: false + ) + ) + } + + for recent in RecentFilesStore.items(limit: 12) { + let standardized = recent.url.standardizedFileURL.path + if fileURLSet.contains(standardized) { continue } + items.append( + QuickFileSwitcherPanel.Item( + id: "file:\(standardized)", + title: recent.title, + subtitle: recent.subtitle, + isPinned: recent.isPinned, + canTogglePin: true ) ) } @@ -5006,11 +5040,14 @@ struct ContentView: View { for url in quickSwitcherProjectFileURLs { let standardized = url.standardizedFileURL.path if fileURLSet.contains(standardized) { continue } + if items.contains(where: { $0.id == "file:\(standardized)" }) { continue } items.append( QuickFileSwitcherPanel.Item( id: "file:\(standardized)", title: url.lastPathComponent, - subtitle: standardized + subtitle: standardized, + isPinned: false, + canTogglePin: true ) ) } @@ -5019,14 +5056,22 @@ struct ContentView: View { if query.isEmpty { return Array( items - .sorted { quickSwitcherRecencyScore(for: $0.id) > quickSwitcherRecencyScore(for: $1.id) } + .sorted { + let leftPinned = $0.isPinned ? 1 : 0 + let rightPinned = $1.isPinned ? 1 : 0 + if leftPinned != rightPinned { + return leftPinned > rightPinned + } + return quickSwitcherRecencyScore(for: $0.id) > quickSwitcherRecencyScore(for: $1.id) + } .prefix(300) ) } let ranked = items.compactMap { item -> (QuickFileSwitcherPanel.Item, Int)? in guard let score = quickSwitcherMatchScore(for: item, query: query) else { return nil } - return (item, score + quickSwitcherRecencyScore(for: item.id)) + let pinBoost = item.isPinned ? 400 : 0 + return (item, score + quickSwitcherRecencyScore(for: item.id) + pinBoost) } .sorted { if $0.1 == $1.1 { @@ -5057,6 +5102,13 @@ struct ContentView: View { } } + private func toggleQuickSwitcherPin(_ item: QuickFileSwitcherPanel.Item) { + guard item.canTogglePin, item.id.hasPrefix("file:") else { return } + let path = String(item.id.dropFirst(5)) + RecentFilesStore.togglePinned(URL(fileURLWithPath: path)) + recentFilesRefreshToken = UUID() + } + private func performQuickSwitcherCommand(_ commandID: String) { switch commandID { case "cmd:new_tab": @@ -5087,6 +5139,82 @@ struct ContentView: View { UserDefaults.standard.set(quickSwitcherRecentItemIDs, forKey: quickSwitcherRecentsDefaultsKey) } + private var startupRecentFiles: [RecentFilesStore.Item] { + _ = recentFilesRefreshToken + return RecentFilesStore.items(limit: 5) + } + + private var shouldShowStartupRecentFilesCard: Bool { + guard !brainDumpLayoutEnabled else { return false } + guard viewModel.tabs.count == 1 else { return false } + guard let tab = viewModel.selectedTab else { return false } + guard !tab.isLoadingContent else { return false } + guard tab.fileURL == nil else { return false } + guard tab.content.isEmpty else { return false } + return !startupRecentFiles.isEmpty + } + + private var startupRecentFilesCard: some View { + VStack(alignment: .leading, spacing: 14) { + Text("Recent Files") + .font(.headline) + + ForEach(startupRecentFiles) { item in + HStack(spacing: 12) { + Button { + _ = viewModel.openFile(url: item.url) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text(item.title) + .lineLimit(1) + Text(item.subtitle) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + + Button { + RecentFilesStore.togglePinned(item.url) + } label: { + Image(systemName: item.isPinned ? "star.fill" : "star") + .foregroundStyle(item.isPinned ? Color.yellow : .secondary) + } + .buttonStyle(.plain) + .accessibilityLabel(item.isPinned ? "Unpin recent file" : "Pin recent file") + .accessibilityHint("Keeps this file near the top of recent files") + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(.thinMaterial) + ) + } + + Button("Open File…") { + openFileFromToolbar() + } + .font(.subheadline.weight(.semibold)) + } + .padding(20) + .frame(maxWidth: 520) + .background( + RoundedRectangle(cornerRadius: 22, style: .continuous) + .fill(.regularMaterial) + ) + .overlay( + RoundedRectangle(cornerRadius: 22, style: .continuous) + .strokeBorder(Color.primary.opacity(0.08), lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.08), radius: 16, x: 0, y: 6) + .padding(24) + .accessibilityElement(children: .contain) + .accessibilityLabel("Recent files") + } + private func quickSwitcherRecencyScore(for itemID: String) -> Int { guard let index = quickSwitcherRecentItemIDs.firstIndex(of: itemID) else { return 0 } return max(0, 120 - (index * 5)) diff --git a/Neon Vision Editor/UI/PanelsAndHelpers.swift b/Neon Vision Editor/UI/PanelsAndHelpers.swift index 45b30f3..fdf0486 100644 --- a/Neon Vision Editor/UI/PanelsAndHelpers.swift +++ b/Neon Vision Editor/UI/PanelsAndHelpers.swift @@ -166,11 +166,14 @@ struct QuickFileSwitcherPanel: View { let id: String let title: String let subtitle: String + let isPinned: Bool + let canTogglePin: Bool } @Binding var query: String let items: [Item] let onSelect: (Item) -> Void + let onTogglePin: (Item) -> Void @Environment(\.dismiss) private var dismiss @FocusState private var queryFieldFocused: Bool @@ -185,23 +188,38 @@ struct QuickFileSwitcherPanel: View { .focused($queryFieldFocused) List(items) { item in - Button { - onSelect(item) - dismiss() - } label: { - VStack(alignment: .leading, spacing: 2) { - Text(item.title) - .lineLimit(1) - Text(item.subtitle) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) + HStack(spacing: 10) { + Button { + onSelect(item) + dismiss() + } label: { + VStack(alignment: .leading, spacing: 2) { + Text(item.title) + .lineLimit(1) + Text(item.subtitle) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + .accessibilityLabel(item.title) + .accessibilityValue(item.subtitle) + .accessibilityHint("Opens the selected item") + + if item.canTogglePin { + Button { + onTogglePin(item) + } label: { + Image(systemName: item.isPinned ? "star.fill" : "star") + .foregroundStyle(item.isPinned ? Color.yellow : .secondary) + } + .buttonStyle(.plain) + .accessibilityLabel(item.isPinned ? "Unpin recent file" : "Pin recent file") + .accessibilityHint("Keeps this file near the top of recent results") } } - .buttonStyle(.plain) - .accessibilityLabel(item.title) - .accessibilityValue(item.subtitle) - .accessibilityHint("Opens the selected item") } .listStyle(.plain) .accessibilityLabel("Command Palette Results") @@ -971,6 +989,8 @@ extension Notification.Name { static let showUpdaterRequested = Notification.Name("showUpdaterRequested") static let showSettingsRequested = Notification.Name("showSettingsRequested") static let closeSelectedTabRequested = Notification.Name("closeSelectedTabRequested") + static let openRecentFileRequested = Notification.Name("openRecentFileRequested") + static let recentFilesDidChange = Notification.Name("recentFilesDidChange") } extension NSRange { diff --git a/Neon Vision EditorTests/RecentFilesStoreTests.swift b/Neon Vision EditorTests/RecentFilesStoreTests.swift new file mode 100644 index 0000000..a7cc9dc --- /dev/null +++ b/Neon Vision EditorTests/RecentFilesStoreTests.swift @@ -0,0 +1,70 @@ +import XCTest +@testable import Neon_Vision_Editor + +final class RecentFilesStoreTests: XCTestCase { + private var temporaryDirectoryURL: URL! + + override func setUpWithError() throws { + temporaryDirectoryURL = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true) + clearStore() + } + + override func tearDownWithError() throws { + clearStore() + if let temporaryDirectoryURL { + try? FileManager.default.removeItem(at: temporaryDirectoryURL) + } + temporaryDirectoryURL = nil + } + + func testRememberOrdersMostRecentFirst() throws { + let first = try makeFile(named: "first.txt") + let second = try makeFile(named: "second.txt") + + RecentFilesStore.remember(first) + RecentFilesStore.remember(second) + + XCTAssertEqual(RecentFilesStore.items(limit: 10).map(\.title), ["second.txt", "first.txt"]) + } + + func testPinnedItemsStayAtTop() throws { + let first = try makeFile(named: "first.txt") + let second = try makeFile(named: "second.txt") + let third = try makeFile(named: "third.txt") + + RecentFilesStore.remember(first) + RecentFilesStore.remember(second) + RecentFilesStore.remember(third) + RecentFilesStore.togglePinned(first) + + let items = RecentFilesStore.items(limit: 10) + XCTAssertEqual(items.map(\.title), ["first.txt", "third.txt", "second.txt"]) + XCTAssertEqual(items.first?.isPinned, true) + } + + func testClearUnpinnedRetainsPinnedItems() throws { + let pinned = try makeFile(named: "pinned.txt") + let unpinned = try makeFile(named: "unpinned.txt") + + RecentFilesStore.remember(pinned) + RecentFilesStore.remember(unpinned) + RecentFilesStore.togglePinned(pinned) + RecentFilesStore.clearUnpinned() + + XCTAssertEqual(RecentFilesStore.items(limit: 10).map(\.title), ["pinned.txt"]) + } + + private func makeFile(named name: String) throws -> URL { + let url = temporaryDirectoryURL.appendingPathComponent(name) + try "sample".write(to: url, atomically: true, encoding: .utf8) + return url + } + + private func clearStore() { + let defaults = UserDefaults.standard + defaults.removeObject(forKey: "RecentFilesPathsV1") + defaults.removeObject(forKey: "PinnedRecentFilesPathsV1") + } +}