Skip to content

Commit 3ad08bf

Browse files
rockbrunojosh-arnold-1
authored andcommitted
Add refactored handlers for initialize and the shutdown requests (spotify#15)
1 parent 84add04 commit 3ad08bf

23 files changed

+623
-20
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright (c) 2025 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import BuildServerProtocol
21+
import Foundation
22+
import LanguageServerProtocol
23+
24+
package let sourcekitBazelBSPVersion = "0.0.1"
25+
26+
/// Handles the `initialize` request.
27+
///
28+
/// This is the first request that the LSP sends, and it contains the initial configuration
29+
/// for the server. We gather all information needed to operate the server, return it to the LSP,
30+
/// and then register the other handlers that will handle the rest of the requests.
31+
final class InitializeHandler {
32+
33+
private let baseConfig: BaseServerConfig
34+
private let commandRunner: CommandRunner
35+
36+
private weak var connection: LSPConnection?
37+
38+
init(
39+
baseConfig: BaseServerConfig,
40+
commandRunner: CommandRunner = ShellCommandRunner(),
41+
connection: LSPConnection? = nil,
42+
) {
43+
self.baseConfig = baseConfig
44+
self.commandRunner = commandRunner
45+
self.connection = connection
46+
}
47+
48+
func initializeBuild(
49+
_ request: InitializeBuildRequest,
50+
_ id: RequestID
51+
) throws -> InitializeBuildResponse {
52+
let taskId = TaskId(id: "initializeBuild-\(id.description)")
53+
connection?.startWorkTask(
54+
id: taskId,
55+
title: "Indexing: Initializing sourcekit-bazel-bsp"
56+
)
57+
do {
58+
let initializedConfig = try makeInitializedConfig(
59+
fromRequest: request,
60+
baseConfig: baseConfig
61+
)
62+
let result = buildResponse(
63+
fromRequest: request,
64+
and: initializedConfig
65+
)
66+
connection?.finishTask(id: taskId, status: .ok)
67+
return result
68+
} catch {
69+
connection?.finishTask(id: taskId, status: .error)
70+
throw error
71+
}
72+
}
73+
74+
func makeInitializedConfig(
75+
fromRequest request: InitializeBuildRequest,
76+
baseConfig: BaseServerConfig,
77+
) throws -> InitializedServerConfig {
78+
let rootUri = request.rootUri.arbitrarySchemeURL.path
79+
logger.debug("rootUri: \(rootUri, privacy: .public)")
80+
let regularOutputBase = URL(
81+
fileURLWithPath: try commandRunner.bazel(
82+
baseConfig: baseConfig,
83+
rootUri: rootUri,
84+
cmd: "info output_base"
85+
)
86+
)
87+
logger.debug("regularOutputBase: \(regularOutputBase, privacy: .public)")
88+
89+
// Setup the special output base path where we will run indexing commands from.
90+
let regularOutputBaseLastPath = regularOutputBase.lastPathComponent
91+
let outputBase =
92+
regularOutputBase
93+
.deletingLastPathComponent()
94+
.appendingPathComponent("\(regularOutputBaseLastPath)-sourcekit-bazel-bsp")
95+
.path
96+
logger.debug("outputBase: \(outputBase, privacy: .public)")
97+
98+
// Now, get the full output path based on the above output base.
99+
let outputPath = try commandRunner.bazelIndexAction(
100+
baseConfig: baseConfig,
101+
outputBase: outputBase,
102+
cmd: "info output_path",
103+
rootUri: rootUri
104+
)
105+
logger.debug("outputPath: \(outputPath, privacy: .public)")
106+
107+
// Collecting the rest of the env's details
108+
let devDir = try commandRunner.run("xcode-select --print-path")
109+
let sdkRoot = try commandRunner.run("xcrun --sdk iphonesimulator --show-sdk-path")
110+
111+
return InitializedServerConfig(
112+
baseConfig: baseConfig,
113+
rootUri: rootUri,
114+
outputBase: outputBase,
115+
outputPath: outputPath,
116+
devDir: devDir,
117+
sdkRoot: sdkRoot
118+
)
119+
}
120+
121+
func buildResponse(
122+
fromRequest request: InitializeBuildRequest,
123+
and initializedConfig: InitializedServerConfig,
124+
) -> InitializeBuildResponse {
125+
let capabilities = request.capabilities
126+
let watchers: [FileSystemWatcher]?
127+
let rootUri = initializedConfig.rootUri
128+
if let filesToWatch = initializedConfig.baseConfig.filesToWatch {
129+
watchers = filesToWatch.components(separatedBy: ",").map {
130+
FileSystemWatcher(globPattern: rootUri + "/" + $0)
131+
}
132+
} else {
133+
watchers = nil
134+
}
135+
return InitializeBuildResponse(
136+
displayName: "sourcekit-bazel-bsp",
137+
version: sourcekitBazelBSPVersion,
138+
bspVersion: "2.2.0",
139+
capabilities: BuildServerCapabilities(
140+
compileProvider: .init(languageIds: capabilities.languageIds),
141+
testProvider: .init(languageIds: capabilities.languageIds),
142+
runProvider: .init(languageIds: capabilities.languageIds),
143+
debugProvider: .init(languageIds: capabilities.languageIds),
144+
inverseSourcesProvider: true,
145+
dependencySourcesProvider: true,
146+
resourcesProvider: true,
147+
outputPathsProvider: false, // FIXME: Not sure if we need this or not
148+
buildTargetChangedProvider: true,
149+
canReload: true,
150+
),
151+
dataKind: InitializeBuildResponseDataKind.sourceKit,
152+
data: SourceKitInitializeBuildResponseData(
153+
indexDatabasePath: initializedConfig.indexDatabasePath,
154+
indexStorePath: initializedConfig.indexStorePath,
155+
outputPathsProvider: nil, // FIXME: Not sure if we need this or not
156+
prepareProvider: true,
157+
sourceKitOptionsProvider: true,
158+
watchers: watchers,
159+
).encodeToLSPAny()
160+
)
161+
}
162+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2025 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import BuildServerProtocol
21+
import Foundation
22+
import LanguageServerProtocol
23+
24+
/// Handles the `build/shutdown` and `onBuildExit` messages.
25+
///
26+
/// According to the BSP spec, the server should first receive the `build/shutdown` request,
27+
/// and then the `onBuildExit` notification. We then exit with different codes depending on
28+
/// whether the LSP followed the spec correctly or not.
29+
final class ShutdownHandler {
30+
31+
private var didAskToShutdown = false
32+
private var terminateHandler: ((Int32) -> Void)
33+
34+
init(terminateHandler: ((Int32) -> Void)? = nil) {
35+
self.terminateHandler =
36+
terminateHandler ?? { code in
37+
safeTerminate(code)
38+
}
39+
}
40+
41+
func buildShutdown(
42+
_ request: BuildShutdownRequest,
43+
_ id: RequestID
44+
) throws -> VoidResponse {
45+
didAskToShutdown = true
46+
return VoidResponse()
47+
}
48+
49+
func onBuildExit(_ notification: OnBuildExitNotification) throws {
50+
terminateHandler(didAskToShutdown ? 0 : 1)
51+
}
52+
}
53+
54+
func safeTerminate(_ code: Int32) {
55+
// Use _Exit to avoid running static destructors due to https://github.com/swiftlang/swift/issues/55112.
56+
// (Copied from sourcekit-lsp)
57+
_Exit(code)
58+
}

Sources/SourceKitBazelBSP/MessageHandler/RequestHandlers/BuildTargetSourcesHandler.swift renamed to Sources/SourceKitBazelBSP/RequestHandlers/_old/BuildTargetSourcesHandler.swift

File renamed without changes.

Sources/SourceKitBazelBSP/MessageHandler/RequestHandlers/CompilerArgumentsProcessor.swift renamed to Sources/SourceKitBazelBSP/RequestHandlers/_old/CompilerArgumentsProcessor.swift

File renamed without changes.

Sources/SourceKitBazelBSP/MessageHandler/RequestHandlers/InitializeRequestHandler.swift renamed to Sources/SourceKitBazelBSP/RequestHandlers/_old/InitializeRequestHandler.swift

File renamed without changes.

Sources/SourceKitBazelBSP/MessageHandler/RequestHandlers/PrepareTargetHandler.swift renamed to Sources/SourceKitBazelBSP/RequestHandlers/_old/PrepareTargetHandler.swift

File renamed without changes.

Sources/SourceKitBazelBSP/MessageHandler/RequestHandlers/TaskLogger.swift renamed to Sources/SourceKitBazelBSP/RequestHandlers/_old/TaskLogger.swift

File renamed without changes.

Sources/SourceKitBazelBSP/MessageHandler/RequestHandlers/TextDocumentSourceKitOptionsHandler.swift renamed to Sources/SourceKitBazelBSP/RequestHandlers/_old/TextDocumentSourceKitOptionsHandler.swift

File renamed without changes.

Sources/SourceKitBazelBSP/MessageHandler/RequestHandlers/WorkspaceBuildTargetsHandler.swift renamed to Sources/SourceKitBazelBSP/RequestHandlers/_old/WorkspaceBuildTargetsHandler.swift

File renamed without changes.

Sources/SourceKitBazelBSP/Server/BaseServerConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Foundation
2424
/// Used to bootstrap the connection to sourcekit-lsp, so that later we can create the
2525
/// more complete `InitializedServerConfig` struct containing all information
2626
/// needed to operate the server.
27-
package struct BaseServerConfig {
27+
package struct BaseServerConfig: Equatable {
2828
let bazelWrapper: String
2929
let targets: [String]
3030
let indexFlags: [String]

0 commit comments

Comments
 (0)