Skip to content

Commit 8c8b68c

Browse files
authored
Clean-up the BSPServer entrypoint code (#11)
1 parent 3303fb5 commit 8c8b68c

File tree

8 files changed

+257
-20
lines changed

8 files changed

+257
-20
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 Foundation
21+
import LanguageServerProtocol
22+
import LanguageServerProtocolJSONRPC
23+
24+
/// Extends the original sourcekit-lsp `Connection` type to include JSONRPCConnection's start method.
25+
package protocol LSPConnection: Connection {
26+
func start(
27+
receiveHandler: MessageHandler,
28+
closeHandler: @escaping @Sendable () async -> Void
29+
)
30+
}
31+
32+
extension JSONRPCConnection: LSPConnection {}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 Foundation
21+
import OSLog
22+
23+
/// Simple helper to create loggers under the `sourcekit-bazel-bsp` subsystem.
24+
package func makeBSPLogger(withCategory category: String) -> Logger {
25+
Logger(subsystem: "sourcekit-bazel-bsp", category: category)
26+
}

Sources/SourceKitBazelBSP/BSPServer.swift renamed to Sources/SourceKitBazelBSP/SourceKitBazelBSPServer.swift

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,44 @@
1919

2020
import BuildServerProtocol
2121
import Foundation
22+
import LanguageServerProtocol
2223
import LanguageServerProtocolJSONRPC
23-
import OSLog
2424

25-
let logger = Logger(subsystem: "sourcekit-bazel-bsp", category: "bsp-server")
25+
let logger = makeBSPLogger(withCategory: "bsp-server")
2626

27-
package final class BSPServer {
27+
package final class SourceKitBazelBSPServer {
2828

29-
let baseConfig: BaseServerConfig
30-
let connection: JSONRPCConnection
29+
let connection: LSPConnection
30+
let handler: MessageHandler
3131

32-
package init(baseConfig: BaseServerConfig) {
33-
self.baseConfig = baseConfig
34-
self.connection = JSONRPCConnection(
32+
package convenience init(
33+
baseConfig: BaseServerConfig,
34+
inputHandle: FileHandle = .standardInput,
35+
outputHandle: FileHandle = .standardOutput
36+
) {
37+
let connection = JSONRPCConnection(
3538
name: "sourcekit-lsp",
36-
protocol: bspRegistry,
37-
inFD: FileHandle.standardInput,
38-
outFD: FileHandle.standardOutput
39+
protocol: BuildServerProtocol.bspRegistry,
40+
inFD: inputHandle,
41+
outFD: outputHandle
3942
)
43+
let handler = BSPServerMessageHandlerImpl(baseConfig: baseConfig, connection: connection)
44+
self.init(connection: connection, handler: handler)
4045
}
4146

42-
package func run() throws {
47+
package init(
48+
connection: LSPConnection,
49+
handler: MessageHandler
50+
) {
51+
self.connection = connection
52+
self.handler = handler
53+
}
54+
55+
package func run(parkThread: Bool = true) throws {
4356
logger.info("Connecting to sourcekit-lsp...")
4457

4558
connection.start(
46-
receiveHandler: BSPServerMessageHandlerImpl(
47-
baseConfig: baseConfig,
48-
connection: connection
49-
),
59+
receiveHandler: handler,
5060
closeHandler: {
5161
logger.info("Connection closed, exiting.")
5262
// Use _Exit to avoid running static destructors due to https://github.com/swiftlang/swift/issues/55112.
@@ -55,9 +65,14 @@ package final class BSPServer {
5565
}
5666
)
5767

58-
logger.info("Connection established, parking main thread.")
68+
// For usage with unit tests, since we don't want to block the thread when using mocks
69+
guard parkThread else {
70+
return
71+
}
72+
73+
logger.info("Connection established, parking thread.")
5974

60-
// Park the main function by sleeping for 10 years.
75+
// Park the thread by sleeping for 10 years.
6176
// All request handling is done on other threads and sourcekit-bazel-bsp exits by calling `_Exit` when it receives a
6277
// shutdown notification.
6378
// (Copied from sourcekit-lsp)

Sources/sourcekit-bazel-bsp/Commands/Serve/Serve.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ struct Serve: ParsableCommand {
2626
@Option(help: "The name of the Bazel CLI to invoke (e.g. 'bazelisk')")
2727
var bazelWrapper: String = "bazel"
2828

29+
// FIXME: We should support any library target, not just the app ones.
30+
// The problem is that ios_application targets apply transitions that don't get reflected
31+
// when building libraries individually. Queries have a --universe_scope flag to account for this,
32+
// but this is not available for build actions at the moment. We need to find a stable way of building
33+
// libraries with the same configs that would be applied when building the full app.
2934
@Option(
3035
parsing: .singleValue,
31-
help: "The Bazel ios_application or test targets that this should serve a BSP for.")
36+
help:
37+
"The Bazel ios_application or test targets that this should serve a BSP for. Can be specified multiple times."
38+
)
3239
var target: [String]
3340

3441
@Option(
@@ -53,7 +60,7 @@ struct Serve: ParsableCommand {
5360
indexFlags: indexFlag.map { "--" + $0 },
5461
filesToWatch: filesToWatch
5562
)
56-
let server = BSPServer(baseConfig: config)
63+
let server = SourceKitBazelBSPServer(baseConfig: config)
5764
try server.run()
5865
}
5966
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 LanguageServerProtocol
22+
import LanguageServerProtocolJSONRPC
23+
24+
@testable import SourceKitBazelBSP
25+
26+
final class LSPConnectionFake: LSPConnection {
27+
28+
nonisolated(unsafe) private(set) var startCalled = false
29+
nonisolated(unsafe) private(set) var startReceivedHandler: MessageHandler?
30+
31+
func start(
32+
receiveHandler: MessageHandler,
33+
closeHandler: @escaping @Sendable () async -> Void
34+
) {
35+
startCalled = true
36+
startReceivedHandler = receiveHandler
37+
}
38+
39+
func nextRequestID() -> LanguageServerProtocol.RequestID {
40+
unimplemented()
41+
}
42+
43+
func send(_ notification: some NotificationType) {
44+
unimplemented()
45+
}
46+
47+
func send<Request>(
48+
_ request: Request, id: LanguageServerProtocol.RequestID,
49+
reply: @escaping @Sendable (LanguageServerProtocol.LSPResult<Request.Response>) -> Void
50+
) where Request: LanguageServerProtocol.RequestType {
51+
unimplemented()
52+
}
53+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 LanguageServerProtocol
22+
23+
@testable import SourceKitBazelBSP
24+
25+
final class MessageHandlerFake: MessageHandler {
26+
func handle(_ notification: some NotificationType) {
27+
preconditionFailure("Not implemented")
28+
}
29+
30+
func handle<Request: RequestType>(
31+
_ request: Request,
32+
id: RequestID,
33+
reply: @escaping (LSPResult<Request.Response>) -> Void
34+
) {
35+
preconditionFailure("Not implemented")
36+
}
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 Foundation
21+
22+
func unimplemented() -> Never {
23+
preconditionFailure("Not implemented")
24+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 LanguageServerProtocol
22+
import LanguageServerProtocolJSONRPC
23+
import Testing
24+
25+
@testable import SourceKitBazelBSP
26+
27+
@Suite struct SourceKitBazelBSPServerTests {
28+
@Test
29+
func runAttachesHandler() throws {
30+
let mockConnection = LSPConnectionFake()
31+
let mockHandler = MessageHandlerFake()
32+
33+
let server = SourceKitBazelBSPServer(
34+
connection: mockConnection,
35+
handler: mockHandler
36+
)
37+
38+
try server.run(parkThread: false)
39+
40+
#expect(mockConnection.startCalled == true)
41+
#expect(mockConnection.startReceivedHandler === mockHandler)
42+
}
43+
}

0 commit comments

Comments
 (0)