Skip to content

Commit 4d00c90

Browse files
committed
Add notification to inform SourceKit-LSP about the currently active document
SourceKit-LSP prepares the currently active file for editor functionality and currently infers the currently active document from whichever received the last `TextDocumentRequest`. If an editor is capable of doing so, it should be able to report the document that the user currently has focused so that SourceKit-LSP does not have to infer this information from other requests. Also clean up some handling code for experimental capabilities.
1 parent 4cb8a56 commit 4d00c90

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
@@ -98,6 +98,7 @@ public let builtinRequests: [_RequestType.Type] = [
9898
public let builtinNotifications: [NotificationType.Type] = [
9999
CancelRequestNotification.self,
100100
CancelWorkDoneProgressNotification.self,
101+
DidChangeActiveDocumentNotification.self,
101102
DidChangeConfigurationNotification.self,
102103
DidChangeNotebookDocumentNotification.self,
103104
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
@@ -650,6 +650,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
650650
logger.log("Received notification: \(notification.forLogging)")
651651

652652
switch notification {
653+
case let notification as DidChangeActiveDocumentNotification:
654+
await self.didChangeActiveDocument(notification)
653655
case let notification as DidChangeTextDocumentNotification:
654656
await self.changeDocument(notification)
655657
case let notification as DidChangeWorkspaceFoldersNotification:
@@ -736,15 +738,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
736738
logger.log("Received request \(id): \(params.forLogging)")
737739

738740
if let textDocumentRequest = params as? any TextDocumentRequest {
739-
// When we are requesting information from a document, poke preparation of its target. We don't want to wait for
740-
// the preparation to finish because that would cause too big a delay.
741-
// In practice, while the user is working on a file, we'll get a text document request for it on a regular basis,
742-
// which prepares the files. For files that are open but aren't being worked on (eg. a different tab), we don't
743-
// get requests, ensuring that we don't unnecessarily prepare them.
744-
let workspace = await self.workspaceForDocument(uri: textDocumentRequest.textDocument.uri)
745-
await workspace?.semanticIndexManager?.schedulePreparationForEditorFunctionality(
746-
of: textDocumentRequest.textDocument.uri
747-
)
741+
await self.clientInteractedWithDocument(textDocumentRequest.textDocument.uri)
748742
}
749743

750744
switch request {
@@ -931,25 +925,22 @@ extension SourceKitLSPServer {
931925
//
932926
// The below is a workaround for the vscode-swift extension since it cannot set client capabilities.
933927
// It passes "workspace/peekDocuments" through the `initializationOptions`.
934-
//
935-
// Similarly, for "workspace/getReferenceDocument".
936928
var clientCapabilities = req.capabilities
937929
if case .dictionary(let initializationOptions) = req.initializationOptions {
938-
if let peekDocuments = initializationOptions["workspace/peekDocuments"] {
939-
if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental {
940-
experimentalCapabilities["workspace/peekDocuments"] = peekDocuments
941-
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
942-
} else {
943-
clientCapabilities.experimental = .dictionary(["workspace/peekDocuments": peekDocuments])
930+
let experimentalClientCapabilities = [
931+
PeekDocumentsRequest.method,
932+
GetReferenceDocumentRequest.method,
933+
DidChangeActiveDocumentNotification.method,
934+
]
935+
for capabilityName in experimentalClientCapabilities {
936+
guard let experimentalCapability = initializationOptions[capabilityName] else {
937+
continue
944938
}
945-
}
946-
947-
if let getReferenceDocument = initializationOptions["workspace/getReferenceDocument"] {
948939
if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental {
949-
experimentalCapabilities["workspace/getReferenceDocument"] = getReferenceDocument
940+
experimentalCapabilities[capabilityName] = experimentalCapability
950941
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
951942
} else {
952-
clientCapabilities.experimental = .dictionary(["workspace/getReferenceDocument": getReferenceDocument])
943+
clientCapabilities.experimental = .dictionary([capabilityName: experimentalCapability])
953944
}
954945
}
955946

@@ -1081,10 +1072,11 @@ extension SourceKitLSPServer {
10811072
: ExecuteCommandOptions(commands: builtinSwiftCommands)
10821073

10831074
var experimentalCapabilities: [String: LSPAny] = [
1084-
"workspace/tests": .dictionary(["version": .int(2)]),
1085-
"textDocument/tests": .dictionary(["version": .int(2)]),
1086-
"workspace/triggerReindex": .dictionary(["version": .int(1)]),
1087-
"workspace/getReferenceDocument": .dictionary(["version": .int(1)]),
1075+
WorkspaceTestsRequest.method: .dictionary(["version": .int(2)]),
1076+
DocumentTestsRequest.method: .dictionary(["version": .int(2)]),
1077+
TriggerReindexRequest.method: .dictionary(["version": .int(1)]),
1078+
GetReferenceDocumentRequest.method: .dictionary(["version": .int(1)]),
1079+
DidChangeActiveDocumentNotification.method: .dictionary(["version": .int(1)]),
10881080
]
10891081
#if canImport(SwiftDocC)
10901082
experimentalCapabilities["textDocument/doccDocumentation"] = .dictionary(["version": .int(1)])
@@ -1268,7 +1260,7 @@ extension SourceKitLSPServer {
12681260
)
12691261
return
12701262
}
1271-
await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: uri)
1263+
await self.clientInteractedWithDocument(uri)
12721264
await openDocument(notification, workspace: workspace)
12731265
}
12741266

@@ -1352,7 +1344,7 @@ extension SourceKitLSPServer {
13521344
)
13531345
return
13541346
}
1355-
await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: uri)
1347+
await self.clientInteractedWithDocument(uri)
13561348

13571349
// If the document is ready, we can handle the change right now.
13581350
let editResult = orLog("Editing document") {
@@ -1374,6 +1366,30 @@ extension SourceKitLSPServer {
13741366
)
13751367
}
13761368

1369+
/// If the client doesn't support the `window/didChangeActiveDocument` notification, when the client interacts with a
1370+
/// document, infer that this is the currently active document and poke preparation of its target. We don't want to
1371+
/// wait for the preparation to finish because that would cause too big a delay.
1372+
///
1373+
/// In practice, while the user is working on a file, we'll get a text document request for it on a regular basis,
1374+
/// which prepares the files. For files that are open but aren't being worked on (eg. a different tab), we don't
1375+
/// get requests, ensuring that we don't unnecessarily prepare them.
1376+
func clientInteractedWithDocument(_ uri: DocumentURI) async {
1377+
if self.capabilityRegistry?.clientHasExperimentalCapability(DidChangeActiveDocumentNotification.method) ?? false {
1378+
// The client actively notifies us about the currently active document, so we shouldn't infer the active document
1379+
// based on other requests that the client sends us.
1380+
return
1381+
}
1382+
await self.didChangeActiveDocument(
1383+
DidChangeActiveDocumentNotification(textDocument: TextDocumentIdentifier(uri))
1384+
)
1385+
}
1386+
1387+
func didChangeActiveDocument(_ notification: DidChangeActiveDocumentNotification) async {
1388+
let workspace = await self.workspaceForDocument(uri: notification.textDocument.uri)
1389+
await workspace?.semanticIndexManager?
1390+
.schedulePreparationForEditorFunctionality(of: notification.textDocument.uri)
1391+
}
1392+
13771393
func willSaveDocument(
13781394
_ notification: WillSaveTextDocumentNotification,
13791395
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)