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" + ), ] )