From bfe2e04d16ea76737fdaf2a6167e0952d9067a87 Mon Sep 17 00:00:00 2001 From: Pavel Kasila Date: Sat, 16 Apr 2022 13:53:30 +0300 Subject: [PATCH 1/5] Add ability to load LanguageServer extensions --- CodeEdit/Info.plist | 2 +- .../src/ExtensionsManager.swift | 111 ++++++++++++++---- .../src/Models/DownloadedPlugin.swift | 1 + .../ExtensionsStore/src/Models/Plugin.swift | 1 + .../Modules/LSP/src/LSPClient.swift | 18 +++ CodeEditModules/Package.swift | 7 +- 6 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 CodeEditModules/Modules/LSP/src/LSPClient.swift diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist index fb1b719d8..f5220a81d 100644 --- a/CodeEdit/Info.plist +++ b/CodeEdit/Info.plist @@ -45,6 +45,6 @@ GitHash - c7fee3a29a4477ce93e6e892af08e32078fa19ac + 2c0117e07080912ae7d8aa0e23b8b635868113f8 diff --git a/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift b/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift index cb14c4780..77a31f73a 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,71 @@ 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, _) in + 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 LSPClient(lspURL, workspace: workspaceURL) + } + + 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 LSPClient(lspURL, workspace: workspaceURL) + } + /// Preloads all extensions' bundles to `loadedBundles` public func preload() throws { let plugins = try self.dbQueue.read { database in @@ -95,27 +143,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 + } } } @@ -168,7 +231,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 +266,12 @@ 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, _) in + loadedPlugins.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..f8205d0f5 --- /dev/null +++ b/CodeEditModules/Modules/LSP/src/LSPClient.swift @@ -0,0 +1,18 @@ +// +// LSPClient.swift +// +// +// Created by Pavel Kasila on 16.04.22. +// + +import Foundation + +public class LSPClient { + var exec: URL + var workspace: URL + + public init(_ exec: URL, workspace: URL) { + self.exec = exec + self.workspace = workspace + } +} 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" + ), ] ) From cba30ade536c314a5b8ef6450831e3778330fa68 Mon Sep 17 00:00:00 2001 From: Pavel Kasila Date: Sat, 16 Apr 2022 14:20:14 +0300 Subject: [PATCH 2/5] run `Process` for Language Server --- CodeEdit/Info.plist | 2 +- .../ExtensionsStore/src/ExtensionsManager.swift | 16 +++++++++++----- CodeEditModules/Modules/LSP/src/LSPClient.swift | 8 +++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist index f5220a81d..f65d3b4cb 100644 --- a/CodeEdit/Info.plist +++ b/CodeEdit/Info.plist @@ -45,6 +45,6 @@ GitHash - 2c0117e07080912ae7d8aa0e23b8b635868113f8 + bfe2e04d16ea76737fdaf2a6167e0952d9067a87 diff --git a/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift b/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift index 77a31f73a..9f2d5a317 100644 --- a/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift +++ b/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift @@ -74,7 +74,8 @@ public final class ExtensionsManager { loadedLanguageServers.filter { elem in return elem.key.workspace == url - }.forEach { (key: PluginWorkspaceKey, _) in + }.forEach { (key: PluginWorkspaceKey, client: LSPClient) in + client.close() loadedLanguageServers.removeValue(forKey: key) } } @@ -118,7 +119,9 @@ public final class ExtensionsManager { return nil } - return LSPClient(lspURL, workspace: workspaceURL) + return try LSPClient(lspURL, + workspace: workspaceURL, + arguments: loadedBundles[id]?.infoDictionary?["CELSPArguments"] as? [String]) } guard let bundle = try loadBundle(id: id, withExtension: "celsp") else { @@ -133,7 +136,9 @@ public final class ExtensionsManager { return nil } - return LSPClient(lspURL, workspace: workspaceURL) + return try LSPClient(lspURL, + workspace: workspaceURL, + arguments: loadedBundles[id]?.infoDictionary?["CELSPArguments"] as? [String]) } /// Preloads all extensions' bundles to `loadedBundles` @@ -269,8 +274,9 @@ public final class ExtensionsManager { loadedLanguageServers.filter { elem in return elem.key.releaseID == entry.release - }.forEach { (key: PluginWorkspaceKey, _) in - loadedPlugins.removeValue(forKey: key) + }.forEach { (key: PluginWorkspaceKey, client: LSPClient) in + client.close() + loadedLanguageServers.removeValue(forKey: key) } } diff --git a/CodeEditModules/Modules/LSP/src/LSPClient.swift b/CodeEditModules/Modules/LSP/src/LSPClient.swift index f8205d0f5..b4ec8c4c8 100644 --- a/CodeEditModules/Modules/LSP/src/LSPClient.swift +++ b/CodeEditModules/Modules/LSP/src/LSPClient.swift @@ -10,9 +10,15 @@ import Foundation public class LSPClient { var exec: URL var workspace: URL + var process: Process - public init(_ exec: URL, workspace: URL) { + public init(_ exec: URL, workspace: URL, arguments: [String]?) throws { self.exec = exec self.workspace = workspace + self.process = try Process.run(exec, arguments: arguments ?? ["--stdio"], terminationHandler: nil) + } + + public func close() { + self.process.terminate() } } From 2d44f050802ae0058b149cac764295ba3a7a6352 Mon Sep 17 00:00:00 2001 From: Pavel Kasila Date: Sat, 16 Apr 2022 14:24:59 +0300 Subject: [PATCH 3/5] Add docs to `LSPClient` --- CodeEdit/Info.plist | 2 +- CodeEditModules/Modules/LSP/src/LSPClient.swift | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist index f65d3b4cb..35023fedb 100644 --- a/CodeEdit/Info.plist +++ b/CodeEdit/Info.plist @@ -45,6 +45,6 @@ GitHash - bfe2e04d16ea76737fdaf2a6167e0952d9067a87 + cba30ade536c314a5b8ef6450831e3778330fa68 diff --git a/CodeEditModules/Modules/LSP/src/LSPClient.swift b/CodeEditModules/Modules/LSP/src/LSPClient.swift index b4ec8c4c8..aa59cf7ad 100644 --- a/CodeEditModules/Modules/LSP/src/LSPClient.swift +++ b/CodeEditModules/Modules/LSP/src/LSPClient.swift @@ -7,17 +7,24 @@ import Foundation +/// A LSP client to handle Language Server process public class LSPClient { var exec: URL var workspace: URL var process: Process - public init(_ exec: URL, workspace: URL, arguments: [String]?) throws { - self.exec = exec + /// 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.exec = executable self.workspace = workspace - self.process = try Process.run(exec, arguments: arguments ?? ["--stdio"], terminationHandler: nil) + self.process = try Process.run(executable, arguments: arguments ?? ["--stdio"], terminationHandler: nil) } + /// Close the process public func close() { self.process.terminate() } From df5126734cd5f8c976187eba5d43204046775769 Mon Sep 17 00:00:00 2001 From: Pavel Kasila Date: Sat, 16 Apr 2022 14:26:08 +0300 Subject: [PATCH 4/5] use full `executable` instead of `exec` --- CodeEdit/Info.plist | 2 +- CodeEditModules/Modules/LSP/src/LSPClient.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist index 35023fedb..5bf85dff9 100644 --- a/CodeEdit/Info.plist +++ b/CodeEdit/Info.plist @@ -45,6 +45,6 @@ GitHash - cba30ade536c314a5b8ef6450831e3778330fa68 + 2d44f050802ae0058b149cac764295ba3a7a6352 diff --git a/CodeEditModules/Modules/LSP/src/LSPClient.swift b/CodeEditModules/Modules/LSP/src/LSPClient.swift index aa59cf7ad..df6a5936b 100644 --- a/CodeEditModules/Modules/LSP/src/LSPClient.swift +++ b/CodeEditModules/Modules/LSP/src/LSPClient.swift @@ -9,7 +9,7 @@ import Foundation /// A LSP client to handle Language Server process public class LSPClient { - var exec: URL + var executable: URL var workspace: URL var process: Process @@ -19,7 +19,7 @@ public class LSPClient { /// - 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.exec = executable + self.executable = executable self.workspace = workspace self.process = try Process.run(executable, arguments: arguments ?? ["--stdio"], terminationHandler: nil) } From 8d178ef7f6fba4502a81792977311b3be7f710b6 Mon Sep 17 00:00:00 2001 From: Pavel Kasila Date: Sat, 16 Apr 2022 14:44:30 +0300 Subject: [PATCH 5/5] Set Language Server's executable permissions to `-r-xr-xr-x` (555) --- CodeEdit/Info.plist | 2 +- .../Modules/ExtensionsStore/src/ExtensionsManager.swift | 1 + CodeEditModules/Modules/LSP/src/LSPClient.swift | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist index 5bf85dff9..793c19bae 100644 --- a/CodeEdit/Info.plist +++ b/CodeEdit/Info.plist @@ -45,6 +45,6 @@ GitHash - 2d44f050802ae0058b149cac764295ba3a7a6352 + df5126734cd5f8c976187eba5d43204046775769 diff --git a/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift b/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift index 9f2d5a317..4573bc854 100644 --- a/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift +++ b/CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift @@ -212,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) { diff --git a/CodeEditModules/Modules/LSP/src/LSPClient.swift b/CodeEditModules/Modules/LSP/src/LSPClient.swift index df6a5936b..63dac3da6 100644 --- a/CodeEditModules/Modules/LSP/src/LSPClient.swift +++ b/CodeEditModules/Modules/LSP/src/LSPClient.swift @@ -20,6 +20,7 @@ public class LSPClient { /// - 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) }