Skip to content

Commit 6bbc444

Browse files
committed
Migrate widget to composable architecture
1 parent 9e1dd6f commit 6bbc444

File tree

20 files changed

+1718
-1034
lines changed

20 files changed

+1718
-1034
lines changed

Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/Package.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ let package = Package(
5151
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"),
5252
.package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"),
5353
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"),
54+
.package(
55+
url: "https://github.com/pointfreeco/swift-composable-architecture",
56+
from: "0.55.0"
57+
),
5458
],
5559
targets: [
5660
// MARK: - Main
@@ -220,6 +224,7 @@ let package = Package(
220224
name: "ChatTab",
221225
dependencies: [
222226
"SharedUIComponents",
227+
.product(name: "OpenAIService", package: "Tool"),
223228
.product(name: "Logger", package: "Tool"),
224229
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
225230
]
@@ -249,6 +254,7 @@ let package = Package(
249254
.product(name: "Logger", package: "Tool"),
250255
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
251256
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
257+
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
252258
]
253259
),
254260
.testTarget(name: "SuggestionWidgetTests", dependencies: ["SuggestionWidget"]),

Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ public final class GraphicalUserInterfaceController {
88
nonisolated let suggestionWidget = SuggestionWidgetController()
99
private nonisolated init() {
1010
Task { @MainActor in
11-
suggestionWidget.dataSource = WidgetDataSource.shared
12-
suggestionWidget.onOpenChatClicked = { [weak self] in
11+
suggestionWidget.dependency.suggestionWidgetDataSource = WidgetDataSource.shared
12+
suggestionWidget.dependency.onOpenChatClicked = { [weak self] in
1313
Task {
1414
let uri = try await Environment.fetchFocusedElementURI()
1515
let dataSource = WidgetDataSource.shared
1616
await dataSource.createChatIfNeeded(for: uri)
17-
self?.suggestionWidget.presentChatRoom(fileURL: uri)
17+
self?.suggestionWidget.presentChatRoom()
1818
}
1919
}
20-
suggestionWidget.onCustomCommandClicked = { command in
20+
suggestionWidget.dependency.onCustomCommandClicked = { command in
2121
Task {
2222
let commandHandler = PseudoCommandHandler()
2323
await commandHandler.handleCustomCommand(command)

Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import ChatService
2-
import SuggestionModel
32
import Foundation
43
import OpenAIService
4+
import SuggestionModel
55
import SuggestionWidget
66

77
struct PresentInWindowSuggestionPresenter {
88
func presentSuggestion(fileURL: URL) {
99
Task { @MainActor in
1010
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
11-
controller.suggestCode(fileURL: fileURL)
11+
controller.suggestCode()
1212
}
1313
}
1414

1515
func discardSuggestion(fileURL: URL) {
1616
Task { @MainActor in
1717
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
18-
controller.discardSuggestion(fileURL: fileURL)
18+
controller.discardSuggestion()
1919
}
2020
}
2121

@@ -45,28 +45,29 @@ struct PresentInWindowSuggestionPresenter {
4545
func closeChatRoom(fileURL: URL) {
4646
Task { @MainActor in
4747
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
48-
controller.closeChatRoom(fileURL: fileURL)
48+
controller.closeChatRoom()
4949
}
5050
}
5151

5252
func presentChatRoom(fileURL: URL) {
5353
Task { @MainActor in
5454
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
55-
controller.presentChatRoom(fileURL: fileURL)
55+
controller.presentChatRoom()
5656
}
5757
}
58-
58+
5959
func presentPromptToCode(fileURL: URL) {
6060
Task { @MainActor in
6161
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
62-
controller.presentPromptToCode(fileURL: fileURL)
62+
controller.presentPromptToCode()
6363
}
6464
}
65-
65+
6666
func closePromptToCode(fileURL: URL) {
6767
Task { @MainActor in
6868
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
69-
controller.discardPromptToCode(fileURL: fileURL)
69+
controller.discardPromptToCode()
7070
}
7171
}
7272
}
73+

Core/Sources/SharedUIComponents/SyntaxHighlighting.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Foundation
33
import Highlightr
44
import Splash
55
import SwiftUI
6-
import XPCShared
76

87
public func highlightedCodeBlock(
98
code: String,
Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,34 @@
11
import ActiveApplicationMonitor
22
import AppKit
3-
import SwiftUI
43
import ChatTab
4+
import ComposableArchitecture
5+
import SwiftUI
56

67
private let r: Double = 8
78

8-
@MainActor
9-
final class ChatWindowViewModel: ObservableObject {
10-
@Published var chat: ChatProvider?
11-
@Published var colorScheme: ColorScheme
12-
@Published var isPanelDisplayed = false
13-
@Published var chatPanelInASeparateWindow = false
14-
15-
public init(chat: ChatProvider? = nil, colorScheme: ColorScheme = .dark) {
16-
self.chat = chat
17-
self.colorScheme = colorScheme
18-
}
19-
}
20-
219
struct ChatWindowView: View {
22-
@ObservedObject var viewModel: ChatWindowViewModel
10+
let store: StoreOf<ChatPanelFeature>
2311

2412
var body: some View {
25-
Group {
26-
if let chat = viewModel.chat {
27-
ChatPanel(chat: chat)
28-
.background {
29-
Button(action: {
30-
viewModel.isPanelDisplayed = false
31-
if let app = ActiveApplicationMonitor.previousActiveApplication,
32-
app.isXcode
33-
{
34-
app.activate()
13+
WithViewStore(store, observe: { $0 }) { viewStore in
14+
Group {
15+
if let chat = viewStore.chat {
16+
ChatPanel(chat: chat)
17+
.background {
18+
Button(action: {
19+
viewStore.send(.hideButtonClicked)
20+
}) {
21+
EmptyView()
3522
}
36-
}) {
37-
EmptyView()
23+
.keyboardShortcut("M", modifiers: [.command])
3824
}
39-
.keyboardShortcut("M", modifiers: [.command])
40-
}
25+
}
4126
}
27+
.xcodeStyleFrame()
28+
.opacity(viewStore.isPanelDisplayed ? 1 : 0)
29+
.frame(minWidth: Style.panelWidth, minHeight: Style.panelHeight)
30+
.preferredColorScheme(viewStore.colorScheme)
4231
}
43-
.opacity(viewModel.isPanelDisplayed ? 1 : 0)
44-
.frame(minWidth: Style.panelWidth, minHeight: Style.panelHeight)
45-
.preferredColorScheme(viewModel.colorScheme)
4632
}
4733
}
4834

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import ActiveApplicationMonitor
2+
import AppKit
3+
import ChatTab
4+
import ComposableArchitecture
5+
import SwiftUI
6+
7+
extension ChatProvider: Equatable {
8+
public static func == (lhs: ChatProvider, rhs: ChatProvider) -> Bool {
9+
lhs.id == rhs.id
10+
}
11+
}
12+
13+
struct ChatPanelFeature: ReducerProtocol {
14+
struct State: Equatable {
15+
var chat: ChatProvider?
16+
var colorScheme: ColorScheme = .light
17+
var isPanelDisplayed = false
18+
var chatPanelInASeparateWindow = false
19+
}
20+
21+
enum Action: Equatable {
22+
case hideButtonClicked
23+
case toggleChatPanelDetachedButtonClicked
24+
case detachChatPanel
25+
case attachChatPanel
26+
case presentChatPanel(forceDetach: Bool)
27+
case closeChatPanel
28+
29+
case updateContent
30+
case updateChatProvider(ChatProvider?)
31+
}
32+
33+
@Dependency(\.suggestionWidgetControllerDependency) var suggestionWidgetControllerDependency
34+
@Dependency(\.xcodeInspector) var xcodeInspector
35+
@Dependency(\.activeApplicationMonitor) var activeApplicationMonitor
36+
37+
var body: some ReducerProtocol<State, Action> {
38+
Reduce { state, action in
39+
switch action {
40+
case .hideButtonClicked:
41+
state.isPanelDisplayed = false
42+
if let app = activeApplicationMonitor.previousActiveApplication, app.isXcode {
43+
app.activate()
44+
}
45+
return .none
46+
47+
case .toggleChatPanelDetachedButtonClicked:
48+
state.chatPanelInASeparateWindow.toggle()
49+
return .none
50+
51+
case .detachChatPanel:
52+
state.chatPanelInASeparateWindow = true
53+
return .none
54+
55+
case .attachChatPanel:
56+
state.chatPanelInASeparateWindow = false
57+
return .none
58+
59+
case .closeChatPanel:
60+
state.chat = nil
61+
return .none
62+
63+
case let .presentChatPanel(forceDetach):
64+
if forceDetach {
65+
state.chatPanelInASeparateWindow = true
66+
}
67+
let oldChatProviderId = state.chat?.id
68+
return .run { send in
69+
guard let provider = await fetchChatProvider(
70+
fileURL: xcodeInspector.activeDocumentURL
71+
) else { return }
72+
73+
if oldChatProviderId != provider.id {
74+
await send(.updateChatProvider(provider))
75+
}
76+
77+
try await Task.sleep(nanoseconds: 150_000_000)
78+
await NSApplication.shared.activate(ignoringOtherApps: true)
79+
}
80+
81+
case .updateContent:
82+
let oldChatProviderId = state.chat?.id
83+
return .run { send in
84+
if let provider = await fetchChatProvider(
85+
fileURL: xcodeInspector.activeDocumentURL
86+
) {
87+
if oldChatProviderId != provider.id {
88+
await send(.updateChatProvider(provider))
89+
}
90+
} else {
91+
await send(.updateChatProvider(nil))
92+
}
93+
}
94+
95+
case let .updateChatProvider(provider):
96+
state.chat = provider
97+
state.isPanelDisplayed = provider != nil
98+
return .none
99+
}
100+
}
101+
}
102+
103+
func fetchChatProvider(fileURL: URL) async -> ChatProvider? {
104+
await suggestionWidgetControllerDependency
105+
.suggestionWidgetDataSource?
106+
.chatForFile(at: fileURL)
107+
}
108+
}

0 commit comments

Comments
 (0)