Skip to content

Commit 04fde2c

Browse files
Added installation queuing, fix runtime warnings
1 parent 64ceb74 commit 04fde2c

File tree

4 files changed

+222
-43
lines changed

4 files changed

+222
-43
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//
2+
// InstallationQueueManager.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 3/13/25.
6+
//
7+
8+
import Foundation
9+
10+
/// A class to manage queued installations of language servers
11+
class InstallationQueueManager {
12+
static let shared: InstallationQueueManager = .init()
13+
14+
/// The maximum number of concurrent installations allowed
15+
private let maxConcurrentInstallations: Int = 2
16+
/// Queue of pending installations
17+
private var installationQueue: [(RegistryItem, (Result<Void, Error>) -> Void)] = []
18+
/// Currently running installations
19+
private var runningInstallations: Int = 0
20+
/// Installation status dictionary
21+
private var installationStatus: [String: PackageInstallationStatus] = [:]
22+
23+
private init() {}
24+
25+
/// Add a package to the installation queue
26+
func queueInstallation(package: RegistryItem, completion: @escaping (Result<Void, Error>) -> Void) {
27+
installationStatus[package.name] = .queued
28+
installationQueue.append((package, completion))
29+
processNextInstallations()
30+
31+
// Notify UI that package is queued
32+
DispatchQueue.main.async {
33+
NotificationCenter.default.post(
34+
name: .installationStatusChanged,
35+
object: nil,
36+
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.queued]
37+
)
38+
}
39+
}
40+
41+
/// Process next installations from the queue if possible
42+
private func processNextInstallations() {
43+
while runningInstallations < maxConcurrentInstallations && !installationQueue.isEmpty {
44+
let (package, completion) = installationQueue.removeFirst()
45+
runningInstallations += 1
46+
installationStatus[package.name] = .installing
47+
48+
// Notify UI that installation is now in progress
49+
DispatchQueue.main.async {
50+
NotificationCenter.default.post(
51+
name: .installationStatusChanged,
52+
object: nil,
53+
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.installing]
54+
)
55+
}
56+
57+
Task {
58+
do {
59+
try await RegistryManager.shared.installPackage(package: package)
60+
61+
// Notify UI that installation is complete
62+
installationStatus[package.name] = .installed
63+
DispatchQueue.main.async {
64+
NotificationCenter.default.post(
65+
name: .installationStatusChanged,
66+
object: nil,
67+
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.installed]
68+
)
69+
completion(.success(()))
70+
}
71+
} catch {
72+
// Notify UI that installation failed
73+
installationStatus[package.name] = .failed(error)
74+
DispatchQueue.main.async {
75+
NotificationCenter.default.post(
76+
name: .installationStatusChanged,
77+
object: nil,
78+
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.failed(error)]
79+
)
80+
completion(.failure(error))
81+
}
82+
}
83+
84+
runningInstallations -= 1
85+
processNextInstallations()
86+
}
87+
}
88+
}
89+
90+
/// Cancel an installation if it's in the queue
91+
func cancelInstallation(packageName: String) {
92+
installationQueue.removeAll { $0.0.name == packageName }
93+
installationStatus[packageName] = .cancelled
94+
95+
// Notify UI that installation was cancelled
96+
DispatchQueue.main.async {
97+
NotificationCenter.default.post(
98+
name: .installationStatusChanged,
99+
object: nil,
100+
userInfo: ["packageName": packageName, "status": PackageInstallationStatus.cancelled]
101+
)
102+
}
103+
}
104+
105+
/// Get the current status of an installation
106+
func getInstallationStatus(packageName: String) -> PackageInstallationStatus {
107+
return installationStatus[packageName] ?? .notQueued
108+
}
109+
}
110+
111+
/// Status of a package installation
112+
enum PackageInstallationStatus: Equatable {
113+
case notQueued
114+
case queued
115+
case installing
116+
case installed
117+
case failed(Error)
118+
case cancelled
119+
120+
static func == (lhs: PackageInstallationStatus, rhs: PackageInstallationStatus) -> Bool {
121+
switch (lhs, rhs) {
122+
case (.notQueued, .notQueued):
123+
return true
124+
case (.queued, .queued):
125+
return true
126+
case (.installing, .installing):
127+
return true
128+
case (.installed, .installed):
129+
return true
130+
case (.cancelled, .cancelled):
131+
return true
132+
case (.failed, .failed):
133+
return true
134+
default:
135+
return false
136+
}
137+
}
138+
}
139+
140+
extension Notification.Name {
141+
static let installationStatusChanged = Notification.Name("installationStatusChanged")
142+
}

CodeEdit/Features/LSP/Registry/RegistryManager.swift

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ final class RegistryManager {
2727
private let checksumURL = URL(
2828
string: "https://github.com/mason-org/mason-registry/releases/latest/download/checksums.txt"
2929
)!
30-
/// A queue for installing packages concurrently
31-
private let installQueue: OperationQueue
32-
/// The max amount of package concurrent installs
33-
private let maxConcurrentInstallations: Int = 2
3430

3531
/// Rreference to cached registry data. Will be removed from memory after a certain amount of time.
3632
private var cachedRegistry: CachedRegistry?
@@ -63,11 +59,6 @@ final class RegistryManager {
6359
@AppSettings(\.languageServers.installedLanguageServers)
6460
var installedLanguageServers: [String: SettingsData.InstalledLanguageServer]
6561

66-
private init() {
67-
installQueue = OperationQueue()
68-
installQueue.maxConcurrentOperationCount = maxConcurrentInstallations
69-
}
70-
7162
deinit {
7263
cleanupTimer?.invalidate()
7364
}
@@ -153,11 +144,13 @@ final class RegistryManager {
153144
}
154145

155146
// Save to settings
156-
installedLanguageServers[entry.name] = .init(
157-
packageName: entry.name,
158-
isEnabled: true,
159-
version: method.version ?? ""
160-
)
147+
DispatchQueue.main.async { [weak self] in
148+
self?.installedLanguageServers[entry.name] = .init(
149+
packageName: entry.name,
150+
isEnabled: true,
151+
version: method.version ?? ""
152+
)
153+
}
161154
Self.updateActivityViewer(entry.name, activityTitle, fail: false)
162155
}
163156

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

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct LanguageServerRowView: View, Equatable {
1919
private let cleanedSubtitle: String
2020

2121
@State private var isHovering: Bool = false
22-
@State private var isInstalling: Bool = false
22+
@State private var installationStatus: PackageInstallationStatus = .notQueued
2323
@State private var isInstalled: Bool = false
2424
@State private var isEnabled = false
2525

@@ -77,16 +77,38 @@ struct LanguageServerRowView: View, Equatable {
7777
.onHover { hovering in
7878
isHovering = hovering
7979
}
80+
.onAppear {
81+
// Check if this package is already in the installation queue
82+
installationStatus = InstallationQueueManager.shared.getInstallationStatus(packageName: packageName)
83+
}
84+
.onReceive(NotificationCenter.default.publisher(for: .installationStatusChanged)) { notification in
85+
if let notificationPackageName = notification.userInfo?["packageName"] as? String,
86+
notificationPackageName == packageName,
87+
let status = notification.userInfo?["status"] as? PackageInstallationStatus {
88+
installationStatus = status
89+
if case .installed = status {
90+
isInstalled = true
91+
isEnabled = true
92+
}
93+
}
94+
}
8095
}
8196

8297
@ViewBuilder
8398
private func installationButton() -> some View {
8499
if isInstalled {
85100
installedRow()
86-
} else if isInstalling {
87-
isInstallingRow()
88-
} else if isHovering {
89-
isHoveringRow()
101+
} else {
102+
switch installationStatus {
103+
case .installing, .queued:
104+
isInstallingRow()
105+
case .failed:
106+
failedRow()
107+
default:
108+
if isHovering {
109+
isHoveringRow()
110+
}
111+
}
90112
}
91113
}
92114

@@ -95,7 +117,6 @@ struct LanguageServerRowView: View, Equatable {
95117
HStack {
96118
if isHovering {
97119
Button {
98-
isInstalling = false
99120
isInstalled = false
100121
} label: {
101122
Text("Remove")
@@ -113,32 +134,49 @@ struct LanguageServerRowView: View, Equatable {
113134

114135
@ViewBuilder
115136
private func isInstallingRow() -> some View {
116-
ZStack {
117-
CECircularProgressView()
118-
.frame(width: 20, height: 20)
119-
Button {
120-
isInstalling = false
121-
onCancel()
122-
} label: {
123-
Image(systemName: "stop.fill")
124-
.font(.system(size: 8))
125-
.foregroundColor(.blue)
137+
HStack {
138+
if case .queued = installationStatus {
139+
Text("Queued")
140+
.font(.caption)
141+
.foregroundColor(.secondary)
142+
}
143+
144+
ZStack {
145+
CECircularProgressView()
146+
.frame(width: 20, height: 20)
147+
Button {
148+
InstallationQueueManager.shared.cancelInstallation(packageName: packageName)
149+
onCancel()
150+
} label: {
151+
Image(systemName: "stop.fill")
152+
.font(.system(size: 8))
153+
.foregroundColor(.blue)
154+
}
155+
.buttonStyle(.plain)
156+
.contentShape(Rectangle())
126157
}
127-
.buttonStyle(.plain)
128-
.contentShape(Rectangle())
129158
}
130159
}
131160

132161
@ViewBuilder
133-
private func isHoveringRow() -> some View {
162+
private func failedRow() -> some View {
134163
Button {
135-
isInstalling = true
164+
// Reset status and retry installation
165+
installationStatus = .notQueued
166+
Task {
167+
await onInstall()
168+
}
169+
} label: {
170+
Text("Retry")
171+
.foregroundColor(.red)
172+
}
173+
}
136174

175+
@ViewBuilder
176+
private func isHoveringRow() -> some View {
177+
Button {
137178
Task {
138179
await onInstall()
139-
isInstalling = false
140-
isInstalled = true
141-
isEnabled = true
142180
}
143181
} label: {
144182
Text("Install")

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@ struct LanguageServersView: View {
3030
subtitle: item.description,
3131
isInstalled: RegistryManager.shared.installedLanguageServers[item.name] != nil,
3232
isEnabled: RegistryManager.shared.installedLanguageServers[item.name]?.isEnabled ?? false,
33-
onCancel: { },
33+
onCancel: {
34+
InstallationQueueManager.shared.cancelInstallation(packageName: item.name)
35+
},
3436
onInstall: {
35-
do {
36-
try await RegistryManager.shared.installPackage(package: item)
37-
} catch {
38-
didError = true
39-
installationFailure = InstallationFailure(error: error.localizedDescription)
37+
let item = item // Capture for closure
38+
InstallationQueueManager.shared.queueInstallation(package: item) { result in
39+
switch result {
40+
case .success:
41+
break
42+
case .failure(let error):
43+
didError = true
44+
installationFailure = InstallationFailure(error: error.localizedDescription)
45+
}
4046
}
4147
}
4248
)

0 commit comments

Comments
 (0)