Skip to content

Commit 6b40bb3

Browse files
Convert to actor
1 parent 04fde2c commit 6b40bb3

File tree

2 files changed

+146
-103
lines changed

2 files changed

+146
-103
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// RegistryManager+Parsing.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 3/14/25.
6+
//
7+
8+
import Foundation
9+
10+
extension RegistryManager {
11+
/// Parse a registry entry and create the appropriate installation method
12+
internal static func parseRegistryEntry(_ entry: RegistryItem) -> InstallationMethod {
13+
let sourceId = entry.source.id
14+
if sourceId.hasPrefix("pkg:cargo/") {
15+
return PackageSourceParser.parseCargoPackage(entry)
16+
} else if sourceId.hasPrefix("pkg:npm/") {
17+
return PackageSourceParser.parseNpmPackage(entry)
18+
} else if sourceId.hasPrefix("pkg:pypi/") {
19+
return PackageSourceParser.parsePythonPackage(entry)
20+
} else if sourceId.hasPrefix("pkg:gem/") {
21+
return PackageSourceParser.parseRubyGem(entry)
22+
} else if sourceId.hasPrefix("pkg:golang/") {
23+
return PackageSourceParser.parseGolangPackage(entry)
24+
} else if sourceId.hasPrefix("pkg:github/") {
25+
return PackageSourceParser.parseGithubPackage(entry)
26+
} else {
27+
return .unknown
28+
}
29+
}
30+
31+
/// Create the appropriate package manager for the given installation method
32+
internal static func createPackageManager(
33+
for method: InstallationMethod,
34+
_ installPath: URL
35+
) -> PackageManagerProtocol? {
36+
switch method.packageManagerType {
37+
case .npm:
38+
return NPMPackageManager(installationDirectory: installPath)
39+
case .cargo:
40+
return CargoPackageManager(installationDirectory: installPath)
41+
case .pip:
42+
return PipPackageManager(installationDirectory: installPath)
43+
case .golang:
44+
return GolangPackageManager(installationDirectory: installPath)
45+
case .github, .sourceBuild:
46+
return GithubPackageManager(installationDirectory: installPath)
47+
case .nuget, .opam, .gem, .composer:
48+
// TODO: IMPLEMENT OTHER PACKAGE MANAGERS
49+
return nil
50+
case .none:
51+
return nil
52+
}
53+
}
54+
}

CodeEdit/Features/LSP/Registry/RegistryManager.swift

Lines changed: 92 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private let installPath = homeDirectory
1616
.appending(path: "CodeEdit")
1717
.appending(path: "language-servers")
1818

19+
@MainActor
1920
final class RegistryManager {
2021
static let shared: RegistryManager = .init()
2122

@@ -47,8 +48,11 @@ final class RegistryManager {
4748
cleanupTimer = Timer.scheduledTimer(
4849
withTimeInterval: CachedRegistry.expirationInterval, repeats: false
4950
) { [weak self] _ in
50-
self?.cachedRegistry = nil
51-
self?.cleanupTimer = nil
51+
Task { @MainActor in
52+
guard let self = self else { return }
53+
self.cachedRegistry = nil
54+
self.cleanupTimer = nil
55+
}
5256
}
5357
return items
5458
}
@@ -65,44 +69,63 @@ final class RegistryManager {
6569

6670
/// Downloads the latest registry and saves to "~/Library/Application Support/CodeEdit/extensions"
6771
func update() async {
68-
async let zipDataTask = download(from: registryURL)
69-
async let checksumsTask = download(from: checksumURL)
72+
// swiftlint:disable:next large_tuple
73+
let result = await Task.detached(priority: .userInitiated) { () -> (
74+
registryData: Data?, checksumData: Data?, error: Error?
75+
) in
76+
do {
77+
async let zipDataTask = Self.download(from: self.registryURL)
78+
async let checksumsTask = Self.download(from: self.checksumURL)
79+
80+
let (registryData, checksumData) = try await (zipDataTask, checksumsTask)
81+
return (registryData, checksumData, nil)
82+
} catch {
83+
return (nil, nil, error)
84+
}
85+
}.value
86+
87+
if let error = result.error {
88+
handleUpdateError(error)
89+
return
90+
}
91+
92+
guard let registryData = result.registryData, let checksumData = result.checksumData else {
93+
return
94+
}
7095

7196
do {
7297
// Make sure the extensions folder exists first
7398
try FileManager.default.createDirectory(at: installPath, withIntermediateDirectories: true)
7499

75-
let (registryData, checksumData) = try await (zipDataTask, checksumsTask)
76-
77100
let tempZipURL = installPath.appending(path: "temp.zip")
78101
let checksumDestination = installPath.appending(path: "checksums.txt")
79102

80-
do {
81-
// Delete existing zip data if it exists
82-
if FileManager.default.fileExists(atPath: tempZipURL.path) {
83-
try FileManager.default.removeItem(at: tempZipURL)
84-
}
85-
let registryJsonPath = installPath.appending(path: "registry.json").path
86-
if FileManager.default.fileExists(atPath: registryJsonPath) {
87-
try FileManager.default.removeItem(atPath: registryJsonPath)
88-
}
89-
90-
// Write the zip data to a temporary file, then unzip
91-
try registryData.write(to: tempZipURL)
92-
try FileManager.default.unzipItem(at: tempZipURL, to: installPath)
103+
// Delete existing zip data if it exists
104+
if FileManager.default.fileExists(atPath: tempZipURL.path) {
93105
try FileManager.default.removeItem(at: tempZipURL)
106+
}
107+
let registryJsonPath = installPath.appending(path: "registry.json").path
108+
if FileManager.default.fileExists(atPath: registryJsonPath) {
109+
try FileManager.default.removeItem(atPath: registryJsonPath)
110+
}
94111

95-
try checksumData.write(to: checksumDestination)
112+
// Write the zip data to a temporary file, then unzip
113+
try registryData.write(to: tempZipURL)
114+
try FileManager.default.unzipItem(at: tempZipURL, to: installPath)
115+
try FileManager.default.removeItem(at: tempZipURL)
96116

97-
DispatchQueue.main.async {
98-
NotificationCenter.default.post(name: .RegistryUpdatedNotification, object: nil)
99-
}
100-
} catch {
101-
print("Error details: \(error)")
102-
throw RegistryManagerError.writeFailed(error: error)
103-
}
104-
} catch let error as RegistryManagerError {
105-
switch error {
117+
try checksumData.write(to: checksumDestination)
118+
119+
NotificationCenter.default.post(name: .RegistryUpdatedNotification, object: nil)
120+
} catch {
121+
print("Error details: \(error)")
122+
handleUpdateError(RegistryManagerError.writeFailed(error: error))
123+
}
124+
}
125+
126+
private func handleUpdateError(_ error: Error) {
127+
if let regError = error as? RegistryManagerError {
128+
switch regError {
106129
case .invalidResponse(let statusCode):
107130
print("Invalid response received: \(statusCode)")
108131
case let .downloadFailed(url, error):
@@ -112,50 +135,56 @@ final class RegistryManager {
112135
case let .writeFailed(error):
113136
print("Failed to write files to disk: \(error.localizedDescription)")
114137
}
115-
} catch {
138+
} else {
116139
print("Unexpected registry error: \(error.localizedDescription)")
117140
}
118141
}
119142

120143
func installPackage(package entry: RegistryItem) async throws {
121-
let method = Self.parseRegistryEntry(entry)
122-
guard let manager = Self.createPackageManager(for: method) else {
123-
throw PackageManagerError.invalidConfiguration
124-
}
144+
return try await Task.detached(priority: .userInitiated) { () in
145+
let method = await Self.parseRegistryEntry(entry)
146+
guard let manager = await Self.createPackageManager(for: method, installPath) else {
147+
throw PackageManagerError.invalidConfiguration
148+
}
125149

126-
// Add to activity viewer
127-
let activityTitle = "\(entry.name)\("@" + (method.version ?? "latest"))"
128-
NotificationCenter.default.post(
129-
name: .taskNotification,
130-
object: nil,
131-
userInfo: [
132-
"id": entry.name,
133-
"action": "create",
134-
"title": "Installing \(activityTitle)"
135-
]
136-
)
150+
// Add to activity viewer
151+
let activityTitle = "\(entry.name)\("@" + (method.version ?? "latest"))"
152+
await MainActor.run {
153+
NotificationCenter.default.post(
154+
name: .taskNotification,
155+
object: nil,
156+
userInfo: [
157+
"id": entry.name,
158+
"action": "create",
159+
"title": "Installing \(activityTitle)"
160+
]
161+
)
162+
}
137163

138-
do {
139-
try await manager.install(method: method)
140-
} catch {
141-
Self.updateActivityViewer(entry.name, activityTitle, fail: true)
142-
// Throw error again so the UI can catch it
143-
throw error
144-
}
164+
do {
165+
try await manager.install(method: method)
166+
} catch {
167+
await MainActor.run {
168+
Self.updateActivityViewer(entry.name, activityTitle, fail: true)
169+
}
170+
// Throw error again so the UI can catch it
171+
throw error
172+
}
145173

146-
// Save to settings
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-
}
154-
Self.updateActivityViewer(entry.name, activityTitle, fail: false)
174+
// Update settings on the main thread
175+
await MainActor.run {
176+
self.installedLanguageServers[entry.name] = .init(
177+
packageName: entry.name,
178+
isEnabled: true,
179+
version: method.version ?? ""
180+
)
181+
Self.updateActivityViewer(entry.name, activityTitle, fail: false)
182+
}
183+
}.value
155184
}
156185

157186
/// Attempts downloading from `url`, with error handling and a retry policy
158-
private func download(from url: URL, attempt: Int = 1) async throws -> Data {
187+
private static func download(from url: URL, attempt: Int = 1) async throws -> Data {
159188
do {
160189
let (data, response) = try await URLSession.shared.data(from: url)
161190

@@ -210,48 +239,8 @@ final class RegistryManager {
210239
}
211240
}
212241

213-
/// Parse a registry entry and create the appropriate installation method
214-
private static func parseRegistryEntry(_ entry: RegistryItem) -> InstallationMethod {
215-
let sourceId = entry.source.id
216-
if sourceId.hasPrefix("pkg:cargo/") {
217-
return PackageSourceParser.parseCargoPackage(entry)
218-
} else if sourceId.hasPrefix("pkg:npm/") {
219-
return PackageSourceParser.parseNpmPackage(entry)
220-
} else if sourceId.hasPrefix("pkg:pypi/") {
221-
return PackageSourceParser.parsePythonPackage(entry)
222-
} else if sourceId.hasPrefix("pkg:gem/") {
223-
return PackageSourceParser.parseRubyGem(entry)
224-
} else if sourceId.hasPrefix("pkg:golang/") {
225-
return PackageSourceParser.parseGolangPackage(entry)
226-
} else if sourceId.hasPrefix("pkg:github/") {
227-
return PackageSourceParser.parseGithubPackage(entry)
228-
} else {
229-
return .unknown
230-
}
231-
}
232-
233-
/// Create the appropriate package manager for the given installation method
234-
private static func createPackageManager(for method: InstallationMethod) -> PackageManagerProtocol? {
235-
switch method.packageManagerType {
236-
case .npm:
237-
return NPMPackageManager(installationDirectory: installPath)
238-
case .cargo:
239-
return CargoPackageManager(installationDirectory: installPath)
240-
case .pip:
241-
return PipPackageManager(installationDirectory: installPath)
242-
case .golang:
243-
return GolangPackageManager(installationDirectory: installPath)
244-
case .github, .sourceBuild:
245-
return GithubPackageManager(installationDirectory: installPath)
246-
case .nuget, .opam, .gem, .composer:
247-
// TODO: IMPLEMENT OTHER PACKAGE MANAGERS
248-
return nil
249-
case .none:
250-
return nil
251-
}
252-
}
253-
254242
/// Updates the activity viewer with the status of the language server installation
243+
@MainActor
255244
private static func updateActivityViewer(
256245
_ id: String,
257246
_ activityName: String,

0 commit comments

Comments
 (0)