Skip to content

Commit 160a879

Browse files
Update WatchedFileChangeHandler.swift to listen for created and deleted changes (#38)
* Update WatchedFileChangeHandler.swift to listen for created and deleted changes * Add 'WatchedFileChangeHandlerTests.swift'
1 parent b9d1379 commit 160a879

File tree

9 files changed

+585
-25
lines changed

9 files changed

+585
-25
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/// Protocol defining the minimal interface needed for a target store
24+
protocol BazelTargetStoreProtocol {
25+
/// Clear any cached target information
26+
func clearCache()
27+
28+
/// Fetch and update target information from Bazel
29+
func fetchTargets() throws -> [BuildTarget]
30+
31+
/// Get the BSP URIs of targets that contain a given source file
32+
func bspURIs(containingSrc src: URI) throws -> [URI]
33+
}

Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ enum BazelTargetStoreError: Error, LocalizedError {
3434

3535
/// Abstraction that can queries, processes, and stores the project's dependency graph and its files.
3636
/// Used by many of the requests to calculate and provide data about the project's targets.
37-
final class BazelTargetStore {
38-
37+
final class BazelTargetStore: BazelTargetStoreProtocol {
3938
// The list of rules we currently care about and can process
4039
static let supportedRuleTypes: Set<String> = ["source file", "swift_library", "objc_library"]
4140

@@ -75,6 +74,7 @@ final class BazelTargetStore {
7574
return bspURIs
7675
}
7776

77+
@discardableResult
7878
func fetchTargets() throws -> [BuildTarget] {
7979
var targetData: [(BuildTarget, [URI])] = []
8080
let targets: [BlazeQuery_Target] = try bazelTargetQuerier.queryTargets(

Sources/SourceKitBazelBSP/RequestHandlers/InitializeHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ final class InitializeHandler {
142142
let rootUri = initializedConfig.rootUri
143143
if let filesToWatch = initializedConfig.baseConfig.filesToWatch {
144144
watchers = filesToWatch.components(separatedBy: ",").map {
145-
FileSystemWatcher(globPattern: rootUri + "/" + $0)
145+
FileSystemWatcher(globPattern: rootUri + "/" + $0, kind: [.change, .create, .delete])
146146
}
147147
} else {
148148
watchers = nil
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 BuildServerProtocol
21+
import LanguageServerProtocol
22+
23+
/// Represents a target that was affected by a file change
24+
struct AffectedTarget: Hashable {
25+
let uri: URI // Build target URI, not document URI
26+
let kind: FileChangeType
27+
}
28+
29+
/// Protocol for objects that need to be notified when build targets are invalidated
30+
protocol InvalidatedTargetObserver: AnyObject {
31+
func invalidate(targets: Set<AffectedTarget>) throws
32+
}

Sources/SourceKitBazelBSP/RequestHandlers/PrepareHandler.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ final class PrepareHandler {
8787
}
8888

8989
extension PrepareHandler: InvalidatedTargetObserver {
90-
func invalidate(targets: Set<URI>) throws {
91-
buildCache.subtract(targets)
90+
func invalidate(targets: Set<AffectedTarget>) throws {
91+
// Extract just the URIs from the affected targets for the build cache
92+
let targetURIs = Set(targets.map(\.uri))
93+
buildCache.subtract(targetURIs)
9294
}
9395

9496
func invalidateBuildCache() {

Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/SKOptionsHandler.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ private let logger = makeFileLevelBSPLogger()
2626
/// Handles the `textDocument/sourceKitOptions` request.
2727
///
2828
/// Returns the compiler arguments for the provided target based on previously gathered information.
29-
final class SKOptionsHandler {
29+
final class SKOptionsHandler: InvalidatedTargetObserver {
3030

3131
private let initializedConfig: InitializedServerConfig
3232
private let targetStore: BazelTargetStore
@@ -86,4 +86,13 @@ final class SKOptionsHandler {
8686
workingDirectory: initializedConfig.rootUri
8787
)
8888
}
89+
90+
// MARK: - InvalidatedTargetObserver
91+
92+
func invalidate(targets: Set<AffectedTarget>) throws {
93+
// Only clear cache if at least one file was created or deleted
94+
if targets.contains(where: { $0.kind == .created || $0.kind == .deleted }) {
95+
extractor.clearCache()
96+
}
97+
}
8998
}

Sources/SourceKitBazelBSP/RequestHandlers/WatchedFileChangeHandler.swift

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,45 +21,119 @@ import BuildServerProtocol
2121
import Foundation
2222
import LanguageServerProtocol
2323

24-
protocol InvalidatedTargetObserver: AnyObject {
25-
func invalidate(targets: Set<URI>) throws
26-
}
24+
private let logger = makeFileLevelBSPLogger()
2725

2826
/// Handles the file changing notification.
2927
///
3028
/// This is intended to tell the LSP which targets are invalidated by a change.
3129
final class WatchedFileChangeHandler {
32-
33-
private let targetStore: BazelTargetStore
30+
private let targetStore: BazelTargetStoreProtocol
3431
private var observers: [any InvalidatedTargetObserver]
3532
private weak var connection: LSPConnection?
3633

37-
init(targetStore: BazelTargetStore, observers: [any InvalidatedTargetObserver] = [], connection: LSPConnection) {
34+
init(
35+
targetStore: BazelTargetStoreProtocol,
36+
observers: [any InvalidatedTargetObserver] = [],
37+
connection: LSPConnection
38+
) {
3839
self.targetStore = targetStore
3940
self.observers = observers
4041
self.connection = connection
4142
}
4243

4344
func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws {
44-
// FIXME: This only deals with changes, not deletions or creations
45-
// For those, we need to invalidate the compilation options cache too
46-
// and probably also re-compile the app
47-
let changes = notification.changes.filter { $0.type == .changed }.map { $0.uri }
48-
var affectedTargets: Set<URI> = []
49-
for change in changes {
50-
let targetsForSrc = try targetStore.bspURIs(containingSrc: change)
51-
for target in targetsForSrc {
52-
affectedTargets.insert(target)
45+
// First, calculate deleted targets before we clear them from the targetStore
46+
let deletedTargets = {
47+
do {
48+
return try notification.changes
49+
.filter { $0.type == .deleted }
50+
.flatMap { change -> [AffectedTarget] in
51+
try targetStore.bspURIs(containingSrc: change.uri)
52+
.map { AffectedTarget(uri: $0, kind: change.type) }
53+
}
54+
} catch {
55+
logger.error("Error calculating deleted targets: \(error)")
56+
return []
57+
}
58+
}()
59+
60+
// If there are any 'created' files, we need to clear the targetStore and fetch targets again
61+
// Otherwise, the targetStore won't know about them
62+
if notification.changes.contains(where: { $0.type == .created }) {
63+
targetStore.clearCache()
64+
do {
65+
_ = try targetStore.fetchTargets()
66+
} catch {
67+
logger.error("Error fetching targets after file creation: \(error)")
68+
// Continue processing with existing target store data
5369
}
5470
}
71+
72+
// Now that the targetStore knows about the newly created files, we can calculate the created targets
73+
let createdTargets = {
74+
do {
75+
return try notification.changes
76+
.filter { $0.type == .created }
77+
.flatMap { change -> [AffectedTarget] in
78+
try targetStore.bspURIs(containingSrc: change.uri)
79+
.map { AffectedTarget(uri: $0, kind: change.type) }
80+
}
81+
} catch {
82+
logger.error("Error calculating created targets: \(error)")
83+
return []
84+
}
85+
}()
86+
87+
// Finally, calculate the changed targets
88+
let changedTargets = {
89+
do {
90+
return try notification.changes
91+
.filter { $0.type == .changed }
92+
.flatMap { change -> [AffectedTarget] in
93+
try targetStore.bspURIs(containingSrc: change.uri)
94+
.map { AffectedTarget(uri: $0, kind: change.type) }
95+
}
96+
} catch {
97+
logger.error("Error calculating changed targets: \(error)")
98+
return []
99+
}
100+
}()
101+
102+
let affectedTargets: Set<AffectedTarget> = Set(deletedTargets + createdTargets + changedTargets)
103+
104+
// Invalidate our observers about the affected targets
55105
for observer in observers {
56-
try observer.invalidate(targets: affectedTargets)
106+
do {
107+
try observer.invalidate(targets: affectedTargets)
108+
} catch {
109+
logger.error("Error invalidating observer: \(error)")
110+
// Continue with other observers
111+
}
57112
}
113+
114+
// Notify SK-LSP about the affected targets
58115
let response = OnBuildTargetDidChangeNotification(
59-
changes: affectedTargets.map {
60-
BuildTargetEvent(target: BuildTargetIdentifier(uri: $0), kind: .changed, dataKind: nil, data: nil)
116+
changes: affectedTargets.map { target in
117+
BuildTargetEvent(
118+
target: BuildTargetIdentifier(uri: target.uri),
119+
kind: target.kind.buildTargetEventKind,
120+
dataKind: nil,
121+
data: nil
122+
)
61123
}
62124
)
125+
63126
connection?.send(response)
64127
}
65128
}
129+
130+
extension FileChangeType {
131+
fileprivate var buildTargetEventKind: BuildTargetEventKind? {
132+
switch self {
133+
case .changed: return .changed
134+
case .created: return .created
135+
case .deleted: return .deleted
136+
default: return nil
137+
}
138+
}
139+
}

Sources/SourceKitBazelBSP/Server/SourceKitBazelBSPServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ package final class SourceKitBazelBSPServer {
9494
// OnWatchedFilesDidChangeNotification
9595
let watchedFileChangeHandler = WatchedFileChangeHandler(
9696
targetStore: targetStore,
97-
observers: [prepareHandler],
97+
observers: [prepareHandler, skOptionsHandler],
9898
connection: connection
9999
)
100100
registry.register(notificationHandler: watchedFileChangeHandler.onWatchedFilesDidChange)

0 commit comments

Comments
 (0)