Skip to content

Commit f044fd9

Browse files
Language server installation menu (#1997)
### Description A system to allow users to download and manage LSP servers from the settings menu. This utilizes the mason registry to track the language servers. ### Related Issues ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots
1 parent 704da8a commit f044fd9

33 files changed

+2837
-2
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
283BDCBD2972EEBD002AFF81 /* Package.resolved in Resources */ = {isa = PBXBuildFile; fileRef = 283BDCBC2972EEBD002AFF81 /* Package.resolved */; };
1212
284DC8512978BA2600BF2770 /* .all-contributorsrc in Resources */ = {isa = PBXBuildFile; fileRef = 284DC8502978BA2600BF2770 /* .all-contributorsrc */; };
1313
2BE487F428245162003F3F64 /* OpenWithCodeEdit.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
14+
302AD7FF2D8054D500231E16 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 30818CB42D4E563900967860 /* ZIPFoundation */; };
1415
30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; };
1516
30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; };
1617
583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; };
@@ -32,6 +33,7 @@
3233
6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 6C81916A29B41DD300B75C92 /* DequeModule */; };
3334
6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */; };
3435
6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */; };
36+
6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9DB9E32D55656300ACD86E /* CodeEditSourceEditor */; };
3537
6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; };
3638
6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; };
3739
6CAAF69429BCD78600A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; };
@@ -167,6 +169,7 @@
167169
isa = PBXFrameworksBuildPhase;
168170
buildActionMask = 2147483647;
169171
files = (
172+
302AD7FF2D8054D500231E16 /* ZIPFoundation in Frameworks */,
170173
6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */,
171174
6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */,
172175
58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */,
@@ -192,6 +195,7 @@
192195
6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */,
193196
6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */,
194197
6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */,
198+
6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */,
195199
);
196200
runOnlyForDeploymentPostprocessing = 0;
197201
};
@@ -324,6 +328,7 @@
324328
6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */,
325329
6CB94D022CA1205100E8651C /* AsyncAlgorithms */,
326330
6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */,
331+
30818CB42D4E563900967860 /* ZIPFoundation */,
327332
6C73A6D22D4F1E550012D95C /* CodeEditSourceEditor */,
328333
5EACE6212DF4BF08005E08B8 /* WelcomeWindow */,
329334
5E4485602DF600D9008BBE69 /* AboutWindow */,
@@ -431,6 +436,7 @@
431436
303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */,
432437
6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
433438
6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */,
439+
30ED7B722DD299E600ACC922 /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
434440
5EACE6202DF4BF08005E08B8 /* XCRemoteSwiftPackageReference "WelcomeWindow" */,
435441
5E44855F2DF600D9008BBE69 /* XCRemoteSwiftPackageReference "AboutWindow" */,
436442
6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
@@ -1673,6 +1679,14 @@
16731679
minimumVersion = 0.13.2;
16741680
};
16751681
};
1682+
30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = {
1683+
isa = XCRemoteSwiftPackageReference;
1684+
repositoryURL = "https://github.com/weichsel/ZIPFoundation";
1685+
requirement = {
1686+
kind = upToNextMajorVersion;
1687+
minimumVersion = 0.9.19;
1688+
};
1689+
};
16761690
30CB648F2C16CA8100CC8A9E /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */ = {
16771691
isa = XCRemoteSwiftPackageReference;
16781692
repositoryURL = "https://github.com/ChimeHQ/LanguageServerProtocol";
@@ -1689,6 +1703,14 @@
16891703
minimumVersion = 0.8.0;
16901704
};
16911705
};
1706+
30ED7B722DD299E600ACC922 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = {
1707+
isa = XCRemoteSwiftPackageReference;
1708+
repositoryURL = "https://github.com/weichsel/ZIPFoundation";
1709+
requirement = {
1710+
kind = upToNextMajorVersion;
1711+
minimumVersion = 0.9.19;
1712+
};
1713+
};
16921714
583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
16931715
isa = XCRemoteSwiftPackageReference;
16941716
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing.git";
@@ -1785,6 +1807,14 @@
17851807
minimumVersion = 1.2.0;
17861808
};
17871809
};
1810+
6C9DB9E22D55656300ACD86E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = {
1811+
isa = XCRemoteSwiftPackageReference;
1812+
repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor";
1813+
requirement = {
1814+
kind = upToNextMajorVersion;
1815+
minimumVersion = 0.10.0;
1816+
};
1817+
};
17881818
6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = {
17891819
isa = XCRemoteSwiftPackageReference;
17901820
repositoryURL = "https://github.com/apple/swift-async-algorithms.git";
@@ -1801,6 +1831,11 @@
18011831
package = 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */;
18021832
productName = CodeEditSymbols;
18031833
};
1834+
30818CB42D4E563900967860 /* ZIPFoundation */ = {
1835+
isa = XCSwiftPackageProductDependency;
1836+
package = 30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
1837+
productName = ZIPFoundation;
1838+
};
18041839
30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */ = {
18051840
isa = XCSwiftPackageProductDependency;
18061841
package = 30CB648F2C16CA8100CC8A9E /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */;
@@ -1897,6 +1932,11 @@
18971932
package = 6C85BB422C210EFD00EB5DEF /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
18981933
productName = SwiftUIIntrospect;
18991934
};
1935+
6C9DB9E32D55656300ACD86E /* CodeEditSourceEditor */ = {
1936+
isa = XCSwiftPackageProductDependency;
1937+
package = 6C9DB9E22D55656300ACD86E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */;
1938+
productName = CodeEditSourceEditor;
1939+
};
19001940
6CB4463F2B6DFF3A00539ED0 /* CodeEditSourceEditor */ = {
19011941
isa = XCSwiftPackageProductDependency;
19021942
productName = CodeEditSourceEditor;

CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// InstallationMethod.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 5/12/25.
6+
//
7+
8+
import Foundation
9+
10+
/// Installation method enum with all supported types
11+
enum InstallationMethod: Equatable {
12+
/// For standard package manager installations
13+
case standardPackage(source: PackageSource)
14+
/// For packages that need to be built from source with custom build steps
15+
case sourceBuild(source: PackageSource, command: String)
16+
/// For direct binary downloads
17+
case binaryDownload(source: PackageSource, url: URL)
18+
/// For installations that aren't recognized
19+
case unknown
20+
21+
var packageName: String? {
22+
switch self {
23+
case .standardPackage(let source),
24+
.sourceBuild(let source, _),
25+
.binaryDownload(let source, _):
26+
return source.pkgName
27+
case .unknown:
28+
return nil
29+
}
30+
}
31+
32+
var version: String? {
33+
switch self {
34+
case .standardPackage(let source),
35+
.sourceBuild(let source, _),
36+
.binaryDownload(let source, _):
37+
return source.version
38+
case .unknown:
39+
return nil
40+
}
41+
}
42+
43+
var packageManagerType: PackageManagerType? {
44+
switch self {
45+
case .standardPackage(let source),
46+
.sourceBuild(let source, _),
47+
.binaryDownload(let source, _):
48+
return source.type
49+
case .unknown:
50+
return nil
51+
}
52+
}
53+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
final 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: Set<String> = []
20+
/// Installation status dictionary
21+
private var installationStatus: [String: PackageInstallationStatus] = [:]
22+
23+
/// Add a package to the installation queue
24+
func queueInstallation(package: RegistryItem, completion: @escaping (Result<Void, Error>) -> Void) {
25+
// If we're already at max capacity and this isn't already running, mark as queued
26+
if runningInstallations.count >= maxConcurrentInstallations && !runningInstallations.contains(package.name) {
27+
installationStatus[package.name] = .queued
28+
installationQueue.append((package, completion))
29+
30+
// Notify UI that package is queued
31+
DispatchQueue.main.async {
32+
NotificationCenter.default.post(
33+
name: .installationStatusChanged,
34+
object: nil,
35+
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.queued]
36+
)
37+
}
38+
} else {
39+
startInstallation(package: package, completion: completion)
40+
}
41+
}
42+
43+
/// Starts the actual installation process for a package
44+
private func startInstallation(package: RegistryItem, completion: @escaping (Result<Void, Error>) -> Void) {
45+
installationStatus[package.name] = .installing
46+
runningInstallations.insert(package.name)
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.remove(package.name)
85+
processNextInstallations()
86+
}
87+
}
88+
89+
/// Process next installations from the queue if possible
90+
private func processNextInstallations() {
91+
while runningInstallations.count < maxConcurrentInstallations && !installationQueue.isEmpty {
92+
let (package, completion) = installationQueue.removeFirst()
93+
if runningInstallations.contains(package.name) {
94+
continue
95+
}
96+
97+
startInstallation(package: package, completion: completion)
98+
}
99+
}
100+
101+
/// Cancel an installation if it's in the queue
102+
func cancelInstallation(packageName: String) {
103+
installationQueue.removeAll { $0.0.name == packageName }
104+
installationStatus[packageName] = .cancelled
105+
runningInstallations.remove(packageName)
106+
107+
// Notify UI that installation was cancelled
108+
DispatchQueue.main.async {
109+
NotificationCenter.default.post(
110+
name: .installationStatusChanged,
111+
object: nil,
112+
userInfo: ["packageName": packageName, "status": PackageInstallationStatus.cancelled]
113+
)
114+
}
115+
processNextInstallations()
116+
}
117+
118+
/// Get the current status of an installation
119+
func getInstallationStatus(packageName: String) -> PackageInstallationStatus {
120+
return installationStatus[packageName] ?? .notQueued
121+
}
122+
123+
/// Cleans up installation status by removing completed or failed installations
124+
func cleanUpInstallationStatus() {
125+
let statusKeys = installationStatus.keys.map { $0 }
126+
for packageName in statusKeys {
127+
if let status = installationStatus[packageName] {
128+
switch status {
129+
case .installed, .failed, .cancelled:
130+
installationStatus.removeValue(forKey: packageName)
131+
case .queued, .installing, .notQueued:
132+
break
133+
}
134+
}
135+
}
136+
137+
// If an item is in runningInstallations but not in an active state in the status dictionary,
138+
// it might be a stale reference
139+
let currentRunning = runningInstallations.map { $0 }
140+
for packageName in currentRunning {
141+
let status = installationStatus[packageName]
142+
if status != .installing {
143+
runningInstallations.remove(packageName)
144+
}
145+
}
146+
147+
// Check for orphaned queue items
148+
installationQueue = installationQueue.filter { item, _ in
149+
return installationStatus[item.name] == .queued
150+
}
151+
}
152+
}
153+
154+
/// Status of a package installation
155+
enum PackageInstallationStatus: Equatable {
156+
case notQueued
157+
case queued
158+
case installing
159+
case installed
160+
case failed(Error)
161+
case cancelled
162+
163+
static func == (lhs: PackageInstallationStatus, rhs: PackageInstallationStatus) -> Bool {
164+
switch (lhs, rhs) {
165+
case (.notQueued, .notQueued):
166+
return true
167+
case (.queued, .queued):
168+
return true
169+
case (.installing, .installing):
170+
return true
171+
case (.installed, .installed):
172+
return true
173+
case (.cancelled, .cancelled):
174+
return true
175+
case (.failed, .failed):
176+
return true
177+
default:
178+
return false
179+
}
180+
}
181+
}
182+
183+
extension Notification.Name {
184+
static let installationStatusChanged = Notification.Name("installationStatusChanged")
185+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// PackageManagerError.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 5/12/25.
6+
//
7+
8+
enum PackageManagerError: Error {
9+
case packageManagerNotInstalled
10+
case initializationFailed(String)
11+
case installationFailed(String)
12+
case invalidConfiguration
13+
}

0 commit comments

Comments
 (0)