Skip to content

Commit 8b7c398

Browse files
authored
Merge pull request swiftlang#157 from rmaz/filewatching
Add support for file watching in the build server protocol
2 parents 4f3a5c9 + d9b57b5 commit 8b7c398

File tree

7 files changed

+209
-5
lines changed

7 files changed

+209
-5
lines changed

Sources/BuildServerProtocol/Messages.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import LanguageServerProtocol
1313

1414
fileprivate let requestTypes: [_RequestType.Type] = [
1515
InitializeBuild.self,
16+
RegisterForChanges.self,
1617
ShutdownBuild.self,
1718
SourceKitOptions.self,
1819
]
1920

2021
fileprivate let notificationTypes: [NotificationType.Type] = [
2122
ExitBuildNotification.self,
23+
FileOptionsChangedNotification.self,
2224
InitializedBuildNotification.self,
2325
]
2426

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
import LanguageServerProtocol
13+
14+
15+
/// The register for changes request is sent from the language
16+
/// server to the build server to register or unregister for
17+
/// changes in file options or dependencies. On changes a
18+
/// FileOptionsChangedNotification is sent.
19+
public struct RegisterForChanges: RequestType {
20+
public static let method: String = "textDocument/registerForChanges"
21+
public typealias Response = VoidResponse
22+
23+
/// The URL of the document to get options for.
24+
public var uri: URL
25+
26+
/// Whether to register or unregister for the file.
27+
public var action: RegisterAction
28+
29+
public init(uri: URL, action: RegisterAction) {
30+
self.uri = uri
31+
self.action = action
32+
}
33+
}
34+
35+
public enum RegisterAction: String, Hashable, Codable {
36+
case register = "register"
37+
case unregister = "unregister"
38+
}
39+
40+
/// The FileOptionsChangedNotification is sent from the
41+
/// build server to the language server when it detects
42+
/// changes to a registered files build settings.
43+
public struct FileOptionsChangedNotification: NotificationType {
44+
public static let method: String = "build/sourceKitOptionsChanged"
45+
46+
/// The URL of the document that has changed settings.
47+
public var uri: URL
48+
49+
/// The updated options for the registered file.
50+
public var updatedOptions: SourceKitOptionsResult
51+
}

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import SKSupport
1616
import Foundation
1717
import BuildServerProtocol
1818

19+
typealias Notification = LanguageServerProtocol.Notification
20+
1921
/// A `BuildSystem` based on communicating with a build server
2022
///
2123
/// Provides build settings from a build server launched based on a
@@ -25,19 +27,24 @@ public final class BuildServerBuildSystem {
2527
let projectRoot: AbsolutePath
2628
let buildFolder: AbsolutePath?
2729
let serverConfig: BuildServerConfig
30+
let requestQueue: DispatchQueue
2831

2932
var handler: BuildServerHandler?
3033
var buildServer: Connection?
3134
public private(set) var indexStorePath: AbsolutePath?
3235

3336
/// Delegate to handle any build system events.
34-
public weak var delegate: BuildSystemDelegate? = nil
37+
public weak var delegate: BuildSystemDelegate? {
38+
get { return self.handler?.delegate }
39+
set { self.handler?.delegate = newValue }
40+
}
3541

3642
public init(projectRoot: AbsolutePath, buildFolder: AbsolutePath?, fileSystem: FileSystem = localFileSystem) throws {
3743
let configPath = projectRoot.appending(component: "buildServer.json")
3844
let config = try loadBuildServerConfig(path: configPath, fileSystem: fileSystem)
3945
self.buildFolder = buildFolder
4046
self.projectRoot = projectRoot
47+
self.requestQueue = DispatchQueue(label: "build_server_request_queue")
4148
self.serverConfig = config
4249
try self.initializeBuildServer()
4350
}
@@ -114,24 +121,43 @@ private func readReponseDataKey(data: LSPAny?, key: String) -> String? {
114121
}
115122

116123
final class BuildServerHandler: LanguageServerEndpoint {
117-
override func _registerBuiltinHandlers() { }
124+
125+
public weak var delegate: BuildSystemDelegate? = nil
126+
127+
override func _registerBuiltinHandlers() {
128+
_register(BuildServerHandler.handleFileOptionsChanged)
129+
}
130+
131+
func handleFileOptionsChanged(_ notification: Notification<FileOptionsChangedNotification>) {
132+
// TODO: add delegate method to include the changed settings directly
133+
self.delegate?.fileBuildSettingsChanged([notification.params.uri])
134+
}
118135
}
119136

120137
extension BuildServerBuildSystem: BuildSystem {
121138

122139
/// Register the given file for build-system level change notifications, such as command
123140
/// line flag changes, dependency changes, etc.
124141
public func registerForChangeNotifications(for url: LanguageServerProtocol.URL) {
125-
// TODO: Implement via BSP extensions.
142+
let request = RegisterForChanges(uri: url, action: .register)
143+
_ = self.buildServer?.send(request, queue: requestQueue, reply: { result in
144+
if let error = result.failure {
145+
log("error registering \(url): \(error)", level: .error)
146+
}
147+
})
126148
}
127149

128150
/// Unregister the given file for build-system level change notifications, such as command
129151
/// line flag changes, dependency changes, etc.
130152
public func unregisterForChangeNotifications(for url: LanguageServerProtocol.URL) {
131-
// TODO: Implement via BSP extensions.
153+
let request = RegisterForChanges(uri: url, action: .unregister)
154+
_ = self.buildServer?.send(request, queue: requestQueue, reply: { result in
155+
if let error = result.failure {
156+
log("error unregistering \(url): \(error)", level: .error)
157+
}
158+
})
132159
}
133160

134-
135161
public var indexDatabasePath: AbsolutePath? {
136162
return buildFolder?.appending(components: "index", "db")
137163
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "client name",
3+
"version": "10",
4+
"bspVersion": "2.0",
5+
"languages": ["a", "b"],
6+
"argv": ["server.py"]
7+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python
2+
3+
import json
4+
import os
5+
import sys
6+
7+
8+
def send(data):
9+
dataStr = json.dumps(data)
10+
sys.stdout.write("Content-Length: {}\r\n\r\n{}".format(len(dataStr), dataStr))
11+
sys.stdout.flush()
12+
13+
14+
while True:
15+
line = sys.stdin.readline()
16+
if len(line) == 0:
17+
break
18+
19+
assert line.startswith('Content-Length:')
20+
length = int(line[len('Content-Length:'):])
21+
sys.stdin.readline()
22+
message = json.loads(sys.stdin.read(length))
23+
24+
response = None
25+
notification = None
26+
27+
if message["method"] == "build/initialize":
28+
response = {
29+
"jsonrpc": "2.0",
30+
"id": message["id"],
31+
"result": {
32+
"displayName": "test server",
33+
"version": "0.1",
34+
"bspVersion": "2.0",
35+
"rootUri": "blah",
36+
"capabilities": {"languageIds": ["a", "b"]},
37+
"data": {
38+
"indexStorePath": "some/index/store/path"
39+
}
40+
}
41+
}
42+
elif message["method"] == "build/initialized":
43+
continue
44+
elif message["method"] == "build/shutdown":
45+
response = {
46+
"jsonrpc": "2.0",
47+
"id": message["id"],
48+
"result": None
49+
}
50+
elif message["method"] == "build/exit":
51+
break
52+
elif message["method"] == "textDocument/registerForChanges":
53+
response = {
54+
"jsonrpc": "2.0",
55+
"id": message["id"],
56+
"result": None
57+
}
58+
if message["params"]["action"] == "register":
59+
notification = {
60+
"jsonrpc": "2.0",
61+
"method": "build/sourceKitOptionsChanged",
62+
"params": {
63+
"uri": message["params"]["uri"],
64+
"updatedOptions": {
65+
"options": ["a", "b"],
66+
"workingDirectory": "/some/dir"
67+
}
68+
}
69+
}
70+
71+
# ignore other notifications
72+
elif "id" in message:
73+
response = {
74+
"jsonrpc": "2.0",
75+
"id": message["id"],
76+
"error": {
77+
"code": -32600,
78+
"message": "unhandled method {}".format(message["method"]),
79+
}
80+
}
81+
82+
if response: send(response)
83+
if notification: send(notification)

Tests/SKCoreTests/BuildServerBuildSystemTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,38 @@ final class BuildServerBuildSystemTests: XCTestCase {
4646
XCTAssertNil(buildSystem?.settings(for: missingFileURL, Language.swift))
4747
}
4848

49+
func testFileRegistration() {
50+
let root = AbsolutePath(
51+
inputsDirectory().appendingPathComponent(testDirectoryName, isDirectory: true).path)
52+
let buildFolder = AbsolutePath(NSTemporaryDirectory())
53+
let buildSystem = try? BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder)
54+
XCTAssertNotNil(buildSystem)
55+
56+
let fileUrl = URL(fileURLWithPath: "/some/file/path")
57+
let expectation = XCTestExpectation(description: "\(fileUrl) settings updated")
58+
let buildSystemDelegate = TestDelegate(expectations: [fileUrl: expectation])
59+
buildSystem?.delegate = buildSystemDelegate
60+
buildSystem?.registerForChangeNotifications(for: fileUrl)
61+
62+
let result = XCTWaiter.wait(for: [expectation], timeout: 1)
63+
if result != .completed {
64+
fatalError("error \(result) waiting for settings notification")
65+
}
66+
}
67+
68+
}
69+
70+
final class TestDelegate: BuildSystemDelegate {
71+
72+
let expectations: [URL:XCTestExpectation]
73+
74+
public init(expectations: [URL:XCTestExpectation]) {
75+
self.expectations = expectations
76+
}
77+
78+
func fileBuildSettingsChanged(_ changedFiles: Set<URL>) {
79+
for url in changedFiles {
80+
expectations[url]?.fulfill()
81+
}
82+
}
4983
}

Tests/SKCoreTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ extension BuildServerBuildSystemTests {
66
// `swift test --generate-linuxmain`
77
// to regenerate.
88
static let __allTests__BuildServerBuildSystemTests = [
9+
("testFileRegistration", testFileRegistration),
910
("testServerInitialize", testServerInitialize),
1011
("testSettings", testSettings),
1112
]

0 commit comments

Comments
 (0)