forked from intitni/CopilotForXcode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCodeiumChatBrowser.swift
More file actions
178 lines (150 loc) · 5.86 KB
/
CodeiumChatBrowser.swift
File metadata and controls
178 lines (150 loc) · 5.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import ComposableArchitecture
import Foundation
import Preferences
import WebKit
import Workspace
import XcodeInspector
@Reducer
struct CodeiumChatBrowser {
@ObservableState
struct State: Equatable {
var loadingProgress: Double = 0
var isLoading = false
var title = "Codeium Chat"
var error: String?
var url: URL?
}
enum Action: Equatable, BindableAction {
case binding(BindingAction<State>)
case initialize
case loadCurrentWorkspace
case reload
case presentError(String)
case removeError
case observeTitleChange
case updateTitle(String)
case observeURLChange
case updateURL(URL?)
case observeIsLoading
case updateIsLoading(Double)
}
let webView: WKWebView
let uuid = UUID()
private enum CancelID: Hashable {
case observeTitleChange(UUID)
case observeURLChange(UUID)
case observeIsLoading(UUID)
}
@Dependency(\.workspacePool) var workspacePool
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .initialize:
return .merge(
.run { send in await send(.observeTitleChange) },
.run { send in await send(.observeURLChange) },
.run { send in await send(.observeIsLoading) }
)
case .loadCurrentWorkspace:
return .run { send in
guard let workspaceURL = await XcodeInspector.shared.safe.activeWorkspaceURL
else {
await send(.presentError("Can't find workspace."))
return
}
do {
let workspace = try await workspacePool
.fetchOrCreateWorkspace(workspaceURL: workspaceURL)
let codeiumPlugin = workspace.plugin(for: CodeiumWorkspacePlugin.self)
guard let service = await codeiumPlugin?.codeiumService
else {
await send(.presentError("Can't start service."))
return
}
let url = try await service.getChatURL()
await send(.removeError)
await webView.load(URLRequest(url: url))
} catch {
await send(.presentError(error.localizedDescription))
}
}
case .reload:
webView.reload()
return .none
case .removeError:
state.error = nil
return .none
case let .presentError(error):
state.error = error
return .none
// MARK: Observation
case .observeTitleChange:
let stream = AsyncStream<String> { continuation in
let observation = webView.observe(\.title, options: [.new, .initial]) {
webView, _ in
continuation.yield(webView.title ?? "")
}
continuation.onTermination = { _ in
observation.invalidate()
}
}
return .run { send in
for await title in stream where !title.isEmpty {
try Task.checkCancellation()
await send(.updateTitle(title))
}
}
.cancellable(id: CancelID.observeTitleChange(uuid), cancelInFlight: true)
case let .updateTitle(title):
state.title = title
return .none
case .observeURLChange:
let stream = AsyncStream<URL?> { continuation in
let observation = webView.observe(\.url, options: [.new, .initial]) {
_, url in
if let it = url.newValue {
continuation.yield(it)
}
}
continuation.onTermination = { _ in
observation.invalidate()
}
}
return .run { send in
for await url in stream {
try Task.checkCancellation()
await send(.updateURL(url))
}
}.cancellable(id: CancelID.observeURLChange(uuid), cancelInFlight: true)
case let .updateURL(url):
state.url = url
return .none
case .observeIsLoading:
let stream = AsyncStream<Double> { continuation in
let observation = webView
.observe(\.estimatedProgress, options: [.new]) { _, estimatedProgress in
if let it = estimatedProgress.newValue {
continuation.yield(it)
}
}
continuation.onTermination = { _ in
observation.invalidate()
}
}
return .run { send in
for await isLoading in stream {
try Task.checkCancellation()
await send(.updateIsLoading(isLoading))
}
}.cancellable(id: CancelID.observeIsLoading(uuid), cancelInFlight: true)
case let .updateIsLoading(progress):
state.isLoading = progress != 1
state.loadingProgress = progress
return .none
}
}
}
}