Skip to content

Commit 4f1b05c

Browse files
committed
Use timed debounce instead for prompt to code debounce
2 parents e2f776f + 42cae22 commit 4f1b05c

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

Core/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ let package = Package(
275275
.product(name: "AppMonitoring", package: "Tool"),
276276
.product(name: "ChatTab", package: "Tool"),
277277
.product(name: "Logger", package: "Tool"),
278+
.product(name: "CustomAsyncAlgorithms", package: "Tool"),
278279
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
279280
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
280281
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),

Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AppKit
22
import ComposableArchitecture
3+
import CustomAsyncAlgorithms
34
import Dependencies
45
import Foundation
56
import PromptToCodeService
@@ -43,7 +44,7 @@ public struct PromptToCode: ReducerProtocol {
4344
}
4445
}
4546
}
46-
47+
4748
public enum FocusField: Equatable {
4849
case textField
4950
}
@@ -113,7 +114,7 @@ public struct PromptToCode: ReducerProtocol {
113114
self.generateDescriptionRequirement = generateDescriptionRequirement
114115
self.isAttachedToSelectionRange = isAttachedToSelectionRange
115116
self.commandName = commandName
116-
117+
117118
if selectionRange?.isEmpty ?? true {
118119
self.isAttachedToSelectionRange = false
119120
}
@@ -151,7 +152,7 @@ public struct PromptToCode: ReducerProtocol {
151152
switch action {
152153
case .binding:
153154
return .none
154-
155+
155156
case .focusOnTextField:
156157
state.focusedField = .textField
157158
return .none
@@ -186,8 +187,8 @@ public struct PromptToCode: ReducerProtocol {
186187
extraSystemPrompt: copiedState.extraSystemPrompt,
187188
generateDescriptionRequirement: copiedState
188189
.generateDescriptionRequirement
189-
)
190-
#warning("TODO: make the action call debounced.")
190+
).timedDebounce(for: 0.2)
191+
191192
for try await fragment in stream {
192193
try Task.checkCancellation()
193194
await send(.modifyCodeChunkReceived(

Tool/Package.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ let package = Package(
4444
.library(name: "GitIgnoreCheck", targets: ["GitIgnoreCheck"]),
4545
.library(name: "DebounceFunction", targets: ["DebounceFunction"]),
4646
.library(name: "AsyncPassthroughSubject", targets: ["AsyncPassthroughSubject"]),
47+
.library(name: "CustomAsyncAlgorithms", targets: ["CustomAsyncAlgorithms"]),
4748
],
4849
dependencies: [
4950
// A fork of https://github.com/aespinilla/Tiktoken to allow loading from local files.
@@ -85,6 +86,13 @@ let package = Package(
8586

8687
.target(name: "ObjectiveCExceptionHandling"),
8788

89+
.target(
90+
name: "CustomAsyncAlgorithms",
91+
dependencies: [
92+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms")
93+
]
94+
),
95+
8896
.target(
8997
name: "Keychain",
9098
dependencies: ["Configs", "Preferences"]
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Foundation
2+
3+
private actor TimedDebounceFunction<Element> {
4+
let duration: TimeInterval
5+
let block: (Element) async -> Void
6+
7+
var task: Task<Void, Error>?
8+
var lastValue: Element?
9+
var lastFireTime: Date = .init(timeIntervalSince1970: 0)
10+
11+
init(duration: TimeInterval, block: @escaping (Element) async -> Void) {
12+
self.duration = duration
13+
self.block = block
14+
}
15+
16+
func callAsFunction(_ value: Element) async {
17+
task?.cancel()
18+
if lastFireTime.timeIntervalSinceNow < -duration {
19+
await fire(value)
20+
task = nil
21+
} else {
22+
lastValue = value
23+
task = Task.detached { [weak self, duration] in
24+
try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
25+
await self?.fire(value)
26+
}
27+
}
28+
}
29+
30+
func finish() async {
31+
task?.cancel()
32+
if let lastValue {
33+
await fire(lastValue)
34+
}
35+
}
36+
37+
private func fire(_ value: Element) async {
38+
lastFireTime = Date()
39+
lastValue = nil
40+
await block(value)
41+
}
42+
}
43+
44+
public extension AsyncSequence {
45+
/// Debounce, but only if the value is received within a certain time frame.
46+
func timedDebounce(
47+
for duration: TimeInterval
48+
) -> AsyncThrowingStream<Element, Error> {
49+
return AsyncThrowingStream { continuation in
50+
Task {
51+
let function = TimedDebounceFunction(duration: duration) { value in
52+
continuation.yield(value)
53+
}
54+
do {
55+
for try await value in self {
56+
await function(value)
57+
}
58+
await function.finish()
59+
continuation.finish()
60+
} catch {
61+
continuation.finish(throwing: error)
62+
}
63+
}
64+
}
65+
}
66+
}
67+

0 commit comments

Comments
 (0)