Skip to content

Commit 1d1f1fb

Browse files
authored
Merge pull request #1989 from ahoppen/didchangactivedocument
Add notification to inform SourceKit-LSP about the currently active document
2 parents ca80ff3 + 4d00c90 commit 1d1f1fb

File tree

10 files changed

+168
-33
lines changed

10 files changed

+168
-33
lines changed

Contributor Documentation/LSP Extensions.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,25 @@ export interface DocumentTestsParams {
434434
}
435435
```
436436
437+
## `window/didChangeActiveDocument`
438+
439+
New notification from the client to the server, telling SourceKit-LSP which document is the currently active primary document.
440+
441+
By default, SourceKit-LSP infers the currently active editor document from the last document that received a request.
442+
If the client supports active reporting of the currently active document, it should check for the
443+
`window/didChangeActiveDocument` experimental server capability. If that capability is present, it should respond with
444+
the `window/didChangeActiveDocument` experimental client capability and send this notification whenever the currently
445+
active document changes.
446+
447+
- params: `DidChangeActiveDocumentParams`
448+
449+
```ts
450+
export interface DidChangeActiveDocumentParams {
451+
/**
452+
* The document that is being displayed in the active editor.
453+
*/
454+
textDocument: TextDocumentIdentifier;
455+
437456
## `window/logMessage`
438457

439458
Added field:

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ add_library(LanguageServerProtocol STATIC
1111
Notifications/CancelRequestNotification.swift
1212
Notifications/CancelWorkDoneProgressNotification.swift
1313
Notifications/ConfigurationNotification.swift
14+
Notifications/DidChangeActiveDocumentNotification.swift
1415
Notifications/DidChangeFileNotifications.swift
1516
Notifications/DidChangeWatchedFilesNotification.swift
1617
Notifications/DidChangeWorkspaceFoldersNotification.swift

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public let builtinRequests: [_RequestType.Type] = [
9999
public let builtinNotifications: [NotificationType.Type] = [
100100
CancelRequestNotification.self,
101101
CancelWorkDoneProgressNotification.self,
102+
DidChangeActiveDocumentNotification.self,
102103
DidChangeConfigurationNotification.self,
103104
DidChangeNotebookDocumentNotification.self,
104105
DidChangeTextDocumentNotification.self,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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+
13+
public struct DidChangeActiveDocumentNotification: NotificationType {
14+
public static let method: String = "window/didChangeActiveDocument"
15+
16+
/// The document that is being displayed in the active editor.
17+
public var textDocument: TextDocumentIdentifier
18+
19+
public init(textDocument: TextDocumentIdentifier) {
20+
self.textDocument = textDocument
21+
}
22+
}

Sources/SourceKitLSP/CapabilityRegistry.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ package final actor CapabilityRegistry {
102102
registration(for: [language], in: pullDiagnostics) != nil
103103
}
104104

105+
package nonisolated var clientSupportsActiveDocumentNotification: Bool {
106+
return clientHasExperimentalCapability(DidChangeActiveDocumentNotification.method)
107+
}
108+
109+
package nonisolated func clientHasExperimentalCapability(_ name: String) -> Bool {
110+
guard case .dictionary(let experimentalCapabilities) = clientCapabilities.experimental else {
111+
return false
112+
}
113+
return experimentalCapabilities[name] == .bool(true)
114+
}
115+
105116
// MARK: Initializer
106117

107118
package init(clientCapabilities: ClientCapabilities) {

Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc
100100
self = .freestanding
101101
case is CancelWorkDoneProgressNotification:
102102
self = .freestanding
103+
case is DidChangeActiveDocumentNotification:
104+
// The notification doesn't change behavior in an observable way, so we can treat it as freestanding.
105+
self = .freestanding
103106
case is DidChangeConfigurationNotification:
104107
self = .globalConfigurationChange
105108
case let notification as DidChangeNotebookDocumentNotification:

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
666666
logger.log("Received notification: \(notification.forLogging)")
667667

668668
switch notification {
669+
case let notification as DidChangeActiveDocumentNotification:
670+
await self.didChangeActiveDocument(notification)
669671
case let notification as DidChangeTextDocumentNotification:
670672
await self.changeDocument(notification)
671673
case let notification as DidChangeWorkspaceFoldersNotification:
@@ -752,15 +754,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
752754
logger.log("Received request \(id): \(params.forLogging)")
753755

754756
if let textDocumentRequest = params as? any TextDocumentRequest {
755-
// When we are requesting information from a document, poke preparation of its target. We don't want to wait for
756-
// the preparation to finish because that would cause too big a delay.
757-
// In practice, while the user is working on a file, we'll get a text document request for it on a regular basis,
758-
// which prepares the files. For files that are open but aren't being worked on (eg. a different tab), we don't
759-
// get requests, ensuring that we don't unnecessarily prepare them.
760-
let workspace = await self.workspaceForDocument(uri: textDocumentRequest.textDocument.uri)
761-
await workspace?.semanticIndexManager?.schedulePreparationForEditorFunctionality(
762-
of: textDocumentRequest.textDocument.uri
763-
)
757+
await self.clientInteractedWithDocument(textDocumentRequest.textDocument.uri)
764758
}
765759

766760
switch request {
@@ -949,25 +943,22 @@ extension SourceKitLSPServer {
949943
//
950944
// The below is a workaround for the vscode-swift extension since it cannot set client capabilities.
951945
// It passes "workspace/peekDocuments" through the `initializationOptions`.
952-
//
953-
// Similarly, for "workspace/getReferenceDocument".
954946
var clientCapabilities = req.capabilities
955947
if case .dictionary(let initializationOptions) = req.initializationOptions {
956-
if let peekDocuments = initializationOptions["workspace/peekDocuments"] {
957-
if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental {
958-
experimentalCapabilities["workspace/peekDocuments"] = peekDocuments
959-
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
960-
} else {
961-
clientCapabilities.experimental = .dictionary(["workspace/peekDocuments": peekDocuments])
948+
let experimentalClientCapabilities = [
949+
PeekDocumentsRequest.method,
950+
GetReferenceDocumentRequest.method,
951+
DidChangeActiveDocumentNotification.method,
952+
]
953+
for capabilityName in experimentalClientCapabilities {
954+
guard let experimentalCapability = initializationOptions[capabilityName] else {
955+
continue
962956
}
963-
}
964-
965-
if let getReferenceDocument = initializationOptions["workspace/getReferenceDocument"] {
966957
if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental {
967-
experimentalCapabilities["workspace/getReferenceDocument"] = getReferenceDocument
958+
experimentalCapabilities[capabilityName] = experimentalCapability
968959
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
969960
} else {
970-
clientCapabilities.experimental = .dictionary(["workspace/getReferenceDocument": getReferenceDocument])
961+
clientCapabilities.experimental = .dictionary([capabilityName: experimentalCapability])
971962
}
972963
}
973964

@@ -1099,10 +1090,11 @@ extension SourceKitLSPServer {
10991090
: ExecuteCommandOptions(commands: builtinSwiftCommands)
11001091

11011092
var experimentalCapabilities: [String: LSPAny] = [
1102-
"workspace/tests": .dictionary(["version": .int(2)]),
1103-
"textDocument/tests": .dictionary(["version": .int(2)]),
1104-
"workspace/triggerReindex": .dictionary(["version": .int(1)]),
1105-
"workspace/getReferenceDocument": .dictionary(["version": .int(1)]),
1093+
WorkspaceTestsRequest.method: .dictionary(["version": .int(2)]),
1094+
DocumentTestsRequest.method: .dictionary(["version": .int(2)]),
1095+
TriggerReindexRequest.method: .dictionary(["version": .int(1)]),
1096+
GetReferenceDocumentRequest.method: .dictionary(["version": .int(1)]),
1097+
DidChangeActiveDocumentNotification.method: .dictionary(["version": .int(1)]),
11061098
]
11071099
#if canImport(SwiftDocC)
11081100
experimentalCapabilities["textDocument/doccDocumentation"] = .dictionary(["version": .int(1)])
@@ -1286,7 +1278,7 @@ extension SourceKitLSPServer {
12861278
)
12871279
return
12881280
}
1289-
await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: uri)
1281+
await self.clientInteractedWithDocument(uri)
12901282
await openDocument(notification, workspace: workspace)
12911283
}
12921284

@@ -1370,7 +1362,7 @@ extension SourceKitLSPServer {
13701362
)
13711363
return
13721364
}
1373-
await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: uri)
1365+
await self.clientInteractedWithDocument(uri)
13741366

13751367
// If the document is ready, we can handle the change right now.
13761368
let editResult = orLog("Editing document") {
@@ -1392,6 +1384,30 @@ extension SourceKitLSPServer {
13921384
)
13931385
}
13941386

1387+
/// If the client doesn't support the `window/didChangeActiveDocument` notification, when the client interacts with a
1388+
/// document, infer that this is the currently active document and poke preparation of its target. We don't want to
1389+
/// wait for the preparation to finish because that would cause too big a delay.
1390+
///
1391+
/// In practice, while the user is working on a file, we'll get a text document request for it on a regular basis,
1392+
/// which prepares the files. For files that are open but aren't being worked on (eg. a different tab), we don't
1393+
/// get requests, ensuring that we don't unnecessarily prepare them.
1394+
func clientInteractedWithDocument(_ uri: DocumentURI) async {
1395+
if self.capabilityRegistry?.clientHasExperimentalCapability(DidChangeActiveDocumentNotification.method) ?? false {
1396+
// The client actively notifies us about the currently active document, so we shouldn't infer the active document
1397+
// based on other requests that the client sends us.
1398+
return
1399+
}
1400+
await self.didChangeActiveDocument(
1401+
DidChangeActiveDocumentNotification(textDocument: TextDocumentIdentifier(uri))
1402+
)
1403+
}
1404+
1405+
func didChangeActiveDocument(_ notification: DidChangeActiveDocumentNotification) async {
1406+
let workspace = await self.workspaceForDocument(uri: notification.textDocument.uri)
1407+
await workspace?.semanticIndexManager?
1408+
.schedulePreparationForEditorFunctionality(of: notification.textDocument.uri)
1409+
}
1410+
13951411
func willSaveDocument(
13961412
_ notification: WillSaveTextDocumentNotification,
13971413
languageService: LanguageService

Sources/SourceKitLSP/Swift/MacroExpansion.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,8 @@ extension SwiftLanguageService {
221221
}
222222
}
223223

224-
if case .dictionary(let experimentalCapabilities) = self.capabilityRegistry.clientCapabilities.experimental,
225-
case .bool(true) = experimentalCapabilities["workspace/peekDocuments"],
226-
case .bool(true) = experimentalCapabilities["workspace/getReferenceDocument"]
224+
if self.capabilityRegistry.clientHasExperimentalCapability(PeekDocumentsRequest.method),
225+
self.capabilityRegistry.clientHasExperimentalCapability(GetReferenceDocumentRequest.method)
227226
{
228227
let expansionURIs = try macroExpansionReferenceDocumentURLs.map { try $0.uri }
229228

Tests/SourceKitLSPTests/ExpandMacroTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ final class ExpandMacroTests: XCTestCase {
268268
files: files,
269269
manifest: SwiftPMTestProject.macroPackageManifest,
270270
capabilities: ClientCapabilities(experimental: [
271-
"workspace/peekDocuments": .bool(peekDocuments),
272-
"workspace/getReferenceDocument": .bool(getReferenceDocument),
271+
PeekDocumentsRequest.method: .bool(peekDocuments),
272+
GetReferenceDocumentRequest.method: .bool(getReferenceDocument),
273273
]),
274274
options: SourceKitLSPOptions.testDefault(),
275275
enableBackgroundIndexing: true

Tests/SourceKitLSPTests/WorkspaceTests.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import BuildServerProtocol
1314
import BuildSystemIntegration
1415
import Foundation
1516
import LanguageServerProtocol
1617
import SKLogging
1718
import SKOptions
1819
import SKTestSupport
20+
import SemanticIndex
1921
import SourceKitLSP
2022
import SwiftExtensions
2123
import TSCBasic
@@ -1123,6 +1125,67 @@ final class WorkspaceTests: XCTestCase {
11231125
project.scratchDirectory
11241126
)
11251127
}
1128+
1129+
func testDidChangeActiveEditorDocument() async throws {
1130+
let didChangeBaseLib = AtomicBool(initialValue: false)
1131+
let didPrepareLibBAfterChangingBaseLib = self.expectation(description: "Did prepare LibB after changing base lib")
1132+
let project = try await SwiftPMTestProject(
1133+
files: [
1134+
"BaseLib/BaseLib.swift": "",
1135+
"LibA/LibA.swift": "",
1136+
"LibB/LibB.swift": "",
1137+
],
1138+
manifest: """
1139+
let package = Package(
1140+
name: "MyLib",
1141+
targets: [
1142+
.target(name: "BaseLib"),
1143+
.target(name: "LibA", dependencies: ["BaseLib"]),
1144+
.target(name: "LibB", dependencies: ["BaseLib"]),
1145+
]
1146+
)
1147+
""",
1148+
capabilities: ClientCapabilities(experimental: [
1149+
DidChangeActiveDocumentNotification.method: .bool(true)
1150+
]),
1151+
hooks: Hooks(
1152+
indexHooks: IndexHooks(preparationTaskDidStart: { task in
1153+
guard didChangeBaseLib.value else {
1154+
return
1155+
}
1156+
do {
1157+
XCTAssert(
1158+
task.targetsToPrepare.contains(try BuildTargetIdentifier(target: "LibB", destination: .target)),
1159+
"Prepared unexpected targets: \(task.targetsToPrepare)"
1160+
)
1161+
try await repeatUntilExpectedResult {
1162+
Task.currentPriority > .low
1163+
}
1164+
didPrepareLibBAfterChangingBaseLib.fulfill()
1165+
} catch {
1166+
XCTFail("Received unexpected error: \(error)")
1167+
}
1168+
})
1169+
),
1170+
enableBackgroundIndexing: true
1171+
)
1172+
1173+
_ = try project.openDocument("LibA.swift")
1174+
let (libBUri, _) = try project.openDocument("LibB.swift")
1175+
1176+
project.testClient.send(
1177+
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: try project.uri(for: "BaseLib.swift"), type: .changed)]
1178+
)
1179+
)
1180+
didChangeBaseLib.value = true
1181+
1182+
project.testClient.send(
1183+
DidChangeActiveDocumentNotification(textDocument: TextDocumentIdentifier(libBUri))
1184+
)
1185+
try await fulfillmentOfOrThrow([didPrepareLibBAfterChangingBaseLib])
1186+
1187+
withExtendedLifetime(project) {}
1188+
}
11261189
}
11271190

11281191
fileprivate let defaultSDKArgs: String = {

0 commit comments

Comments
 (0)