88import Foundation
99
1010class IgnorePatternModel : ObservableObject {
11- @Published var patterns : [ GlobPattern ] = [ ]
12- @Published var selection : Set < GlobPattern > = [ ]
13-
14- let gitConfig = GitConfigClient ( shellClient: currentWorld. shellClient)
11+ @Published var loadingPatterns : Bool = false
12+ @Published var patterns : [ GlobPattern ] = [ ] {
13+ didSet {
14+ if !loadingPatterns {
15+ savePatterns ( )
16+ } else {
17+ loadingPatterns = false
18+ }
19+ }
20+ }
21+ @Published var selection : Set < UUID > = [ ]
1522
16- let fileURL = FileManager . default. homeDirectoryForCurrentUser. appendingPathComponent ( " .gitignore_global " )
23+ private let gitConfig = GitConfigClient ( shellClient: currentWorld. shellClient)
24+ private let fileURL = FileManager . default. homeDirectoryForCurrentUser. appendingPathComponent ( " .gitignore_global " )
25+ private var fileMonitor : DispatchSourceFileSystemObject ?
1726
1827 init ( ) {
1928 loadPatterns ( )
29+ startFileMonitor ( )
30+ }
31+
32+ deinit {
33+ stopFileMonitor ( )
34+ }
35+
36+ private func startFileMonitor( ) {
37+ let fileDescriptor = open ( fileURL. path, O_EVTONLY)
38+ guard fileDescriptor != - 1 else {
39+ return
40+ }
41+
42+ let source = DispatchSource . makeFileSystemObjectSource (
43+ fileDescriptor: fileDescriptor,
44+ eventMask: . write,
45+ queue: DispatchQueue . main
46+ )
47+
48+ source. setEventHandler { [ weak self] in
49+ self ? . loadPatterns ( )
50+ }
51+
52+ source. setCancelHandler {
53+ close ( fileDescriptor)
54+ }
55+
56+ fileMonitor? . cancel ( ) // Cancel any existing monitor
57+ fileMonitor = source
58+ source. resume ( )
59+ }
60+
61+ private func stopFileMonitor( ) {
62+ fileMonitor? . cancel ( )
63+ fileMonitor = nil
2064 }
2165
2266 func loadPatterns( ) {
67+ loadingPatterns = true
68+
2369 guard FileManager . default. fileExists ( atPath: fileURL. path) else {
2470 patterns = [ ]
2571 return
@@ -33,11 +79,139 @@ class IgnorePatternModel: ObservableObject {
3379 }
3480 }
3581
82+ // Map to track the line numbers of patterns.
83+ var patternLineMapping : [ String : Int ] = [ : ]
84+
85+ func getPattern( for id: UUID ) -> GlobPattern ? {
86+ return patterns. first ( where: { $0. id == id } )
87+ }
88+
3689 func savePatterns( ) {
90+ // Suspend the file monitor to avoid self-triggered updates
91+ stopFileMonitor ( )
92+
93+ defer {
94+ startFileMonitor ( )
95+ }
96+
97+ // Get the file contents; if the file doesn't exist, create it with the patterns
98+ guard let fileContent = try ? String ( contentsOf: fileURL) else {
99+ writeAllPatterns ( )
100+ return
101+ }
102+
103+ let lines = fileContent. split ( separator: " \n " , omittingEmptySubsequences: false ) . map ( String . init)
104+ var patternToLineIndex : [ String : Int ] = [ : ] // Map patterns to their line indices
105+ var reorderedLines : [ String ] = [ ] // Store the final reordered lines
106+
107+ // Map existing patterns in the file
108+ for (index, line) in lines. enumerated ( ) {
109+ let trimmedLine = line. trimmingCharacters ( in: . whitespaces)
110+ if !trimmedLine. isEmpty && !trimmedLine. hasPrefix ( " # " ) {
111+ patternToLineIndex [ trimmedLine] = index
112+ }
113+ }
114+
115+ // Add patterns in the new order specified by the `patterns` array
116+ for pattern in patterns {
117+ let value = pattern. value
118+ if let index = patternToLineIndex [ value] {
119+ // Keep the original line if it matches a pattern
120+ reorderedLines. append ( lines [ index] )
121+ patternToLineIndex. removeValue ( forKey: value)
122+ } else {
123+ // Add new patterns that don't exist in the file
124+ reorderedLines. append ( value)
125+ }
126+ }
127+
128+ // Add remaining non-pattern lines (comments, whitespace)
129+ for (index, line) in lines. enumerated ( ) {
130+ let trimmedLine = line. trimmingCharacters ( in: . whitespaces)
131+ if trimmedLine. isEmpty || trimmedLine. hasPrefix ( " # " ) {
132+ reorderedLines. insert ( line, at: index)
133+ }
134+ }
135+
136+ // Ensure single blank line at the end
137+ reorderedLines = cleanUpWhitespace ( in: reorderedLines)
138+
139+ // Write the updated content back to the file
140+ let updatedContent = reorderedLines. joined ( separator: " \n " )
141+ try ? updatedContent. write ( to: fileURL, atomically: true , encoding: . utf8)
142+ }
143+
144+ private func writeAllPatterns( ) {
37145 let content = patterns. map ( \. value) . joined ( separator: " \n " )
38146 try ? content. write ( to: fileURL, atomically: true , encoding: . utf8)
39147 }
40148
149+ private func handlePatterns(
150+ _ lines: inout [ String ] ,
151+ existingPatterns: inout Set < String > ,
152+ patternLineMap: inout [ String : Int ]
153+ ) {
154+ var handledPatterns = Set < String > ( )
155+
156+ // Update or preserve existing patterns
157+ for pattern in patterns {
158+ let value = pattern. value
159+ if let lineIndex = patternLineMap [ value] {
160+ // Pattern already exists, update it in place
161+ lines [ lineIndex] = value
162+ handledPatterns. insert ( value)
163+ } else {
164+ // Check if the pattern has been edited and corresponds to a previous pattern
165+ if let oldPattern = existingPatterns. first ( where: { !handledPatterns. contains ( $0) && $0 != value } ) ,
166+ let lineIndex = patternLineMap [ oldPattern] {
167+ lines [ lineIndex] = value
168+ existingPatterns. remove ( oldPattern)
169+ patternLineMap [ value] = lineIndex
170+ handledPatterns. insert ( value)
171+ } else {
172+ // Append new patterns at the end
173+ if let lastLine = lines. last, lastLine. trimmingCharacters ( in: . whitespaces) . isEmpty {
174+ lines. removeLast ( ) // Remove trailing blank line before appending
175+ }
176+ lines. append ( value)
177+ }
178+ }
179+ }
180+
181+ // Remove patterns no longer in the list
182+ let currentPatterns = Set ( patterns. map ( \. value) )
183+ lines = lines. filter { line in
184+ let trimmedLine = line. trimmingCharacters ( in: . whitespaces)
185+ return trimmedLine. isEmpty || trimmedLine. hasPrefix ( " # " ) || currentPatterns. contains ( trimmedLine)
186+ }
187+ }
188+
189+ private func cleanUpWhitespace( in lines: [ String ] ) -> [ String ] {
190+ var cleanedLines : [ String ] = [ ]
191+ var previousLineWasBlank = false
192+
193+ for line in lines {
194+ let isBlank = line. trimmingCharacters ( in: . whitespaces) . isEmpty
195+ if !( isBlank && previousLineWasBlank) {
196+ cleanedLines. append ( line)
197+ }
198+ previousLineWasBlank = isBlank
199+ }
200+
201+ // Trim extra blank lines at the end, ensuring only a single blank line
202+ while let lastLine = cleanedLines. last, lastLine. trimmingCharacters ( in: . whitespaces) . isEmpty {
203+ cleanedLines. removeLast ( )
204+ }
205+ cleanedLines. append ( " " ) // Ensure exactly one blank line at the end
206+
207+ // Trim whitespace at the top of the file
208+ while let firstLine = cleanedLines. first, firstLine. trimmingCharacters ( in: . whitespaces) . isEmpty {
209+ cleanedLines. removeFirst ( )
210+ }
211+
212+ return cleanedLines
213+ }
214+
41215 @MainActor
42216 func addPattern( ) {
43217 if patterns. isEmpty {
@@ -46,16 +220,12 @@ class IgnorePatternModel: ObservableObject {
46220 }
47221 }
48222 patterns. append ( GlobPattern ( value: " " ) )
49- Task {
50- savePatterns ( )
51- }
52223 }
53224
54225 @MainActor
55- func removePatterns( _ selection: Set < GlobPattern > ? = nil ) {
56- let patternsToRemove = selection ?? self . selection
226+ func removePatterns( _ selection: Set < UUID > ? = nil ) {
227+ let patternsToRemove = selection? . compactMap { getPattern ( for : $0 ) } ?? [ ]
57228 patterns. removeAll { patternsToRemove. contains ( $0) }
58- savePatterns ( )
59229 self . selection. removeAll ( )
60230 }
61231
0 commit comments