77
88import AppKit
99import CoreSpotlight
10+ import OSLog
1011
1112/// Helper methods for managing the recent projects list and donating list items to CoreSpotlight.
1213///
1314/// Limits the number of remembered projects to 100 items.
1415///
1516/// If a UI element needs to listen to changes in this list, listen for the
1617/// ``RecentProjectsStore/didUpdateNotification`` notification.
17- enum RecentProjectsStore {
18- private static let defaultsKey = " recentProjectPaths "
18+ class RecentProjectsStore {
19+ private let logger = Logger ( subsystem: Bundle . main. bundleIdentifier ?? " " , category: " RecentProjectsStore " )
20+
21+ /// The default projects store, uses the `UserDefaults.standard` storage location.
22+ static let shared = RecentProjectsStore ( )
23+
24+ private static let projectsdDefaultsKey = " recentProjectPaths "
1925 static let didUpdateNotification = Notification . Name ( " RecentProjectsStore.didUpdate " )
2026
21- static func recentProjectPaths( ) -> [ String ] {
22- UserDefaults . standard. array ( forKey: defaultsKey) as? [ String ] ?? [ ]
27+ /// The storage location for recent projects
28+ let defaults : UserDefaults
29+
30+ #if DEBUG
31+ /// Create a new store with a `UserDefaults` storage location.
32+ init ( defaults: UserDefaults = UserDefaults . standard) {
33+ self . defaults = defaults
34+ }
35+ #else
36+ /// Create a new store with a `UserDefaults` storage location.
37+ private init ( defaults: UserDefaults = UserDefaults . standard) {
38+ self . defaults = defaults
2339 }
40+ #endif
2441
25- static func recentProjectURLs( ) -> [ URL ] {
26- recentProjectPaths ( ) . map { URL ( filePath: $0) }
42+ /// Gets the recent paths array from `UserDefaults`.
43+ private func recentPaths( ) -> [ String ] {
44+ defaults. array ( forKey: Self . projectsdDefaultsKey) as? [ String ] ?? [ ]
2745 }
2846
29- private static func setPaths( _ paths: [ String ] ) {
30- var paths = paths
31- // Remove duplicates
32- var foundPaths = Set < String > ( )
33- for (idx, path) in paths. enumerated ( ) . reversed ( ) {
34- if foundPaths. contains ( path) {
35- paths. remove ( at: idx)
36- } else {
37- foundPaths. insert ( path)
38- }
39- }
47+ /// Gets all recent paths from `UserDefaults` as an array of `URL`s. Includes both **projects** and
48+ /// **single files**.
49+ /// To filter for either projects or single files, use ``recentProjectURLs()`` or ``recentFileURLs``, respectively.
50+ func recentURLs( ) -> [ URL ] {
51+ recentPaths ( ) . map { URL ( filePath: $0) }
52+ }
4053
41- // Limit list to to 100 items after de-duplication
42- UserDefaults . standard. setValue ( Array ( paths. prefix ( 100 ) ) , forKey: defaultsKey)
54+ /// Gets the recent **Project** `URL`s from `UserDefaults`.
55+ /// To get both single files and projects, use ``recentURLs()``.
56+ func recentProjectURLs( ) -> [ URL ] {
57+ recentURLs ( ) . filter { $0. isFolder }
58+ }
59+
60+ /// Gets the recent **Single File** `URL`s from `UserDefaults`.
61+ /// To get both single files and projects, use ``recentURLs()``.
62+ func recentFileURLs( ) -> [ URL ] {
63+ recentURLs ( ) . filter { !$0. isFolder }
64+ }
65+
66+ /// Save a new paths array to defaults. Automatically limits the list to the most recent `100` items, donates
67+ /// search items to Spotlight, and notifies observers.
68+ private func setPaths( _ paths: [ String ] ) {
69+ defaults. setValue ( Array ( paths. prefix ( 100 ) ) , forKey: Self . projectsdDefaultsKey)
4370 setDocumentControllerRecents ( )
4471 donateSearchableItems ( )
4572 NotificationCenter . default. post ( name: Self . didUpdateNotification, object: nil )
@@ -49,41 +76,45 @@ enum RecentProjectsStore {
4976 /// Moves the path to the front if it was in the list already, or prepends it.
5077 /// Saves the list to defaults when called.
5178 /// - Parameter url: The url that was opened. Any url is accepted. File, directory, https.
52- static func documentOpened( at url: URL ) {
53- var paths = recentProjectURLs ( )
54- if let containedIndex = paths. firstIndex ( where: { $0. componentCompare ( url) } ) {
55- paths. move ( fromOffsets: IndexSet ( integer: containedIndex) , toOffset: 0 )
79+ func documentOpened( at url: URL ) {
80+ var projectURLs = recentURLs ( )
81+
82+ if let containedIndex = projectURLs. firstIndex ( where: { $0. componentCompare ( url) } ) {
83+ projectURLs. move ( fromOffsets: IndexSet ( integer: containedIndex) , toOffset: 0 )
5684 } else {
57- paths . insert ( url, at: 0 )
85+ projectURLs . insert ( url, at: 0 )
5886 }
59- setPaths ( paths. map { $0. path ( percentEncoded: false ) } )
87+
88+ setPaths ( projectURLs. map { $0. path ( percentEncoded: false ) } )
6089 }
6190
62- /// Remove all paths in the set.
91+ /// Remove all project paths in the set.
6392 /// - Parameter paths: The paths to remove.
6493 /// - Returns: The remaining urls in the recent projects list.
65- static func removeRecentProjects( _ paths: Set < URL > ) -> [ URL ] {
66- var recentProjectPaths = recentProjectURLs ( )
94+ func removeRecentProjects( _ paths: Set < URL > ) -> [ URL ] {
95+ let paths = Set ( paths. map { $0. path ( percentEncoded: false ) } )
96+ var recentProjectPaths = recentPaths ( )
6797 recentProjectPaths. removeAll ( where: { paths. contains ( $0) } )
68- setPaths ( recentProjectPaths. map { $0 . path ( percentEncoded : false ) } )
69- return recentProjectURLs ( )
98+ setPaths ( recentProjectPaths)
99+ return recentURLs ( )
70100 }
71101
72- static func clearList( ) {
102+ func clearList( ) {
73103 setPaths ( [ ] )
104+ NotificationCenter . default. post ( name: Self . didUpdateNotification, object: nil )
74105 }
75106
76107 /// Syncs AppKit's recent documents list with ours, keeping the dock menu and other lists up-to-date.
77- private static func setDocumentControllerRecents( ) {
108+ private func setDocumentControllerRecents( ) {
78109 CodeEditDocumentController . shared. clearRecentDocuments ( nil )
79- for path in recentProjectURLs ( ) . prefix ( 10 ) {
110+ for path in recentURLs ( ) . prefix ( 10 ) {
80111 CodeEditDocumentController . shared. noteNewRecentDocumentURL ( path)
81112 }
82113 }
83114
84115 /// Donates all recent URLs to Core Search, making them searchable in Spotlight
85- private static func donateSearchableItems( ) {
86- let searchableItems = recentProjectURLs ( ) . map { entity in
116+ private func donateSearchableItems( ) {
117+ let searchableItems = recentURLs ( ) . map { entity in
87118 let attributeSet = CSSearchableItemAttributeSet ( contentType: . content)
88119 attributeSet. title = entity. lastPathComponent
89120 attributeSet. relatedUniqueIdentifier = entity. path ( )
@@ -93,9 +124,9 @@ enum RecentProjectsStore {
93124 attributeSet: attributeSet
94125 )
95126 }
96- CSSearchableIndex . default ( ) . indexSearchableItems ( searchableItems) { error in
127+ CSSearchableIndex . default ( ) . indexSearchableItems ( searchableItems) { [ weak self ] error in
97128 if let error = error {
98- print ( error)
129+ self ? . logger . debug ( " Failed to donate recent projects, error: \( error , privacy : . auto ) " )
99130 }
100131 }
101132 }
0 commit comments