diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist
index fb1b719d8..793c19bae 100644
--- a/CodeEdit/Info.plist
+++ b/CodeEdit/Info.plist
@@ -45,6 +45,6 @@
GitHash
- c7fee3a29a4477ce93e6e892af08e32078fa19ac
+ df5126734cd5f8c976187eba5d43204046775769
diff --git a/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift b/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift
index cb14c4780..4573bc854 100644
--- a/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift
+++ b/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift
@@ -9,6 +9,7 @@ import Foundation
import Light_Swift_Untar
import CodeEditKit
import GRDB
+import LSP
/// Class which handles all extensions (its bundles, instances for each workspace and so on)
public final class ExtensionsManager {
@@ -28,6 +29,7 @@ public final class ExtensionsManager {
var loadedBundles: [UUID: Bundle] = [:]
var loadedPlugins: [PluginWorkspaceKey: ExtensionInterface] = [:]
+ var loadedLanguageServers: [PluginWorkspaceKey: LSPClient] = [:]
init() throws {
self.codeeditFolder = try FileManager.default
@@ -53,6 +55,11 @@ public final class ExtensionsManager {
table.column("loadable", .boolean)
}
}
+ migrator.registerMigration("v1.0.1") { database in
+ try database.alter(table: DownloadedPlugin.databaseTableName) { body in
+ body.add(column: "sdk", .text).defaults(to: "swift")
+ }
+ }
try migrator.migrate(self.dbQueue)
}
@@ -64,30 +71,76 @@ public final class ExtensionsManager {
}.forEach { (key: PluginWorkspaceKey, _) in
loadedPlugins.removeValue(forKey: key)
}
- }
- private func getExtensionBuilder(id: UUID) throws -> ExtensionBuilder.Type? {
- if loadedBundles.keys.contains(id) {
- return loadedBundles[id]?.principalClass as? ExtensionBuilder.Type
+ loadedLanguageServers.filter { elem in
+ return elem.key.workspace == url
+ }.forEach { (key: PluginWorkspaceKey, client: LSPClient) in
+ client.close()
+ loadedLanguageServers.removeValue(forKey: key)
}
+ }
+ private func loadBundle(id: UUID, withExtension ext: String) throws -> Bundle? {
guard let bundleURL = try FileManager.default.contentsOfDirectory(
at: extensionsFolder.appendingPathComponent(id.uuidString,
isDirectory: true),
includingPropertiesForKeys: nil,
options: .skipsPackageDescendants
- ).first else { return nil }
+ ).first(where: {$0.pathExtension == ext}) else { return nil }
- guard bundleURL.pathExtension == "ceext" else { return nil }
guard let bundle = Bundle(url: bundleURL) else { return nil }
- guard bundle.load() else { return nil }
-
loadedBundles[id] = bundle
+ return bundle
+ }
+
+ private func getExtensionBuilder(id: UUID) throws -> ExtensionBuilder.Type? {
+ if loadedBundles.keys.contains(id) {
+ return loadedBundles[id]?.principalClass as? ExtensionBuilder.Type
+ }
+
+ guard let bundle = try loadBundle(id: id, withExtension: "ceext") else {
+ return nil
+ }
+
+ guard bundle.load() else { return nil }
+
return bundle.principalClass as? ExtensionBuilder.Type
}
+ private func getLSPClient(id: UUID, workspaceURL: URL) throws -> LSPClient? {
+ if loadedBundles.keys.contains(id) {
+ guard let lspFile = loadedBundles[id]?.infoDictionary?["CELSPExecutable"] as? String else {
+ return nil
+ }
+
+ guard let lspURL = loadedBundles[id]?.url(forResource: lspFile, withExtension: nil) else {
+ return nil
+ }
+
+ return try LSPClient(lspURL,
+ workspace: workspaceURL,
+ arguments: loadedBundles[id]?.infoDictionary?["CELSPArguments"] as? [String])
+ }
+
+ guard let bundle = try loadBundle(id: id, withExtension: "celsp") else {
+ return nil
+ }
+
+ guard let lspFile = bundle.infoDictionary?["CELSPExecutable"] as? String else {
+ return nil
+ }
+
+ guard let lspURL = bundle.url(forResource: lspFile, withExtension: nil) else {
+ return nil
+ }
+
+ return try LSPClient(lspURL,
+ workspace: workspaceURL,
+ arguments: loadedBundles[id]?.infoDictionary?["CELSPArguments"] as? [String])
+ }
+
/// Preloads all extensions' bundles to `loadedBundles`
public func preload() throws {
let plugins = try self.dbQueue.read { database in
@@ -95,27 +148,42 @@ public final class ExtensionsManager {
}
try plugins.forEach { plugin in
- _ = try getExtensionBuilder(id: plugin.release)
+ switch plugin.sdk {
+ case .swift:
+ _ = try loadBundle(id: plugin.release, withExtension: "ceext")
+ case .languageServer:
+ _ = try loadBundle(id: plugin.release, withExtension: "celsp")
+ }
}
}
- /// Loads extensions' bundles which were not loaded before and creates `ExtensionInterface` instances
- /// with `ExtensionAPI` created using specified initializer
- /// - Parameter apiInitializer: function which creates `ExtensionAPI` instance based on plugin's ID
- public func load(_ apiInitializer: (String) -> ExtensionAPI) throws {
+ /// Loads extensions' bundles which were not loaded before and passes `ExtensionAPI` as a whole class
+ /// or workspace's URL
+ /// - Parameter apiBuilder: function which creates `ExtensionAPI` instance based on plugin's ID
+ public func load(_ apiBuilder: (String) -> ExtensionAPI) throws {
let plugins = try self.dbQueue.read { database in
- try DownloadedPlugin.filter(Column("loadable") == true).fetchAll(database)
+ try DownloadedPlugin
+ .filter(Column("loadable") == true)
+ .fetchAll(database)
}
try plugins.forEach { plugin in
- guard let builder = try getExtensionBuilder(id: plugin.release) else {
- return
- }
+ let api = apiBuilder(plugin.plugin.uuidString)
+ let key = PluginWorkspaceKey(releaseID: plugin.release, workspace: api.workspaceURL)
- let api = apiInitializer(plugin.plugin.uuidString)
+ switch plugin.sdk {
+ case .swift:
+ guard let builder = try getExtensionBuilder(id: plugin.release) else {
+ return
+ }
- let key = PluginWorkspaceKey(releaseID: plugin.release, workspace: api.workspaceURL)
- loadedPlugins[key] = builder.init().build(withAPI: api)
+ loadedPlugins[key] = builder.init().build(withAPI: api)
+ case .languageServer:
+ guard let client = try getLSPClient(id: plugin.release, workspaceURL: api.workspaceURL) else {
+ return
+ }
+ loadedLanguageServers[key] = client
+ }
}
}
@@ -144,6 +212,7 @@ public final class ExtensionsManager {
)
.appendingPathComponent("\(release.id.uuidString).tar")
+ // TODO: show progress
let (source, _) = try await URLSession.shared.download(from: tarball)
if FileManager.default.fileExists(atPath: cacheTar.path) {
@@ -168,7 +237,7 @@ public final class ExtensionsManager {
// save to db
try await dbQueue.write { database in
- try DownloadedPlugin(plugin: plugin.id, release: release.id, loadable: true)
+ try DownloadedPlugin(plugin: plugin.id, release: release.id, loadable: true, sdk: plugin.sdk)
.insert(database)
}
}
@@ -203,6 +272,13 @@ public final class ExtensionsManager {
}.forEach { (key: PluginWorkspaceKey, _) in
loadedPlugins.removeValue(forKey: key)
}
+
+ loadedLanguageServers.filter { elem in
+ return elem.key.releaseID == entry.release
+ }.forEach { (key: PluginWorkspaceKey, client: LSPClient) in
+ client.close()
+ loadedLanguageServers.removeValue(forKey: key)
+ }
}
/// Checks whether extension's bundle (plugin) is installed
diff --git a/CodeEditModules/Modules/ExtensionsStore/src/Models/DownloadedPlugin.swift b/CodeEditModules/Modules/ExtensionsStore/src/Models/DownloadedPlugin.swift
index 104ec3f7a..627a02334 100644
--- a/CodeEditModules/Modules/ExtensionsStore/src/Models/DownloadedPlugin.swift
+++ b/CodeEditModules/Modules/ExtensionsStore/src/Models/DownloadedPlugin.swift
@@ -15,4 +15,5 @@ public struct DownloadedPlugin: Codable, FetchableRecord, PersistableRecord, Tab
public var plugin: UUID
public var release: UUID
public var loadable: Bool
+ public var sdk: Plugin.SDK
}
diff --git a/CodeEditModules/Modules/ExtensionsStore/src/Models/Plugin.swift b/CodeEditModules/Modules/ExtensionsStore/src/Models/Plugin.swift
index ffe7ec77a..2487a3cd7 100644
--- a/CodeEditModules/Modules/ExtensionsStore/src/Models/Plugin.swift
+++ b/CodeEditModules/Modules/ExtensionsStore/src/Models/Plugin.swift
@@ -18,6 +18,7 @@ public struct Plugin: Codable, Identifiable, Hashable {
public enum SDK: String, Codable, Hashable {
case swift
+ case languageServer = "language_server"
}
public enum ReleaseManagement: String, Codable, Hashable {
diff --git a/CodeEditModules/Modules/LSP/src/LSPClient.swift b/CodeEditModules/Modules/LSP/src/LSPClient.swift
new file mode 100644
index 000000000..63dac3da6
--- /dev/null
+++ b/CodeEditModules/Modules/LSP/src/LSPClient.swift
@@ -0,0 +1,32 @@
+//
+// LSPClient.swift
+//
+//
+// Created by Pavel Kasila on 16.04.22.
+//
+
+import Foundation
+
+/// A LSP client to handle Language Server process
+public class LSPClient {
+ var executable: URL
+ var workspace: URL
+ var process: Process
+
+ /// Initialize new LSP client
+ /// - Parameters:
+ /// - executable: Executable of the Language Server to be run
+ /// - workspace: Workspace's URL
+ /// - arguments: Additional arguments from `CELSPArguments` in `Info.plist` of the Language Server bundle
+ public init(_ executable: URL, workspace: URL, arguments: [String]?) throws {
+ self.executable = executable
+ try FileManager.default.setAttributes([.posixPermissions: 0o555], ofItemAtPath: self.executable.path)
+ self.workspace = workspace
+ self.process = try Process.run(executable, arguments: arguments ?? ["--stdio"], terminationHandler: nil)
+ }
+
+ /// Close the process
+ public func close() {
+ self.process.terminate()
+ }
+}
diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift
index 146449663..de07b1514 100644
--- a/CodeEditModules/Package.swift
+++ b/CodeEditModules/Package.swift
@@ -247,7 +247,8 @@ let package = Package(
dependencies: [
"CodeEditKit",
"Light-Swift-Untar",
- .productItem(name: "GRDB", package: "GRDB.swift", condition: nil)
+ .productItem(name: "GRDB", package: "GRDB.swift", condition: nil),
+ "LSP"
],
path: "Modules/ExtensionsStore/src"
),
@@ -268,5 +269,9 @@ let package = Package(
],
path: "Modules/Feedback/src"
),
+ .target(
+ name: "LSP",
+ path: "Modules/LSP/src"
+ ),
]
)