Skip to content

Commit bea3627

Browse files
committed
Display inline suggestions in a separate window
1 parent ce262fc commit bea3627

File tree

6 files changed

+320
-151
lines changed

6 files changed

+320
-151
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import Environment
2+
import Preferences
3+
import SwiftUI
4+
5+
@MainActor
6+
final class SharedPanelViewModel: ObservableObject {
7+
enum Content {
8+
case suggestion(SuggestionProvider)
9+
case promptToCode(PromptToCodeProvider)
10+
case error(String)
11+
12+
var contentHash: String {
13+
switch self {
14+
case let .error(e):
15+
return "error: \(e)"
16+
case let .suggestion(provider):
17+
return "suggestion: \(provider.code.hashValue)"
18+
case let .promptToCode(provider):
19+
return "provider: \(provider.id)"
20+
}
21+
}
22+
}
23+
24+
@Published var content: Content?
25+
@Published var colorScheme: ColorScheme
26+
27+
init(
28+
content: Content? = nil,
29+
colorScheme: ColorScheme = .dark
30+
) {
31+
self.content = content
32+
self.colorScheme = colorScheme
33+
}
34+
}
35+
36+
@MainActor
37+
final class SharedPanelDisplayController: ObservableObject {
38+
@Published var alignTopToAnchor = false
39+
@Published var isPanelDisplayed: Bool = false
40+
41+
init(
42+
alignTopToAnchor: Bool = false,
43+
isPanelDisplayed: Bool = false
44+
) {
45+
self.alignTopToAnchor = alignTopToAnchor
46+
self.isPanelDisplayed = isPanelDisplayed
47+
}
48+
}
49+
50+
extension View {
51+
@ViewBuilder
52+
func animation<V: Equatable>(
53+
featureFlag: KeyPath<UserDefaultPreferenceKeys, FeatureFlag>,
54+
_ animation: Animation?,
55+
value: V
56+
) -> some View {
57+
let isOn = UserDefaults.shared.value(for: featureFlag)
58+
if isOn {
59+
self.animation(animation, value: value)
60+
} else {
61+
self
62+
}
63+
}
64+
}
65+
66+
struct SharedPanelView: View {
67+
@ObservedObject var viewModel: SharedPanelViewModel
68+
@ObservedObject var displayController: SharedPanelDisplayController
69+
@AppStorage(\.suggestionPresentationMode) var suggestionPresentationMode
70+
71+
var body: some View {
72+
VStack(spacing: 0) {
73+
if !displayController.alignTopToAnchor {
74+
Spacer()
75+
.frame(minHeight: 0, maxHeight: .infinity)
76+
.allowsHitTesting(false)
77+
}
78+
79+
VStack {
80+
if let content = viewModel.content {
81+
ZStack(alignment: .topLeading) {
82+
switch content {
83+
case let .suggestion(suggestion):
84+
switch suggestionPresentationMode {
85+
case .nearbyTextCursor:
86+
EmptyView()
87+
case .floatingWidget:
88+
CodeBlockSuggestionPanel(suggestion: suggestion)
89+
}
90+
case let .promptToCode(provider):
91+
PromptToCodePanel(provider: provider)
92+
case let .error(description):
93+
ErrorPanel(
94+
viewModel: viewModel,
95+
displayController: displayController,
96+
description: description
97+
)
98+
}
99+
}
100+
.frame(maxWidth: .infinity, maxHeight: Style.panelHeight)
101+
.fixedSize(horizontal: false, vertical: true)
102+
.allowsHitTesting(displayController.isPanelDisplayed)
103+
}
104+
}
105+
.frame(maxWidth: .infinity)
106+
107+
if displayController.alignTopToAnchor {
108+
Spacer()
109+
.frame(minHeight: 0, maxHeight: .infinity)
110+
.allowsHitTesting(false)
111+
}
112+
}
113+
.preferredColorScheme(viewModel.colorScheme)
114+
.opacity({
115+
guard displayController.isPanelDisplayed else { return 0 }
116+
guard viewModel.content != nil else { return 0 }
117+
return 1
118+
}())
119+
.animation(
120+
featureFlag: \.animationACrashSuggestion,
121+
.easeInOut(duration: 0.2),
122+
value: viewModel.content?.contentHash
123+
)
124+
.animation(
125+
featureFlag: \.animationBCrashSuggestion,
126+
.easeInOut(duration: 0.2),
127+
value: displayController.isPanelDisplayed
128+
)
129+
.frame(maxWidth: Style.panelWidth, maxHeight: Style.panelHeight)
130+
}
131+
}
132+
133+
struct CommandButtonStyle: ButtonStyle {
134+
let color: Color
135+
136+
func makeBody(configuration: Configuration) -> some View {
137+
configuration.label
138+
.padding(.vertical, 4)
139+
.padding(.horizontal, 8)
140+
.foregroundColor(.white)
141+
.background(
142+
RoundedRectangle(cornerRadius: 4, style: .continuous)
143+
.fill(color.opacity(configuration.isPressed ? 0.8 : 1))
144+
.animation(.easeOut(duration: 0.1), value: configuration.isPressed)
145+
)
146+
.overlay {
147+
RoundedRectangle(cornerRadius: 4, style: .continuous)
148+
.stroke(Color.white.opacity(0.2), style: .init(lineWidth: 1))
149+
}
150+
}
151+
}
152+
153+
// MARK: - Previews
154+
155+
struct SuggestionPanelView_Error_Preview: PreviewProvider {
156+
static var previews: some View {
157+
SharedPanelView(viewModel: .init(
158+
content: .error("This is an error\nerror")
159+
), displayController: .init(isPanelDisplayed: true))
160+
.frame(width: 450, height: 200)
161+
}
162+
}
163+
164+
struct SuggestionPanelView_Both_DisplayingSuggestion_Preview: PreviewProvider {
165+
static var previews: some View {
166+
SharedPanelView(viewModel: .init(
167+
content: .suggestion(SuggestionProvider(
168+
code: """
169+
- (void)addSubview:(UIView *)view {
170+
[self addSubview:view];
171+
}
172+
""",
173+
language: "objective-c",
174+
startLineIndex: 8,
175+
suggestionCount: 2,
176+
currentSuggestionIndex: 0
177+
)),
178+
colorScheme: .dark
179+
), displayController: .init(isPanelDisplayed: true))
180+
.frame(width: 450, height: 200)
181+
.background {
182+
HStack {
183+
Color.red
184+
Color.green
185+
Color.blue
186+
}
187+
}
188+
}
189+
}
190+

Core/Sources/SuggestionWidget/SuggestionPanelContent/ErrorPanel.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import SwiftUI
22

33
struct ErrorPanel: View {
4-
var viewModel: SuggestionPanelViewModel
4+
var viewModel: SharedPanelViewModel
5+
var displayController: SharedPanelDisplayController
56
var description: String
67

78
var body: some View {
@@ -15,7 +16,7 @@ struct ErrorPanel: View {
1516

1617
// close button
1718
Button(action: {
18-
viewModel.isPanelDisplayed = false
19+
displayController.isPanelDisplayed = false
1920
viewModel.content = nil
2021
}) {
2122
Image(systemName: "xmark")
Lines changed: 25 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,28 @@
1-
import Environment
2-
import Preferences
1+
import Foundation
32
import SwiftUI
43

54
@MainActor
6-
final class SuggestionPanelViewModel: ObservableObject {
7-
enum Content {
8-
case suggestion(SuggestionProvider)
9-
case promptToCode(PromptToCodeProvider)
10-
case error(String)
11-
12-
var contentHash: String {
13-
switch self {
14-
case let .error(e):
15-
return "error: \(e)"
16-
case let .suggestion(provider):
17-
return "suggestion: \(provider.code.hashValue)"
18-
case let .promptToCode(provider):
19-
return "provider: \(provider.id)"
20-
}
21-
}
22-
}
23-
24-
@Published var content: Content?
25-
@Published var isPanelDisplayed: Bool
5+
final class SuggestionPanelDisplayController: ObservableObject {
266
@Published var alignTopToAnchor = false
27-
@Published var colorScheme: ColorScheme
7+
@Published var isPanelDisplayed: Bool = false
288

29-
public init(
30-
content: Content? = nil,
31-
isPanelDisplayed: Bool = false,
32-
colorScheme: ColorScheme = .dark
9+
init(
10+
alignTopToAnchor: Bool = false,
11+
isPanelDisplayed: Bool = false
3312
) {
34-
self.content = content
13+
self.alignTopToAnchor = alignTopToAnchor
3514
self.isPanelDisplayed = isPanelDisplayed
36-
self.colorScheme = colorScheme
37-
}
38-
}
39-
40-
extension View {
41-
@ViewBuilder
42-
func animation<V: Equatable>(
43-
featureFlag: KeyPath<UserDefaultPreferenceKeys, FeatureFlag>,
44-
_ animation: Animation?,
45-
value: V
46-
) -> some View {
47-
let isOn = UserDefaults.shared.value(for: featureFlag)
48-
if isOn {
49-
self.animation(animation, value: value)
50-
} else {
51-
self
52-
}
5315
}
5416
}
5517

5618
struct SuggestionPanelView: View {
57-
@ObservedObject var viewModel: SuggestionPanelViewModel
19+
@ObservedObject var viewModel: SharedPanelViewModel
20+
@ObservedObject var displayController: SuggestionPanelDisplayController
21+
@AppStorage(\.suggestionPresentationMode) var suggestionPresentationMode
5822

5923
var body: some View {
6024
VStack(spacing: 0) {
61-
if !viewModel.alignTopToAnchor {
25+
if !displayController.alignTopToAnchor {
6226
Spacer()
6327
.frame(minHeight: 0, maxHeight: .infinity)
6428
.allowsHitTesting(false)
@@ -69,29 +33,32 @@ struct SuggestionPanelView: View {
6933
ZStack(alignment: .topLeading) {
7034
switch content {
7135
case let .suggestion(suggestion):
72-
CodeBlockSuggestionPanel(suggestion: suggestion)
73-
case let .promptToCode(provider):
74-
PromptToCodePanel(provider: provider)
75-
case let .error(description):
76-
ErrorPanel(viewModel: viewModel, description: description)
36+
switch suggestionPresentationMode {
37+
case .nearbyTextCursor:
38+
CodeBlockSuggestionPanel(suggestion: suggestion)
39+
case .floatingWidget:
40+
EmptyView()
41+
}
42+
default:
43+
EmptyView()
7744
}
7845
}
79-
.frame(maxWidth: .infinity, maxHeight: Style.panelHeight)
46+
.frame(maxWidth: .infinity, maxHeight: Style.inlineSuggestionMaxHeight)
8047
.fixedSize(horizontal: false, vertical: true)
81-
.allowsHitTesting(viewModel.isPanelDisplayed)
48+
.allowsHitTesting(displayController.isPanelDisplayed)
8249
}
8350
}
8451
.frame(maxWidth: .infinity)
8552

86-
if viewModel.alignTopToAnchor {
53+
if displayController.alignTopToAnchor {
8754
Spacer()
8855
.frame(minHeight: 0, maxHeight: .infinity)
8956
.allowsHitTesting(false)
9057
}
9158
}
9259
.preferredColorScheme(viewModel.colorScheme)
9360
.opacity({
94-
guard viewModel.isPanelDisplayed else { return 0 }
61+
guard displayController.isPanelDisplayed else { return 0 }
9562
guard viewModel.content != nil else { return 0 }
9663
return 1
9764
}())
@@ -103,69 +70,8 @@ struct SuggestionPanelView: View {
10370
.animation(
10471
featureFlag: \.animationBCrashSuggestion,
10572
.easeInOut(duration: 0.2),
106-
value: viewModel.isPanelDisplayed
73+
value: displayController.isPanelDisplayed
10774
)
108-
.frame(maxWidth: Style.panelWidth, maxHeight: Style.panelHeight)
75+
.frame(maxWidth: Style.inlineSuggestionMinWidth, maxHeight: Style.inlineSuggestionMaxHeight)
10976
}
11077
}
111-
112-
struct CommandButtonStyle: ButtonStyle {
113-
let color: Color
114-
115-
func makeBody(configuration: Configuration) -> some View {
116-
configuration.label
117-
.padding(.vertical, 4)
118-
.padding(.horizontal, 8)
119-
.foregroundColor(.white)
120-
.background(
121-
RoundedRectangle(cornerRadius: 4, style: .continuous)
122-
.fill(color.opacity(configuration.isPressed ? 0.8 : 1))
123-
.animation(.easeOut(duration: 0.1), value: configuration.isPressed)
124-
)
125-
.overlay {
126-
RoundedRectangle(cornerRadius: 4, style: .continuous)
127-
.stroke(Color.white.opacity(0.2), style: .init(lineWidth: 1))
128-
}
129-
}
130-
}
131-
132-
// MARK: - Previews
133-
134-
struct SuggestionPanelView_Error_Preview: PreviewProvider {
135-
static var previews: some View {
136-
SuggestionPanelView(viewModel: .init(
137-
content: .error("This is an error\nerror"),
138-
isPanelDisplayed: true
139-
))
140-
.frame(width: 450, height: 200)
141-
}
142-
}
143-
144-
struct SuggestionPanelView_Both_DisplayingSuggestion_Preview: PreviewProvider {
145-
static var previews: some View {
146-
SuggestionPanelView(viewModel: .init(
147-
content: .suggestion(SuggestionProvider(
148-
code: """
149-
- (void)addSubview:(UIView *)view {
150-
[self addSubview:view];
151-
}
152-
""",
153-
language: "objective-c",
154-
startLineIndex: 8,
155-
suggestionCount: 2,
156-
currentSuggestionIndex: 0
157-
)),
158-
isPanelDisplayed: true,
159-
colorScheme: .dark
160-
))
161-
.frame(width: 450, height: 200)
162-
.background {
163-
HStack {
164-
Color.red
165-
Color.green
166-
Color.blue
167-
}
168-
}
169-
}
170-
}
171-

0 commit comments

Comments
 (0)