Skip to content

Commit

Permalink
Merge pull request #1988 from ahoppen/sourcekitoptions
Browse files Browse the repository at this point in the history
Add an experimental request to return the build settings that SourceKit-LSP uses to process a file
  • Loading branch information
ahoppen authored Feb 27, 2025
2 parents dae74db + 9496b49 commit f4e015b
Show file tree
Hide file tree
Showing 22 changed files with 668 additions and 36 deletions.
4 changes: 4 additions & 0 deletions Contributor Documentation/BSP Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ export interface TextDocumentSourceKitOptionsResult {

/** The working directory for the compile command. */
workingDirectory?: string;

/** Additional data that will not be interpreted by SourceKit-LSP but made available to clients in the
* `workspace/_sourceKitOptions` LSP requests. */
data?: LSPAny;
}
```
Expand Down
96 changes: 96 additions & 0 deletions Contributor Documentation/LSP Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,102 @@ export interface SetOptionsParams {
}
```
## `workspace/_sourceKitOptions`
New request from the client to the server to retrieve the compiler arguments that SourceKit-LSP uses to process the document.
This request does not require the document to be opened in SourceKit-LSP. This is also why it has the `workspace/` instead of the `textDocument/` prefix.
> [!IMPORTANT]
> This request is experimental, guarded behind the `sourcekit-options-request` experimental feature, and may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it.
- params: `SourceKitOptionsRequest`
- result: `SourceKitOptionsResult`
```ts
export interface SourceKitOptionsRequest {
/**
* The document to get options for
*/
textDocument: TextDocumentIdentifier;

/**
* If specified, explicitly request the compiler arguments when interpreting the document in the context of the given
* target.
*
* The target URI must match the URI that is used by the BSP server to identify the target. This option thus only
* makes sense to specify if the client also controls the BSP server.
*
* When this is `null`, SourceKit-LSP returns the compiler arguments it uses when the the document is opened in the
* client, ie. it infers a canonical target for the document.
*/
target?: DocumentURI;

/**
* Whether SourceKit-LSP should ensure that the document's target is prepared before returning build settings.
*
* There is a tradeoff whether the target should be prepared: Preparing a target may take significant time but if the
* target is not prepared, the build settings might eg. refer to modules that haven't been built yet.
*/
prepareTarget: bool;

/**
* If set to `true` and build settings could not be determined within a timeout (see `buildSettingsTimeout` in the
* SourceKit-LSP configuration file), this request returns fallback build settings.
*
* If set to `true` the request only finishes when build settings were provided by the build system.
*/
allowFallbackSettings: bool
}

/**
* The kind of options that were returned by the `workspace/_sourceKitOptions` request, ie. whether they are fallback
* options or the real compiler options for the file.
*/
export namespace SourceKitOptionsKind {
/**
* The SourceKit options are known to SourceKit-LSP and returned them.
*/
export const normal = "normal"

/**
* SourceKit-LSP was unable to determine the build settings for this file and synthesized fallback settings.
*/
export const fallback = "fallback"
}

export interface SourceKitOptionsResult {
/**
* The compiler options required for the requested file.
*/
compilerArguments: string[];

/**
* The working directory for the compile command.
*/
workingDirectory?: string;

/**
* Whether SourceKit-LSP was able to determine the build settings or synthesized fallback settings.
*/
kind: SourceKitOptionsKind;

/**
* - `true` If the request requested the file's target to be prepared and the target needed preparing
* - `false` If the request requested the file's target to be prepared and the target was up to date
* - `nil`: If the request did not request the file's target to be prepared or the target could not be prepared for
* other reasons
*/
didPrepareTarget?: bool

/**
* Additional data that the BSP server returned in the `textDocument/sourceKitOptions` BSP request. This data is not
* interpreted by SourceKit-LSP.
*/
data?: LSPAny
}
```
## `workspace/getReferenceDocument`
Expand Down
2 changes: 1 addition & 1 deletion Documentation/Configuration File.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
- `noLazy`: Prepare a target without generating object files but do not do lazy type checking and function body skipping. This uses SwiftPM's `--experimental-prepare-for-indexing-no-lazy` flag.
- `enabled`: Prepare a target without generating object files.
- `cancelTextDocumentRequestsOnEditAndClose: boolean`: Whether sending a `textDocument/didChange` or `textDocument/didClose` notification for a document should cancel all pending requests for that document.
- `experimentalFeatures: ("on-type-formatting"|"set-options-request")[]`: Experimental features that are enabled.
- `experimentalFeatures: ("on-type-formatting"|"set-options-request"|"sourcekit-options-request")[]`: Experimental features that are enabled.
- `swiftPublishDiagnosticsDebounceDuration: number`: The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and sending a `PublishDiagnosticsNotification`.
- `workDoneProgressDebounceDuration: number`: When a task is started that should be displayed to the client as a work done progress, how many milliseconds to wait before actually starting the work done progress. This prevents flickering of the work done progress in the client for short-lived index tasks which end within this duration.
- `sourcekitdRequestTimeout: number`: The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out. In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic functionality. In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that blocks sourcekitd.
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ public struct TextDocumentSourceKitOptionsResponse: ResponseType, Hashable {
/// The working directory for the compile command.
public var workingDirectory: String?

public init(compilerArguments: [String], workingDirectory: String? = nil) {
/// Additional data that will not be interpreted by SourceKit-LSP but made available to clients in the
/// `workspace/_sourceKitOptions` LSP requests.
public var data: LSPAny?

public init(compilerArguments: [String], workingDirectory: String? = nil, data: LSPAny? = nil) {
self.compilerArguments = compilerArguments
self.workingDirectory = workingDirectory
self.data = data
}
}
59 changes: 46 additions & 13 deletions Sources/BuildSystemIntegration/BuildSystemManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
return FileBuildSettings(
compilerArguments: response.compilerArguments,
workingDirectory: response.workingDirectory,
language: language,
data: response.data,
isFallback: false
)
}
Expand Down Expand Up @@ -869,25 +871,49 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
/// be inferred from the primary main file of the document. In practice this means that we will compute the build
/// settings of a C file that includes the header and replace any file references to that C file in the build settings
/// by the header file.
///
/// When a target is passed in, the build settings for the document, interpreted as part of that target, are returned,
/// otherwise a canonical target is inferred for the source file.
///
/// If no language is passed, this method tries to infer the language of the document from the build system. If that
/// fails, it returns `nil`.
package func buildSettingsInferredFromMainFile(
for document: DocumentURI,
language: Language,
target explicitlyRequestedTarget: BuildTargetIdentifier? = nil,
language: Language?,
fallbackAfterTimeout: Bool
) async -> FileBuildSettings? {
func mainFileAndSettings(
basedOn document: DocumentURI
) async -> (mainFile: DocumentURI, settings: FileBuildSettings)? {
let mainFile = await self.mainFile(for: document, language: language)
let settings = await orLog("Getting build settings") {
let target = try await withTimeout(options.buildSettingsTimeoutOrDefault) {
await self.canonicalTarget(for: mainFile)
} resultReceivedAfterTimeout: {
await self.delegate?.fileBuildSettingsChanged([document])
let settings: FileBuildSettings? = await orLog("Getting build settings") {
let target =
if let explicitlyRequestedTarget {
explicitlyRequestedTarget
} else {
try await withTimeout(options.buildSettingsTimeoutOrDefault) {
await self.canonicalTarget(for: mainFile)
} resultReceivedAfterTimeout: {
await self.delegate?.fileBuildSettingsChanged([document])
}
}
var languageForFile: Language
if let language {
languageForFile = language
} else if let target, let language = await self.defaultLanguage(for: mainFile, in: target) {
languageForFile = language
} else if let language = Language(inferredFromFileExtension: mainFile) {
languageForFile = language
} else {
// We don't know the language as which to interpret the document, so we can't ask the build system for its
// settings.
return nil
}
return await self.buildSettings(
for: mainFile,
in: target,
language: language,
language: languageForFile,
fallbackAfterTimeout: fallbackAfterTimeout
)
}
Expand Down Expand Up @@ -1153,10 +1179,12 @@ package actor BuildSystemManager: QueueBasedMessageHandler {

/// Return the main file that should be used to get build settings for `uri`.
///
/// For Swift or normal C files, this will be the file itself. For header
/// files, we pick a main file that includes the header since header files
/// don't have build settings by themselves.
package func mainFile(for uri: DocumentURI, language: Language, useCache: Bool = true) async -> DocumentURI {
/// For Swift or normal C files, this will be the file itself. For header files, we pick a main file that includes the
/// header since header files don't have build settings by themselves.
///
/// `language` is a hint of the document's language to speed up the `main` file lookup. Passing `nil` if the language
/// is unknown should always be safe.
package func mainFile(for uri: DocumentURI, language: Language?, useCache: Bool = true) async -> DocumentURI {
if language == .swift {
// Swift doesn't have main files. Skip the main file provider query.
return uri
Expand Down Expand Up @@ -1351,7 +1379,9 @@ fileprivate extension TextDocumentSourceKitOptionsResponse {

result += supplementalClangIndexingArgs.flatMap { ["-Xcc", $0] }

return TextDocumentSourceKitOptionsResponse(compilerArguments: result, workingDirectory: workingDirectory)
var adjusted = self
adjusted.compilerArguments = result
return adjusted
}

/// Adjust compiler arguments that were created for building to compiler arguments that should be used for indexing
Expand Down Expand Up @@ -1411,7 +1441,10 @@ fileprivate extension TextDocumentSourceKitOptionsResponse {
result.append(
"-fsyntax-only"
)
return TextDocumentSourceKitOptionsResponse(compilerArguments: result, workingDirectory: workingDirectory)

var adjusted = self
adjusted.compilerArguments = result
return adjusted
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/BuildSystemIntegration/FallbackBuildSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ package func fallbackBuildSettings(
default:
return nil
}
return FileBuildSettings(compilerArguments: args, workingDirectory: nil, isFallback: true)
return FileBuildSettings(compilerArguments: args, workingDirectory: nil, language: language, isFallback: true)
}

private func fallbackBuildSettingsSwift(
Expand Down
27 changes: 25 additions & 2 deletions Sources/BuildSystemIntegration/FileBuildSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
//===----------------------------------------------------------------------===//

import Foundation
import LanguageServerProtocol
import LanguageServerProtocolExtensions

#if compiler(>=6)
package import LanguageServerProtocol
#else
import LanguageServerProtocol
#endif

/// Build settings for a single file.
///
/// Encapsulates all the settings needed to compile a single file, including the compiler arguments
Expand All @@ -26,12 +31,28 @@ package struct FileBuildSettings: Equatable, Sendable {
/// The working directory to resolve any relative paths in `compilerArguments`.
package var workingDirectory: String? = nil

/// The language that the document was interpreted as, and which implies the compiler to which the build settings
/// would be passed.
package var language: Language

/// Additional data about the build settings that was received from the BSP server, will not be interpreted by
/// SourceKit-LSP but returned to clients in the `workspace/_sourceKitOptions` LSP request.
package var data: LSPAny?

/// Whether the build settings were computed from a real build system or whether they are synthesized fallback arguments while the build system is still busy computing build settings.
package var isFallback: Bool

package init(compilerArguments: [String], workingDirectory: String? = nil, isFallback: Bool = false) {
package init(
compilerArguments: [String],
workingDirectory: String? = nil,
language: Language,
data: LSPAny? = nil,
isFallback: Bool = false
) {
self.compilerArguments = compilerArguments
self.workingDirectory = workingDirectory
self.language = language
self.data = data
self.isFallback = isFallback
}

Expand Down Expand Up @@ -65,6 +86,8 @@ package struct FileBuildSettings: Equatable, Sendable {
return FileBuildSettings(
compilerArguments: arguments,
workingDirectory: self.workingDirectory,
language: self.language,
data: self.data,
isFallback: self.isFallback
)
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,8 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
// with the `.cpp` file.
let buildSettings = FileBuildSettings(
compilerArguments: try await compilerArguments(for: DocumentURI(substituteFile), in: swiftPMTarget),
workingDirectory: try projectRoot.filePath
workingDirectory: try projectRoot.filePath,
language: request.language
).patching(newFile: DocumentURI(try path.asURL.realpath), originalFile: DocumentURI(substituteFile))
return TextDocumentSourceKitOptionsResponse(
compilerArguments: buildSettings.compilerArguments,
Expand Down
1 change: 1 addition & 0 deletions Sources/LanguageServerProtocol/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ add_library(LanguageServerProtocol STATIC
Requests/ShowMessageRequest.swift
Requests/ShutdownRequest.swift
Requests/SignatureHelpRequest.swift
Requests/SourceKitOptionsRequest.swift
Requests/SymbolInfoRequest.swift
Requests/TriggerReindexRequest.swift
Requests/TypeDefinitionRequest.swift
Expand Down
1 change: 1 addition & 0 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public let builtinRequests: [_RequestType.Type] = [
ShowMessageRequest.self,
ShutdownRequest.self,
SignatureHelpRequest.self,
SourceKitOptionsRequest.self,
SymbolInfoRequest.self,
TriggerReindexRequest.self,
TypeDefinitionRequest.self,
Expand Down
Loading

0 comments on commit f4e015b

Please sign in to comment.