Skip to content

Commit adf0070

Browse files
committed
Merge branch 'feature/suggestion-white-list' into develop
2 parents e11857c + 585fc65 commit adf0070

7 files changed

Lines changed: 335 additions & 33 deletions

File tree

Copilot for Xcode/SettingsView.swift

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,34 @@ import LaunchAgentManager
22
import Preferences
33
import SwiftUI
44

5-
final class Settings: ObservableObject {
6-
@AppStorage(\.quitXPCServiceOnXcodeAndAppQuit)
7-
var quitXPCServiceOnXcodeAndAppQuit: Bool
8-
@AppStorage(\.realtimeSuggestionToggle)
9-
var realtimeSuggestionToggle: Bool
10-
@AppStorage(\.realtimeSuggestionDebounce)
11-
var realtimeSuggestionDebounce: Double
12-
@AppStorage(\.suggestionPresentationMode)
13-
var suggestionPresentationMode: Preferences.PresentationMode
14-
@AppStorage(\.suggestionWidgetPositionMode)
15-
var suggestionWidgetPositionMode: SuggestionWidgetPositionMode
16-
@AppStorage(\.widgetColorScheme)
17-
var widgetColorScheme: WidgetColorScheme
18-
@AppStorage(\.acceptSuggestionWithAccessibilityAPI)
19-
var acceptSuggestionWithAccessibilityAPI: Bool
20-
init() {}
21-
}
22-
235
struct SettingsView: View {
6+
final class Settings: ObservableObject {
7+
@AppStorage(\.quitXPCServiceOnXcodeAndAppQuit)
8+
var quitXPCServiceOnXcodeAndAppQuit: Bool
9+
@AppStorage(\.realtimeSuggestionToggle)
10+
var realtimeSuggestionToggle: Bool
11+
@AppStorage(\.realtimeSuggestionDebounce)
12+
var realtimeSuggestionDebounce: Double
13+
@AppStorage(\.suggestionPresentationMode)
14+
var suggestionPresentationMode: Preferences.PresentationMode
15+
@AppStorage(\.suggestionWidgetPositionMode)
16+
var suggestionWidgetPositionMode: SuggestionWidgetPositionMode
17+
@AppStorage(\.widgetColorScheme)
18+
var widgetColorScheme: WidgetColorScheme
19+
@AppStorage(\.acceptSuggestionWithAccessibilityAPI)
20+
var acceptSuggestionWithAccessibilityAPI: Bool
21+
@AppStorage(\.disableSuggestionFeatureGlobally)
22+
var disableSuggestionFeatureGlobally: Bool
23+
@AppStorage(\.suggestionFeatureEnabledProjectList)
24+
var suggestionFeatureEnabledProjectList: [String]
25+
init() {}
26+
}
27+
2428
@StateObject var settings = Settings()
2529
@State var editingRealtimeSuggestionDebounce: Double = UserDefaults.shared
2630
.value(for: \.realtimeSuggestionDebounce)
2731
@Environment(\.updateChecker) var updateChecker
32+
@State var isSuggestionFeatureEnabledListPickerOpen = false
2833

2934
var body: some View {
3035
Section {
@@ -90,6 +95,21 @@ struct SettingsView: View {
9095
}
9196
.toggleStyle(.switch)
9297

98+
HStack {
99+
Toggle(isOn: $settings.disableSuggestionFeatureGlobally) {
100+
Text("Disable suggestion feature globally")
101+
}
102+
.toggleStyle(.switch)
103+
104+
Button("Enabled Projects") {
105+
isSuggestionFeatureEnabledListPickerOpen = true
106+
}
107+
}.sheet(isPresented: $isSuggestionFeatureEnabledListPickerOpen) {
108+
SuggestionFeatureEnabledProjectListView(
109+
isOpen: $isSuggestionFeatureEnabledListPickerOpen
110+
)
111+
}
112+
93113
HStack {
94114
Slider(value: $editingRealtimeSuggestionDebounce, in: 0...2, step: 0.1) {
95115
Text("Real-time suggestion fetch debounce")
@@ -109,7 +129,7 @@ struct SettingsView: View {
109129
.fill(Color.white.opacity(0.2))
110130
)
111131
}
112-
132+
113133
Toggle(isOn: $settings.acceptSuggestionWithAccessibilityAPI) {
114134
Text("Use accessibility API to accept suggestion in widget")
115135
}
@@ -119,9 +139,136 @@ struct SettingsView: View {
119139
}
120140
}
121141

142+
struct SuggestionFeatureEnabledProjectListView: View {
143+
final class Settings: ObservableObject {
144+
@AppStorage(\.suggestionFeatureEnabledProjectList)
145+
var suggestionFeatureEnabledProjectList: [String]
146+
147+
init(suggestionFeatureEnabledProjectList: AppStorage<[String]>? = nil) {
148+
if let list = suggestionFeatureEnabledProjectList {
149+
_suggestionFeatureEnabledProjectList = list
150+
}
151+
}
152+
}
153+
154+
var isOpen: Binding<Bool>
155+
@State var isAddingNewProject = false
156+
@StateObject var settings = Settings()
157+
158+
var body: some View {
159+
VStack {
160+
HStack {
161+
Button(action: {
162+
self.isOpen.wrappedValue = false
163+
}) {
164+
Image(systemName: "xmark.circle.fill")
165+
.foregroundStyle(.secondary)
166+
.padding()
167+
}
168+
.buttonStyle(.plain)
169+
Text("Enabled Projects")
170+
Spacer()
171+
Button(action: {
172+
isAddingNewProject = true
173+
}) {
174+
Image(systemName: "plus.circle.fill")
175+
.foregroundStyle(.secondary)
176+
.padding()
177+
}
178+
.buttonStyle(.plain)
179+
}
180+
.background(.black.opacity(0.2))
181+
182+
List {
183+
ForEach(
184+
settings.suggestionFeatureEnabledProjectList,
185+
id: \.self
186+
) { project in
187+
HStack {
188+
Text(project)
189+
.contextMenu {
190+
Button("Remove") {
191+
settings.suggestionFeatureEnabledProjectList.removeAll(
192+
where: { $0 == project }
193+
)
194+
}
195+
}
196+
Spacer()
197+
198+
Button(action: {
199+
settings.suggestionFeatureEnabledProjectList.removeAll(
200+
where: { $0 == project }
201+
)
202+
}) {
203+
Image(systemName: "trash.fill")
204+
.foregroundStyle(.secondary)
205+
}
206+
.buttonStyle(.plain)
207+
}
208+
}
209+
}
210+
.overlay {
211+
if settings.suggestionFeatureEnabledProjectList.isEmpty {
212+
Text("""
213+
Empty
214+
Add project with "+" button
215+
Or right clicking the circular widget
216+
""")
217+
.multilineTextAlignment(.center)
218+
}
219+
}
220+
}
221+
.frame(width: 300, height: 400)
222+
.sheet(isPresented: $isAddingNewProject) {
223+
SuggestionFeatureAddEnabledProjectView(isOpen: $isAddingNewProject, settings: settings)
224+
}
225+
}
226+
}
227+
228+
struct SuggestionFeatureAddEnabledProjectView: View {
229+
var isOpen: Binding<Bool>
230+
var settings: SuggestionFeatureEnabledProjectListView.Settings
231+
@State var rootPath = ""
232+
233+
var body: some View {
234+
VStack {
235+
Text("Enter the root path of the project. Do not use `~` to replace /Users/yourUserName.")
236+
TextField("Root path", text: $rootPath)
237+
HStack {
238+
Spacer()
239+
Button("Cancel") {
240+
isOpen.wrappedValue = false
241+
}
242+
Button("Add") {
243+
settings.suggestionFeatureEnabledProjectList.append(rootPath)
244+
isOpen.wrappedValue = false
245+
}
246+
}.buttonStyle(.copilot)
247+
}
248+
.padding()
249+
.frame(minWidth: 500)
250+
}
251+
}
252+
253+
// MARK: - Previews
254+
122255
struct SettingsView_Preview: PreviewProvider {
123256
static var previews: some View {
124257
SettingsView()
125258
.background(.purple)
126259
}
127260
}
261+
262+
struct SuggestionFeatureEnabledProjectListView_Preview: PreviewProvider {
263+
static var previews: some View {
264+
SuggestionFeatureEnabledProjectListView(
265+
isOpen: .constant(true),
266+
settings: .init(suggestionFeatureEnabledProjectList: .init(wrappedValue: [
267+
"hello/2",
268+
"hello/3",
269+
"hello/4",
270+
], "SuggestionFeatureEnabledProjectListView_Preview"))
271+
)
272+
.background(.purple)
273+
}
274+
}

Core/Sources/Preferences/Keys.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@ public struct UserDefaultPreferenceKeys {
133133
public var forceOrderWidgetToFront: HideCommonPrecedingSpacesInSuggestion {
134134
.init()
135135
}
136+
137+
public struct DisableSuggestionFeatureGlobally: UserDefaultPreferenceKey {
138+
public let defaultValue = false
139+
public let key = "DisableSuggestionFeatureGlobally"
140+
}
141+
142+
public var disableSuggestionFeatureGlobally: DisableSuggestionFeatureGlobally {
143+
.init()
144+
}
145+
146+
public struct SuggestionFeatureEnabledProjectList: UserDefaultPreferenceKey {
147+
public let defaultValue: [String] = []
148+
public let key = "SuggestionFeatureEnabledProjectList"
149+
}
150+
151+
public var suggestionFeatureEnabledProjectList: SuggestionFeatureEnabledProjectList {
152+
.init()
153+
}
136154

137155
public var disableLazyVStack: FeatureFlags.DisableLazyVStack { .init() }
138156
}

Core/Sources/Preferences/UserDefaults.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ extension String: UserDefaultsStorable {}
2222
extension Data: UserDefaultsStorable {}
2323
extension URL: UserDefaultsStorable {}
2424

25+
extension Array: RawRepresentable where Element: Codable {
26+
public init?(rawValue: String) {
27+
guard let data = rawValue.data(using: .utf8),
28+
let result = try? JSONDecoder().decode([Element].self, from: data)
29+
else {
30+
return nil
31+
}
32+
self = result
33+
}
34+
35+
public var rawValue: String {
36+
guard let data = try? JSONEncoder().encode(self),
37+
let result = String(data: data, encoding: .utf8)
38+
else {
39+
return "[]"
40+
}
41+
return result
42+
}
43+
}
44+
2545
public extension UserDefaults {
2646
// MARK: - Normal Types
2747

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,16 @@ public class RealtimeSuggestionController {
183183

184184
guard UserDefaults.shared.value(for: \.realtimeSuggestionToggle)
185185
else { return }
186-
186+
187+
if UserDefaults.shared.value(for: \.disableSuggestionFeatureGlobally),
188+
let fileURL = try? await Environment.fetchCurrentFileURL(),
189+
let (workspace, _) = try? await Workspace
190+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
191+
{
192+
let isEnabled = workspace.isSuggestionFeatureEnabled
193+
if !isEnabled { return }
194+
}
195+
187196
if Task.isCancelled { return }
188197

189198
Logger.service.info("Prefetch suggestions.")

0 commit comments

Comments
 (0)