Skip to content

Commit 97bbefb

Browse files
committed
Marked IgnorePatternModel with @mainactor. More documentation. Refactored Limiter to use Timer API and removed unused throttle method.
1 parent 35be9a9 commit 97bbefb

File tree

3 files changed

+41
-52
lines changed

3 files changed

+41
-52
lines changed

CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
import Foundation
99

1010
/// A model to manage Git ignore patterns for a file, including loading, saving, and monitoring changes.
11+
@MainActor
1112
class IgnorePatternModel: ObservableObject {
13+
/// Indicates whether patterns are currently being loaded from the Git ignore file.
1214
@Published var loadingPatterns: Bool = false
15+
16+
/// A collection of Git ignore patterns being managed by this model.
1317
@Published var patterns: [GlobPattern] = [] {
1418
didSet {
1519
if !loadingPatterns {
@@ -19,11 +23,19 @@ class IgnorePatternModel: ObservableObject {
1923
}
2024
}
2125
}
26+
27+
/// Tracks the selected patterns by their unique identifiers (UUIDs).
2228
@Published var selection: Set<UUID> = []
2329

30+
/// A client for interacting with the Git configuration.
2431
private let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient)
32+
33+
/// A file system monitor for detecting changes to the Git ignore file.
2534
private var fileMonitor: DispatchSourceFileSystemObject?
2635

36+
/// Task tracking the current save operation
37+
private var savingTask: Task<Void, Never>?
38+
2739
init() {
2840
Task {
2941
try? await startFileMonitor()
@@ -32,7 +44,9 @@ class IgnorePatternModel: ObservableObject {
3244
}
3345

3446
deinit {
35-
stopFileMonitor()
47+
Task { @MainActor [weak self] in
48+
self?.stopFileMonitor()
49+
}
3650
}
3751

3852
/// Resolves the URL for the Git ignore file.
@@ -69,9 +83,7 @@ class IgnorePatternModel: ObservableObject {
6983
)
7084

7185
source.setEventHandler {
72-
Task {
73-
await self.loadPatterns()
74-
}
86+
Task { await self.loadPatterns() }
7587
}
7688

7789
source.setCancelHandler {
@@ -91,40 +103,30 @@ class IgnorePatternModel: ObservableObject {
91103

92104
/// Loads patterns from the Git ignore file into the `patterns` property.
93105
func loadPatterns() async {
94-
await MainActor.run { loadingPatterns = true } // Ensure `loadingPatterns` is updated on the main thread
106+
loadingPatterns = true
95107

96108
do {
97109
let fileURL = try await gitIgnoreURL()
98110
guard FileManager.default.fileExists(atPath: fileURL.path) else {
99-
await MainActor.run {
100-
patterns = []
101-
loadingPatterns = false // Update on the main thread
102-
}
111+
patterns = []
112+
loadingPatterns = false
103113
return
104114
}
105115

106116
if let content = try? String(contentsOf: fileURL) {
107-
let parsedPatterns = content.split(separator: "\n")
117+
patterns = content.split(separator: "\n")
108118
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
109119
.filter { !$0.isEmpty && !$0.starts(with: "#") }
110120
.map { GlobPattern(value: String($0)) }
111-
112-
await MainActor.run {
113-
patterns = parsedPatterns // Update `patterns` on the main thread
114-
loadingPatterns = false // Ensure `loadingPatterns` is updated on the main thread
115-
}
121+
loadingPatterns = false
116122
} else {
117-
await MainActor.run {
118-
patterns = []
119-
loadingPatterns = false
120-
}
121-
}
122-
} catch {
123-
print("Error loading patterns: \(error)")
124-
await MainActor.run {
125123
patterns = []
126124
loadingPatterns = false
127125
}
126+
} catch {
127+
print("Error loading patterns: \(error)")
128+
patterns = []
129+
loadingPatterns = false
128130
}
129131
}
130132

@@ -137,9 +139,16 @@ class IgnorePatternModel: ObservableObject {
137139

138140
/// Saves the current patterns back to the Git ignore file.
139141
func savePatterns() {
140-
Task {
142+
// Cancel the existing task if it exists
143+
savingTask?.cancel()
144+
145+
// Start a new task for saving patterns
146+
savingTask = Task {
141147
stopFileMonitor()
142-
defer { Task { try? await startFileMonitor() } }
148+
defer {
149+
savingTask = nil // Clear the task when done
150+
Task { try? await startFileMonitor() }
151+
}
143152

144153
do {
145154
let fileURL = try await gitIgnoreURL()
@@ -283,13 +292,12 @@ class IgnorePatternModel: ObservableObject {
283292
}
284293

285294
/// Adds a new, empty pattern to the list of patterns.
286-
@MainActor
287295
func addPattern() {
288296
patterns.append(GlobPattern(value: ""))
289297
}
298+
290299
/// Removes the specified patterns from the list of patterns.
291300
/// - Parameter selection: The set of UUIDs for the patterns to remove. If `nil`, no patterns are removed.
292-
@MainActor
293301
func removePatterns(_ selection: Set<UUID>? = nil) {
294302
let patternsToRemove = selection?.compactMap { getPattern(for: $0) } ?? []
295303
patterns.removeAll { patternsToRemove.contains($0) }

CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ struct SourceControlGitView: View {
5555
}
5656
}
5757
.onAppear {
58+
// Intentionally using an onAppear with a Task instead of just a .task modifier.
59+
// When we did this it was executing too often.
5860
Task {
5961
authorName = try await gitConfig.get(key: "user.name", global: true) ?? ""
6062
authorEmail = try await gitConfig.get(key: "user.email", global: true) ?? ""

CodeEdit/Utils/Limiter.swift

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111
// TODO: Look into improving this API by using async by default so `Task` isn't needed when used.
1212
enum Limiter {
1313
// Keep track of debounce timers and throttle states
14-
private static var debounceTimers: [AnyHashable: AnyCancellable] = [:]
14+
private static var debounceTimers: [AnyHashable: Timer] = [:]
1515
private static var throttleLastExecution: [AnyHashable: Date] = [:]
1616

1717
/// Debounces an action with a specified duration and identifier.
@@ -21,31 +21,10 @@ enum Limiter {
2121
/// - action: The action to be executed after the debounce period.
2222
static func debounce(id: AnyHashable, duration: TimeInterval, action: @escaping () -> Void) {
2323
// Cancel any existing debounce timer for the given ID
24-
debounceTimers[id]?.cancel()
25-
24+
debounceTimers[id]?.invalidate()
2625
// Start a new debounce timer for the given ID
27-
debounceTimers[id] = Timer.publish(every: duration, on: .main, in: .common)
28-
.autoconnect()
29-
.first()
30-
.sink { _ in
31-
action()
32-
debounceTimers[id] = nil
33-
}
34-
}
35-
36-
/// Throttles an action with a specified duration and identifier.
37-
/// - Parameters:
38-
/// - id: A unique identifier for the throttled action.
39-
/// - duration: The throttle duration in seconds.
40-
/// - action: The action to be executed after the throttle period.
41-
static func throttle(id: AnyHashable, duration: TimeInterval, action: @escaping () -> Void) {
42-
// Check the time of the last execution for the given ID
43-
if let lastExecution = throttleLastExecution[id], Date().timeIntervalSince(lastExecution) < duration {
44-
return // Skip this call if it's within the throttle duration
26+
debounceTimers[id] = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { _ in
27+
action()
4528
}
46-
47-
// Update the last execution time and perform the action
48-
throttleLastExecution[id] = Date()
49-
action()
5029
}
5130
}

0 commit comments

Comments
 (0)