Skip to content

Commit c3e3358

Browse files
authored
Merge pull request #465 from pkasila/lsp-extensions
Add ability to load Language Servers as extensions (bundles)
2 parents 2c0117e + 8d178ef commit c3e3358

File tree

6 files changed

+138
-23
lines changed

6 files changed

+138
-23
lines changed

CodeEdit/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@
4545
</dict>
4646
</array>
4747
<key>GitHash</key>
48-
<string>c7fee3a29a4477ce93e6e892af08e32078fa19ac</string>
48+
<string>df5126734cd5f8c976187eba5d43204046775769</string>
4949
</dict>
5050
</plist>

CodeEditModules/Modules/ExtensionsStore/src/ExtensionsManager.swift

Lines changed: 97 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Foundation
99
import Light_Swift_Untar
1010
import CodeEditKit
1111
import GRDB
12+
import LSP
1213

1314
/// Class which handles all extensions (its bundles, instances for each workspace and so on)
1415
public final class ExtensionsManager {
@@ -28,6 +29,7 @@ public final class ExtensionsManager {
2829

2930
var loadedBundles: [UUID: Bundle] = [:]
3031
var loadedPlugins: [PluginWorkspaceKey: ExtensionInterface] = [:]
32+
var loadedLanguageServers: [PluginWorkspaceKey: LSPClient] = [:]
3133

3234
init() throws {
3335
self.codeeditFolder = try FileManager.default
@@ -53,6 +55,11 @@ public final class ExtensionsManager {
5355
table.column("loadable", .boolean)
5456
}
5557
}
58+
migrator.registerMigration("v1.0.1") { database in
59+
try database.alter(table: DownloadedPlugin.databaseTableName) { body in
60+
body.add(column: "sdk", .text).defaults(to: "swift")
61+
}
62+
}
5663
try migrator.migrate(self.dbQueue)
5764
}
5865

@@ -64,58 +71,119 @@ public final class ExtensionsManager {
6471
}.forEach { (key: PluginWorkspaceKey, _) in
6572
loadedPlugins.removeValue(forKey: key)
6673
}
67-
}
6874

69-
private func getExtensionBuilder(id: UUID) throws -> ExtensionBuilder.Type? {
70-
if loadedBundles.keys.contains(id) {
71-
return loadedBundles[id]?.principalClass as? ExtensionBuilder.Type
75+
loadedLanguageServers.filter { elem in
76+
return elem.key.workspace == url
77+
}.forEach { (key: PluginWorkspaceKey, client: LSPClient) in
78+
client.close()
79+
loadedLanguageServers.removeValue(forKey: key)
7280
}
81+
}
7382

83+
private func loadBundle(id: UUID, withExtension ext: String) throws -> Bundle? {
7484
guard let bundleURL = try FileManager.default.contentsOfDirectory(
7585
at: extensionsFolder.appendingPathComponent(id.uuidString,
7686
isDirectory: true),
7787
includingPropertiesForKeys: nil,
7888
options: .skipsPackageDescendants
79-
).first else { return nil }
89+
).first(where: {$0.pathExtension == ext}) else { return nil }
8090

81-
guard bundleURL.pathExtension == "ceext" else { return nil }
8291
guard let bundle = Bundle(url: bundleURL) else { return nil }
8392

84-
guard bundle.load() else { return nil }
85-
8693
loadedBundles[id] = bundle
8794

95+
return bundle
96+
}
97+
98+
private func getExtensionBuilder(id: UUID) throws -> ExtensionBuilder.Type? {
99+
if loadedBundles.keys.contains(id) {
100+
return loadedBundles[id]?.principalClass as? ExtensionBuilder.Type
101+
}
102+
103+
guard let bundle = try loadBundle(id: id, withExtension: "ceext") else {
104+
return nil
105+
}
106+
107+
guard bundle.load() else { return nil }
108+
88109
return bundle.principalClass as? ExtensionBuilder.Type
89110
}
90111

112+
private func getLSPClient(id: UUID, workspaceURL: URL) throws -> LSPClient? {
113+
if loadedBundles.keys.contains(id) {
114+
guard let lspFile = loadedBundles[id]?.infoDictionary?["CELSPExecutable"] as? String else {
115+
return nil
116+
}
117+
118+
guard let lspURL = loadedBundles[id]?.url(forResource: lspFile, withExtension: nil) else {
119+
return nil
120+
}
121+
122+
return try LSPClient(lspURL,
123+
workspace: workspaceURL,
124+
arguments: loadedBundles[id]?.infoDictionary?["CELSPArguments"] as? [String])
125+
}
126+
127+
guard let bundle = try loadBundle(id: id, withExtension: "celsp") else {
128+
return nil
129+
}
130+
131+
guard let lspFile = bundle.infoDictionary?["CELSPExecutable"] as? String else {
132+
return nil
133+
}
134+
135+
guard let lspURL = bundle.url(forResource: lspFile, withExtension: nil) else {
136+
return nil
137+
}
138+
139+
return try LSPClient(lspURL,
140+
workspace: workspaceURL,
141+
arguments: loadedBundles[id]?.infoDictionary?["CELSPArguments"] as? [String])
142+
}
143+
91144
/// Preloads all extensions' bundles to `loadedBundles`
92145
public func preload() throws {
93146
let plugins = try self.dbQueue.read { database in
94147
try DownloadedPlugin.filter(Column("loadable") == true).fetchAll(database)
95148
}
96149

97150
try plugins.forEach { plugin in
98-
_ = try getExtensionBuilder(id: plugin.release)
151+
switch plugin.sdk {
152+
case .swift:
153+
_ = try loadBundle(id: plugin.release, withExtension: "ceext")
154+
case .languageServer:
155+
_ = try loadBundle(id: plugin.release, withExtension: "celsp")
156+
}
99157
}
100158
}
101159

102-
/// Loads extensions' bundles which were not loaded before and creates `ExtensionInterface` instances
103-
/// with `ExtensionAPI` created using specified initializer
104-
/// - Parameter apiInitializer: function which creates `ExtensionAPI` instance based on plugin's ID
105-
public func load(_ apiInitializer: (String) -> ExtensionAPI) throws {
160+
/// Loads extensions' bundles which were not loaded before and passes `ExtensionAPI` as a whole class
161+
/// or workspace's URL
162+
/// - Parameter apiBuilder: function which creates `ExtensionAPI` instance based on plugin's ID
163+
public func load(_ apiBuilder: (String) -> ExtensionAPI) throws {
106164
let plugins = try self.dbQueue.read { database in
107-
try DownloadedPlugin.filter(Column("loadable") == true).fetchAll(database)
165+
try DownloadedPlugin
166+
.filter(Column("loadable") == true)
167+
.fetchAll(database)
108168
}
109169

110170
try plugins.forEach { plugin in
111-
guard let builder = try getExtensionBuilder(id: plugin.release) else {
112-
return
113-
}
171+
let api = apiBuilder(plugin.plugin.uuidString)
172+
let key = PluginWorkspaceKey(releaseID: plugin.release, workspace: api.workspaceURL)
114173

115-
let api = apiInitializer(plugin.plugin.uuidString)
174+
switch plugin.sdk {
175+
case .swift:
176+
guard let builder = try getExtensionBuilder(id: plugin.release) else {
177+
return
178+
}
116179

117-
let key = PluginWorkspaceKey(releaseID: plugin.release, workspace: api.workspaceURL)
118-
loadedPlugins[key] = builder.init().build(withAPI: api)
180+
loadedPlugins[key] = builder.init().build(withAPI: api)
181+
case .languageServer:
182+
guard let client = try getLSPClient(id: plugin.release, workspaceURL: api.workspaceURL) else {
183+
return
184+
}
185+
loadedLanguageServers[key] = client
186+
}
119187
}
120188
}
121189

@@ -144,6 +212,7 @@ public final class ExtensionsManager {
144212
)
145213
.appendingPathComponent("\(release.id.uuidString).tar")
146214

215+
// TODO: show progress
147216
let (source, _) = try await URLSession.shared.download(from: tarball)
148217

149218
if FileManager.default.fileExists(atPath: cacheTar.path) {
@@ -168,7 +237,7 @@ public final class ExtensionsManager {
168237
// save to db
169238

170239
try await dbQueue.write { database in
171-
try DownloadedPlugin(plugin: plugin.id, release: release.id, loadable: true)
240+
try DownloadedPlugin(plugin: plugin.id, release: release.id, loadable: true, sdk: plugin.sdk)
172241
.insert(database)
173242
}
174243
}
@@ -203,6 +272,13 @@ public final class ExtensionsManager {
203272
}.forEach { (key: PluginWorkspaceKey, _) in
204273
loadedPlugins.removeValue(forKey: key)
205274
}
275+
276+
loadedLanguageServers.filter { elem in
277+
return elem.key.releaseID == entry.release
278+
}.forEach { (key: PluginWorkspaceKey, client: LSPClient) in
279+
client.close()
280+
loadedLanguageServers.removeValue(forKey: key)
281+
}
206282
}
207283

208284
/// Checks whether extension's bundle (plugin) is installed

CodeEditModules/Modules/ExtensionsStore/src/Models/DownloadedPlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ public struct DownloadedPlugin: Codable, FetchableRecord, PersistableRecord, Tab
1515
public var plugin: UUID
1616
public var release: UUID
1717
public var loadable: Bool
18+
public var sdk: Plugin.SDK
1819
}

CodeEditModules/Modules/ExtensionsStore/src/Models/Plugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public struct Plugin: Codable, Identifiable, Hashable {
1818

1919
public enum SDK: String, Codable, Hashable {
2020
case swift
21+
case languageServer = "language_server"
2122
}
2223

2324
public enum ReleaseManagement: String, Codable, Hashable {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// LSPClient.swift
3+
//
4+
//
5+
// Created by Pavel Kasila on 16.04.22.
6+
//
7+
8+
import Foundation
9+
10+
/// A LSP client to handle Language Server process
11+
public class LSPClient {
12+
var executable: URL
13+
var workspace: URL
14+
var process: Process
15+
16+
/// Initialize new LSP client
17+
/// - Parameters:
18+
/// - executable: Executable of the Language Server to be run
19+
/// - workspace: Workspace's URL
20+
/// - arguments: Additional arguments from `CELSPArguments` in `Info.plist` of the Language Server bundle
21+
public init(_ executable: URL, workspace: URL, arguments: [String]?) throws {
22+
self.executable = executable
23+
try FileManager.default.setAttributes([.posixPermissions: 0o555], ofItemAtPath: self.executable.path)
24+
self.workspace = workspace
25+
self.process = try Process.run(executable, arguments: arguments ?? ["--stdio"], terminationHandler: nil)
26+
}
27+
28+
/// Close the process
29+
public func close() {
30+
self.process.terminate()
31+
}
32+
}

CodeEditModules/Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ let package = Package(
247247
dependencies: [
248248
"CodeEditKit",
249249
"Light-Swift-Untar",
250-
.productItem(name: "GRDB", package: "GRDB.swift", condition: nil)
250+
.productItem(name: "GRDB", package: "GRDB.swift", condition: nil),
251+
"LSP"
251252
],
252253
path: "Modules/ExtensionsStore/src"
253254
),
@@ -268,5 +269,9 @@ let package = Package(
268269
],
269270
path: "Modules/Feedback/src"
270271
),
272+
.target(
273+
name: "LSP",
274+
path: "Modules/LSP/src"
275+
),
271276
]
272277
)

0 commit comments

Comments
 (0)