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 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
24b3c37
[chore:] Bump Version Number (#2021)
thecoolwinter Apr 8, 2025
2fb1157
Update pre-release.yml - xcpretty
thecoolwinter Apr 8, 2025
8afeb08
Bump Build Number to 45 (#2022)
github-actions[bot] Apr 8, 2025
64ad6a3
Add workflow_dispatch to Release Notes CI
thecoolwinter Apr 8, 2025
a0d1fb9
docs: add pro100filipp as a contributor for code (#2023)
allcontributors[bot] Apr 8, 2025
29bf445
Source Control Filter (#2024)
LeonardoLarranaga Apr 12, 2025
3bafb1c
Disable 'Export All Custom Themes' if custom themes is empty (#2027)
LeonardoLarranaga Apr 16, 2025
f2337c8
Update README.md
austincondiff Apr 19, 2025
14d9709
Sort alphabetically - Folders on top (#2028)
LeonardoLarranaga Apr 20, 2025
1ec7811
Upload dSYMs When Archiving
thecoolwinter Apr 21, 2025
32a3791
Fix Tasks Not Saving (#2034)
thecoolwinter Apr 22, 2025
89c6cc5
Add A Minimap (#2032)
thecoolwinter Apr 27, 2025
e464603
Fix Getting Stuck in Sub-View within Settings (#2038)
Kihron May 6, 2025
3ca4a3a
Fixed History Inspector popover UI bug (#2041)
austincondiff May 9, 2025
d92d6c6
Language Server Syntax Highlights (#1985)
thecoolwinter May 9, 2025
4bd724e
Refactors
FastestMolasses May 12, 2025
f358ab9
Refactors
FastestMolasses May 19, 2025
daae205
Refactors
FastestMolasses May 19, 2025
9fcbfd8
Merge branch 'main' into lsp-install
FastestMolasses May 19, 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
41 changes: 40 additions & 1 deletion 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 All @@ -28,6 +29,7 @@
6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 6C81916A29B41DD300B75C92 /* DequeModule */; };
6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */; };
6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */; };
6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9DB9E32D55656300ACD86E /* CodeEditSourceEditor */; };
6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; };
6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; };
6CAAF69429BCD78600A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; };
Expand Down Expand Up @@ -163,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 All @@ -184,6 +187,7 @@
6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */,
6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */,
6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */,
6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -316,7 +320,7 @@
6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */,
6CB94D022CA1205100E8651C /* AsyncAlgorithms */,
6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */,
6C73A6D22D4F1E550012D95C /* CodeEditSourceEditor */,
30818CB42D4E563900967860 /* ZIPFoundation */,
);
productName = CodeEdit;
productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */;
Expand Down Expand Up @@ -420,6 +424,7 @@
6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */,
6CF368562DBBD274006A77FD /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
30ED7B722DD299E600ACC922 /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
);
preferredProjectObjectVersion = 55;
productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */;
Expand Down Expand Up @@ -1649,6 +1654,14 @@
minimumVersion = 0.13.2;
};
};
30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/weichsel/ZIPFoundation";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.19;
};
};
30CB648F2C16CA8100CC8A9E /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ChimeHQ/LanguageServerProtocol";
Expand All @@ -1665,6 +1678,14 @@
minimumVersion = 0.8.0;
};
};
30ED7B722DD299E600ACC922 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/weichsel/ZIPFoundation";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.19;
};
};
583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing.git";
Expand Down Expand Up @@ -1737,6 +1758,14 @@
minimumVersion = 1.2.0;
};
};
6C9DB9E22D55656300ACD86E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.10.0;
};
};
6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-async-algorithms.git";
Expand All @@ -1761,6 +1790,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 Expand Up @@ -1838,6 +1872,11 @@
package = 6C85BB422C210EFD00EB5DEF /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
productName = SwiftUIIntrospect;
};
6C9DB9E32D55656300ACD86E /* CodeEditSourceEditor */ = {
isa = XCSwiftPackageProductDependency;
package = 6C9DB9E22D55656300ACD86E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */;
productName = CodeEditSourceEditor;
};
6CB4463F2B6DFF3A00539ED0 /* CodeEditSourceEditor */ = {
isa = XCSwiftPackageProductDependency;
productName = CodeEditSourceEditor;
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" : "412b0a26cbeb3f3148a1933dd598c976defe92a6",
"version" : "0.12.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
54 changes: 54 additions & 0 deletions CodeEdit/Features/LSP/Registry/InstallationMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// InstallationMethod.swift
// CodeEdit
//
// Created by Abe Malla on 5/12/25.
//

import Foundation

/// Installation method enum with all supported types
enum InstallationMethod: Equatable {
/// For standard package manager installations
case standardPackage(source: PackageSource)
/// For packages that need to be built from source with custom build steps
case sourceBuild(source: PackageSource, command: String)
/// For direct binary downloads
case binaryDownload(source: PackageSource, url: URL)
/// For installations that aren't recognized
case unknown

var packageName: String? {
switch self {
case .standardPackage(let source),
.sourceBuild(let source, _),
.binaryDownload(let source, _):
return source.pkgName
case .unknown:
return nil
}
}

var version: String? {
switch self {
case .standardPackage(let source),
.sourceBuild(let source, _),
.binaryDownload(let source, _):
return source.version
case .unknown:
return nil
}
}

var packageManagerType: PackageManagerType? {
switch self {
case .standardPackage(let source),
.sourceBuild(let source, _),
.binaryDownload(let source, _):
return source.type
case .unknown:
return nil
}
}
}

Check failure on line 54 in CodeEdit/Features/LSP/Registry/InstallationMethod.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Files should have a single trailing newline (trailing_newline)
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")
}
13 changes: 13 additions & 0 deletions CodeEdit/Features/LSP/Registry/PackageManagerError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// PackageManagerError.swift
// CodeEdit
//
// Created by Abe Malla on 5/12/25.
//

enum PackageManagerError: Error {
case packageManagerNotInstalled
case initializationFailed(String)
case installationFailed(String)
case invalidConfiguration
}
Loading
Loading