Skip to content

Commit c9c07ab

Browse files
committed
Add Confirmation UI, Clean up ListRows
1 parent 581c12f commit c9c07ab

File tree

7 files changed

+235
-92
lines changed

7 files changed

+235
-92
lines changed

CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,21 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable {
2020
let package: RegistryItem
2121
let steps: [PackageManagerInstallStep]
2222

23+
var currentStep: PackageManagerInstallStep? {
24+
steps[safe: currentStepIdx]
25+
}
26+
2327
@Published var accumulatedOutput: [OutputItem] = []
24-
@Published var currentStep: Int = 0
28+
@Published var currentStepIdx: Int = 0
2529
@Published var error: Error?
2630
@Published var progress: Progress
2731

32+
/// If non-nil, indicates that this operation has halted and requires confirmation.
33+
@Published public private(set) var waitingForConfirmation: String?
34+
2835
private let shellClient: ShellClient = .live()
2936
private var operationTask: Task<Void, Error>?
37+
private var confirmationContinuation: CheckedContinuation<Void, Never>?
3038

3139
init(package: RegistryItem, steps: [PackageManagerInstallStep]) {
3240
self.package = package
@@ -44,14 +52,37 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable {
4452

4553
func cancel() {
4654
operationTask?.cancel()
55+
operationTask = nil
56+
}
57+
58+
/// Called by UI to confirm continuing to the next step
59+
func confirmCurrentStep() {
60+
waitingForConfirmation = nil
61+
confirmationContinuation?.resume()
62+
confirmationContinuation = nil
63+
}
64+
65+
private func waitForConfirmation(message: String) async {
66+
waitingForConfirmation = message
67+
await withCheckedContinuation { [weak self] (continuation: CheckedContinuation<Void, Never>) in
68+
self?.confirmationContinuation = continuation
69+
}
4770
}
4871

4972
private func runNext() async throws {
50-
guard currentStep < steps.count, error == nil else {
73+
guard currentStepIdx < steps.count, error == nil else {
5174
return
5275
}
5376

54-
let task = steps[currentStep]
77+
let task = steps[currentStepIdx]
78+
79+
switch task.confirmation {
80+
case .required(let message):
81+
await waitForConfirmation(message: message)
82+
case .none:
83+
break
84+
}
85+
5586
let model = PackageManagerProgressModel(shellClient: shellClient)
5687
progress.addChild(model.progress, withPendingUnitCount: 1)
5788

@@ -78,7 +109,7 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable {
78109
}
79110
}
80111

81-
self.currentStep += 1
112+
self.currentStepIdx += 1
82113

83114
try Task.checkCancellation()
84115
if let error {

CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ final class GithubPackageManager: PackageManagerProtocol {
100100
) -> PackageManagerInstallStep {
101101
PackageManagerInstallStep(
102102
name: "Download Binary Executable",
103-
confirmation: .required(message: "Downloading binary from:\n\(url)")
103+
confirmation: .none
104104
) { model in
105105
do {
106106
await model.status("Downloading \(url)")

CodeEdit/Features/LSP/Registry/RegistryItem.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ struct RegistryItem: Codable {
1818
let source: Source
1919
let bin: [String: String]?
2020

21+
var sanitizedName: String {
22+
name.replacingOccurrences(of: "-", with: " ")
23+
.replacingOccurrences(of: "_", with: " ")
24+
.split(separator: " ")
25+
.map { word -> String in
26+
let str = String(word).lowercased()
27+
// Check for special cases
28+
if str == "ls" || str == "lsp" || str == "ci" || str == "cli" {
29+
return str.uppercased()
30+
}
31+
return str.capitalized
32+
}
33+
.joined(separator: " ")
34+
}
35+
36+
var sanitizedDescription: String {
37+
description.replacingOccurrences(of: "\n", with: " ")
38+
}
39+
40+
var homepageURL: URL? {
41+
URL(string: homepage)
42+
}
43+
44+
/// A pretty version of the homepage URL.
45+
/// Removes the schema (eg https) and leaves the path and domain.
46+
var homepagePretty: String {
47+
guard let homepageURL else { return homepage }
48+
return (homepageURL.host(percentEncoded: false) ?? "") + homepageURL.path(percentEncoded: false)
49+
}
50+
2151
/// The method for installation, parsed from this item's ``source-swift.property`` parameter.
2252
var installMethod: InstallationMethod? {
2353
let sourceId = source.id

CodeEdit/Features/LSP/Registry/RegistryManager.swift

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,17 @@ final class RegistryManager: ObservableObject {
138138
do {
139139
try await operation.run()
140140
} catch {
141-
142-
await MainActor.run { [weak self] in
143-
self?.updateActivityViewer(operation.package.name, activityTitle, fail: true)
144-
}
141+
self?.updateActivityViewer(operation.package.name, activityTitle, fail: true)
145142
return
146143
}
147144

148145
guard !Task.isCancelled else { return }
149-
// Update settings on the main thread
150-
await MainActor.run { [weak self] in
151-
self?.installedLanguageServers[operation.package.name] = .init(
152-
packageName: operation.package.name,
153-
isEnabled: true,
154-
version: method.version ?? ""
155-
)
156-
self?.updateActivityViewer(operation.package.name, activityTitle, fail: false)
157-
}
146+
self?.installedLanguageServers[operation.package.name] = .init(
147+
packageName: operation.package.name,
148+
isEnabled: true,
149+
version: method.version ?? ""
150+
)
151+
self?.updateActivityViewer(operation.package.name, activityTitle, fail: false)
158152
}
159153
}
160154

CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift

Lines changed: 113 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,136 @@
88
import SwiftUI
99

1010
struct LanguageServerInstallView: View {
11+
@Environment(\.dismiss)
12+
var dismiss
13+
@EnvironmentObject private var registryManager: RegistryManager
14+
1115
@ObservedObject var operation: PackageManagerInstallOperation
1216

1317
var body: some View {
14-
VStack(alignment: .leading) {
15-
Text("Installing: " + operation.package.name)
16-
.font(.title)
17-
ProgressView(operation.progress)
18-
.progressViewStyle(.linear)
18+
VStack(spacing: 0) {
19+
formContent
20+
Divider()
21+
footer
22+
}
23+
.constrainHeightToWindow()
24+
.alert(
25+
"Confirm Step",
26+
isPresented: Binding(get: { operation.waitingForConfirmation != nil }, set: { _ in }),
27+
presenting: operation.waitingForConfirmation
28+
) { _ in
29+
Button("Cancel") {
30+
registryManager.cancelInstallation()
31+
}
32+
Button("Continue") {
33+
operation.confirmCurrentStep()
34+
}
35+
} message: { confirmationMessage in
36+
Text(confirmationMessage)
37+
}
38+
}
1939

40+
@ViewBuilder private var formContent: some View {
41+
Form {
42+
packageInfoSection
2043
if let error = operation.error {
21-
VStack(alignment: .leading) {
44+
Section {
2245
HStack(spacing: 0) {
2346
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
2447
Text("Error Occurred")
2548
}
2649
.font(.title3)
2750
ErrorDescriptionLabel(error: error)
2851
}
29-
.overlay { RoundedRectangle(cornerRadius: 8).stroke(.separator) }
3052
}
53+
progressSection
54+
outputSection
55+
}
56+
.formStyle(.grouped)
57+
}
3158

32-
VStack {
33-
ScrollViewReader { proxy in
34-
List(operation.accumulatedOutput) { line in
35-
HStack(spacing: 0) {
36-
Text(line.contents)
37-
.font(.caption.monospaced())
38-
Spacer(minLength: 0)
39-
}
40-
}
41-
.listStyle(.plain)
42-
.listRowSeparator(.hidden)
43-
.onReceive(operation.$accumulatedOutput) { output in
44-
proxy.scrollTo(output.last?.id)
45-
}
59+
@ViewBuilder private var footer: some View {
60+
HStack {
61+
Spacer()
62+
if operation.currentStep != nil {
63+
Button {
64+
registryManager.cancelInstallation()
65+
dismiss()
66+
} label: {
67+
Text("Cancel")
68+
.frame(minWidth: 56)
4669
}
70+
.buttonStyle(.bordered)
71+
} else {
72+
Button {
73+
dismiss()
74+
} label: {
75+
Text("Continue")
76+
.frame(minWidth: 56)
77+
}
78+
.buttonStyle(.borderedProminent)
4779
}
48-
.frame(height: 250)
49-
.overlay { RoundedRectangle(cornerRadius: 8).stroke(.separator) }
5080
}
51-
.frame(maxWidth: .infinity)
5281
.padding()
5382
}
83+
84+
@ViewBuilder private var packageInfoSection: some View {
85+
Section {
86+
LabeledContent("Installing Package", value: operation.package.sanitizedName)
87+
LabeledContent("Source") {
88+
sourceButton
89+
}
90+
Text(operation.package.sanitizedDescription)
91+
.multilineTextAlignment(.leading)
92+
.foregroundColor(.secondary)
93+
.labelsHidden()
94+
}
95+
}
96+
97+
@ViewBuilder private var sourceButton: some View {
98+
if #available(macOS 14.0, *) {
99+
Button(operation.package.homepagePretty) {
100+
guard let homepage = operation.package.homepageURL else { return }
101+
NSWorkspace.shared.open(homepage)
102+
}
103+
.buttonStyle(.plain)
104+
.foregroundColor(Color(NSColor.linkColor))
105+
.focusEffectDisabled()
106+
} else {
107+
Button(operation.package.homepagePretty) {
108+
guard let homepage = operation.package.homepageURL else { return }
109+
NSWorkspace.shared.open(homepage)
110+
}
111+
.buttonStyle(.plain)
112+
.foregroundColor(Color(NSColor.linkColor))
113+
}
114+
}
115+
116+
@ViewBuilder private var progressSection: some View {
117+
Section {
118+
LabeledContent("Step", value: operation.currentStep?.name ?? "Finished")
119+
ProgressView(operation.progress)
120+
.progressViewStyle(.linear)
121+
}
122+
}
123+
124+
@ViewBuilder private var outputSection: some View {
125+
Section {
126+
ScrollViewReader { proxy in
127+
List(operation.accumulatedOutput) { line in
128+
HStack(spacing: 0) {
129+
Text(line.contents)
130+
.font(.caption.monospaced())
131+
Spacer(minLength: 0)
132+
}
133+
}
134+
.listStyle(.plain)
135+
.listRowSeparator(.hidden)
136+
.onReceive(operation.$accumulatedOutput) { output in
137+
proxy.scrollTo(output.last?.id)
138+
}
139+
}
140+
.frame(height: 350)
141+
}
142+
}
54143
}

0 commit comments

Comments
 (0)