Skip to content

Language server installation menu #1997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5ea9de5
Language server installation menu
FastestMolasses Feb 25, 2025
d8ca6b2
Lint
FastestMolasses Mar 4, 2025
95479a5
Merge branch 'main' into lsp-install
FastestMolasses Mar 4, 2025
ca48313
Added notification on file open
FastestMolasses Mar 5, 2025
80d16d9
Small update
FastestMolasses Mar 9, 2025
4c22bc3
Merge branch 'main' into lsp-install
FastestMolasses Mar 9, 2025
21f7138
Refactors
FastestMolasses Mar 11, 2025
c827108
Connect install button
FastestMolasses Mar 11, 2025
b9d3945
Refactors
FastestMolasses Mar 12, 2025
6c9c48a
Fix lint
FastestMolasses Mar 13, 2025
ee93abb
Refactors, added settings loading
FastestMolasses Mar 13, 2025
2f2c022
Refactors, added text icon colors
FastestMolasses Mar 14, 2025
ed9dfe6
Update notifications
FastestMolasses Mar 14, 2025
64ceb74
Add more documentation
FastestMolasses Mar 14, 2025
04fde2c
Added installation queuing, fix runtime warnings
FastestMolasses Mar 14, 2025
6b40bb3
Convert to actor
FastestMolasses Mar 14, 2025
0fe73ef
Refactors, fix and update removals
FastestMolasses Mar 14, 2025
b5d7186
Fix queue feedback and clean up memory
FastestMolasses Mar 14, 2025
1db6a66
Small refactors
FastestMolasses Mar 15, 2025
1ec3820
Temporarily removed Language Servers menu
FastestMolasses Mar 16, 2025
6747bf5
Small refactors
FastestMolasses Mar 31, 2025
08963a2
Merge branch 'main' into lsp-install
FastestMolasses Mar 31, 2025
ae57197
Merge branch 'main' into lsp-install
thecoolwinter Apr 9, 2025
74bf7e5
Correct CESE Version In XcodeProj File
thecoolwinter Apr 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions CodeEdit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
283BDCBD2972EEBD002AFF81 /* Package.resolved in Resources */ = {isa = PBXBuildFile; fileRef = 283BDCBC2972EEBD002AFF81 /* Package.resolved */; };
284DC8512978BA2600BF2770 /* .all-contributorsrc in Resources */ = {isa = PBXBuildFile; fileRef = 284DC8502978BA2600BF2770 /* .all-contributorsrc */; };
2BE487F428245162003F3F64 /* OpenWithCodeEdit.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
302AD7FF2D8054D500231E16 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 30818CB42D4E563900967860 /* ZIPFoundation */; };
30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; };
30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; };
583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; };
Expand Down Expand Up @@ -164,6 +165,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
302AD7FF2D8054D500231E16 /* ZIPFoundation in Frameworks */,
6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */,
6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */,
58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */,
Expand Down Expand Up @@ -318,8 +320,7 @@
6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */,
6CB94D022CA1205100E8651C /* AsyncAlgorithms */,
6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */,
6C73A6D22D4F1E550012D95C /* CodeEditSourceEditor */,
6C9DB9E32D55656300ACD86E /* CodeEditSourceEditor */,
30818CB42D4E563900967860 /* ZIPFoundation */,
);
productName = CodeEdit;
productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */;
Expand Down Expand Up @@ -422,7 +423,8 @@
303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */,
6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */,
6C9DB9E22D55656300ACD86E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
30C549D82D77BDF8008DDEF8 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
);
preferredProjectObjectVersion = 55;
productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */;
Expand Down Expand Up @@ -1647,6 +1649,22 @@
minimumVersion = 0.13.2;
};
};
30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/weichsel/ZIPFoundation";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.19;
};
};
30C549D82D77BDF8008DDEF8 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.10.0;
};
};
30CB648F2C16CA8100CC8A9E /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ChimeHQ/LanguageServerProtocol";
Expand Down Expand Up @@ -1759,6 +1777,11 @@
package = 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */;
productName = CodeEditSymbols;
};
30818CB42D4E563900967860 /* ZIPFoundation */ = {
isa = XCSwiftPackageProductDependency;
package = 30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
productName = ZIPFoundation;
};
30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */ = {
isa = XCSwiftPackageProductDependency;
package = 30CB648F2C16CA8100CC8A9E /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
{
"identity" : "codeeditsourceeditor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CodeEditApp/CodeEditSourceEditor",
"location" : "https://github.com/CodeEditApp/CodeEditSourceEditor.git",
"state" : {
"revision" : "6b2c945501f0a5c15d8aa6d159fb2550c391bdd0",
"version" : "0.10.0"
Expand Down Expand Up @@ -278,6 +278,15 @@
"revision" : "d97db6d63507eb62c536bcb2c4ac7d70c8ec665e",
"version" : "0.23.2"
}
},
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/weichsel/ZIPFoundation",
"state" : {
"revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0",
"version" : "0.9.19"
}
}
],
"version" : 3
Expand Down
185 changes: 185 additions & 0 deletions CodeEdit/Features/LSP/Registry/InstallationQueueManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//
// InstallationQueueManager.swift
// CodeEdit
//
// Created by Abe Malla on 3/13/25.
//

import Foundation

/// A class to manage queued installations of language servers
final class InstallationQueueManager {
static let shared: InstallationQueueManager = .init()

/// The maximum number of concurrent installations allowed
private let maxConcurrentInstallations: Int = 2
/// Queue of pending installations
private var installationQueue: [(RegistryItem, (Result<Void, Error>) -> Void)] = []
/// Currently running installations
private var runningInstallations: Set<String> = []
/// Installation status dictionary
private var installationStatus: [String: PackageInstallationStatus] = [:]

/// Add a package to the installation queue
func queueInstallation(package: RegistryItem, completion: @escaping (Result<Void, Error>) -> Void) {
// If we're already at max capacity and this isn't already running, mark as queued
if runningInstallations.count >= maxConcurrentInstallations && !runningInstallations.contains(package.name) {
installationStatus[package.name] = .queued
installationQueue.append((package, completion))

// Notify UI that package is queued
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .installationStatusChanged,
object: nil,
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.queued]
)
}
} else {
startInstallation(package: package, completion: completion)
}
}

/// Starts the actual installation process for a package
private func startInstallation(package: RegistryItem, completion: @escaping (Result<Void, Error>) -> Void) {
installationStatus[package.name] = .installing
runningInstallations.insert(package.name)

// Notify UI that installation is now in progress
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .installationStatusChanged,
object: nil,
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.installing]
)
}

Task {
do {
try await RegistryManager.shared.installPackage(package: package)

// Notify UI that installation is complete
installationStatus[package.name] = .installed
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .installationStatusChanged,
object: nil,
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.installed]
)
completion(.success(()))
}
} catch {
// Notify UI that installation failed
installationStatus[package.name] = .failed(error)
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .installationStatusChanged,
object: nil,
userInfo: ["packageName": package.name, "status": PackageInstallationStatus.failed(error)]
)
completion(.failure(error))
}
}

runningInstallations.remove(package.name)
processNextInstallations()
}
}

/// Process next installations from the queue if possible
private func processNextInstallations() {
while runningInstallations.count < maxConcurrentInstallations && !installationQueue.isEmpty {
let (package, completion) = installationQueue.removeFirst()
if runningInstallations.contains(package.name) {
continue
}

startInstallation(package: package, completion: completion)
}
}

/// Cancel an installation if it's in the queue
func cancelInstallation(packageName: String) {
installationQueue.removeAll { $0.0.name == packageName }
installationStatus[packageName] = .cancelled
runningInstallations.remove(packageName)

// Notify UI that installation was cancelled
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .installationStatusChanged,
object: nil,
userInfo: ["packageName": packageName, "status": PackageInstallationStatus.cancelled]
)
}
processNextInstallations()
}

/// Get the current status of an installation
func getInstallationStatus(packageName: String) -> PackageInstallationStatus {
return installationStatus[packageName] ?? .notQueued
}

/// Cleans up installation status by removing completed or failed installations
func cleanUpInstallationStatus() {
let statusKeys = installationStatus.keys.map { $0 }
for packageName in statusKeys {
if let status = installationStatus[packageName] {
switch status {
case .installed, .failed, .cancelled:
installationStatus.removeValue(forKey: packageName)
case .queued, .installing, .notQueued:
break
}
}
}

// If an item is in runningInstallations but not in an active state in the status dictionary,
// it might be a stale reference
let currentRunning = runningInstallations.map { $0 }
for packageName in currentRunning {
let status = installationStatus[packageName]
if status != .installing {
runningInstallations.remove(packageName)
}
}

// Check for orphaned queue items
installationQueue = installationQueue.filter { item, _ in
return installationStatus[item.name] == .queued
}
}
}

/// Status of a package installation
enum PackageInstallationStatus: Equatable {
case notQueued
case queued
case installing
case installed
case failed(Error)
case cancelled

static func == (lhs: PackageInstallationStatus, rhs: PackageInstallationStatus) -> Bool {
switch (lhs, rhs) {
case (.notQueued, .notQueued):
return true
case (.queued, .queued):
return true
case (.installing, .installing):
return true
case (.installed, .installed):
return true
case (.cancelled, .cancelled):
return true
case (.failed, .failed):
return true
default:
return false
}
}
}

extension Notification.Name {
static let installationStatusChanged = Notification.Name("installationStatusChanged")
}
Loading