Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Sources/SourceKitLSP/LanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ package protocol LanguageService: AnyObject, Sendable {
func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation]
func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse?
func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint]
func inlayHintResolve(_ req: InlayHintResolveRequest) async throws -> InlayHint
func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens]
func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport
func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]?
Expand Down Expand Up @@ -471,6 +472,10 @@ package extension LanguageService {
throw ResponseError.requestNotImplemented(InlayHintRequest.self)
}

func inlayHintResolve(_ req: InlayHintResolveRequest) async throws -> InlayHint {
return req.inlayHint
}

func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] {
throw ResponseError.requestNotImplemented(CodeLensRequest.self)
}
Expand Down
20 changes: 19 additions & 1 deletion Sources/SourceKitLSP/SourceKitLSPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
initialized = true
case let request as RequestAndReply<InlayHintRequest>:
await self.handleRequest(for: request, requestHandler: self.inlayHint)
case let request as RequestAndReply<InlayHintResolveRequest>:
await request.reply { try await inlayHintResolve(request: request.params) }
case let request as RequestAndReply<IsIndexingRequest>:
await request.reply { try await self.isIndexing(request.params) }
case let request as RequestAndReply<OutputPathsRequest>:
Expand Down Expand Up @@ -1098,7 +1100,7 @@ extension SourceKitLSPServer {
let inlayHintOptions =
await registry.clientHasDynamicInlayHintRegistration
? nil
: ValueOrBool.value(InlayHintOptions(resolveProvider: false))
: ValueOrBool.value(InlayHintOptions(resolveProvider: true))

let semanticTokensOptions =
await registry.clientHasDynamicSemanticTokensRegistration
Expand Down Expand Up @@ -1901,6 +1903,22 @@ extension SourceKitLSPServer {
return try await languageService.inlayHint(req)
}

func inlayHintResolve(
request: InlayHintResolveRequest
) async throws -> InlayHint {
guard case .dictionary(let dict) = request.inlayHint.data,
case .string(let uriString) = dict["uri"],
let uri = try? DocumentURI(string: uriString)
else {
return request.inlayHint
}
guard let workspace = await self.workspaceForDocument(uri: uri) else {
return request.inlayHint
}
let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language
return try await primaryLanguageService(for: uri, language, in: workspace).inlayHintResolve(request)
}

func documentDiagnostic(
_ req: DocumentDiagnosticsRequest,
workspace: Workspace,
Expand Down
47 changes: 47 additions & 0 deletions Sources/SwiftLanguageService/CursorInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,51 @@ extension SwiftLanguageService {
additionalParameters: appendAdditionalParameters
)
}

/// Because of https://github.com/swiftlang/swift/issues/86432 sourcekitd returns a mangled name instead of a USR
/// as the type USR. Work around this by replacing mangled names (starting with `$s`) to a USR, starting with `s:`.
/// We also strip the trailing `D` suffix which represents a type mangling - this may not work correctly for generic
/// types with type arguments.
// TODO: Remove once https://github.com/swiftlang/swift/issues/86432 is fixed
private func convertMangledTypeToUSR(_ mangledType: String) -> String {
var result = mangledType
if result.hasPrefix("$s") {
result = "s:" + result.dropFirst(2)
}
// Strip trailing 'D' (type mangling suffix) to get declaration USR
if result.hasSuffix("D") {
result = String(result.dropLast())
}
Comment on lines +221 to +224
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need to do this, cursor info should be able to handle a type USR with it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm but the tests fail without the D stripping.

Copy link
Copy Markdown
Contributor

@hamishknight hamishknight Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh interesting, I saw handling of Demangle::Node::Kind::Type in the code and assumed that was referring to D but it's actually a different kind. I guess this is okay for now but we really ought to fix this on the sourcekitd side, ideally at the same time as the $s -> s: change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okei

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to add my 2cts, while I’m not a huge fan, I’m fine with having the stripping of D as a temporary workaround for now as long as we try to adjust sourcekitd to work without this hack in the future.

return result
}

/// Get cursor info for a type by looking up its USR.
///
/// - Parameters:
/// - mangledType: The mangled name of the type
/// - snapshot: Document snapshot for context (used to get compile command)
/// - Returns: CursorInfo for the type declaration, or `nil` if not found
func cursorInfoFromTypeUSR(
_ mangledType: String,
in snapshot: DocumentSnapshot
) async throws -> CursorInfo? {
let usr = convertMangledTypeToUSR(mangledType)

let compileCommand = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)
let documentManager = try self.documentManager

let keys = self.keys

let skreq = sourcekitd.dictionary([
keys.cancelOnSubsequentRequest: 0,
keys.usr: usr,
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath,
keys.compilerArgs: compileCommand?.compilerArgs as [any SKDRequestValue]?,
])

let dict = try await send(sourcekitdRequest: \.cursorInfo, skreq, snapshot: snapshot)

return CursorInfo(dict, snapshot: snapshot, documentManager: documentManager, sourcekitd: sourcekitd)
}
}
113 changes: 113 additions & 0 deletions Sources/SwiftLanguageService/InlayHintResolve.swift
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this file to CMakeLists.txt to make the tests pass on Windows?

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import IndexStoreDB
@_spi(SourceKitLSP) package import LanguageServerProtocol
import SemanticIndex
import SourceKitD
import SourceKitLSP

extension SwiftLanguageService {
/// Resolves an inlay hint by looking up the type definition location.
package func inlayHintResolve(_ req: InlayHintResolveRequest) async throws -> InlayHint {
let hint = req.inlayHint

guard hint.kind == .type,
let resolveData = InlayHintResolveData(fromLSPAny: hint.data)
else {
return hint
}

// Fail if document version has changed since the hint was created
let currentSnapshot = try await self.latestSnapshot(for: resolveData.uri)
guard currentSnapshot.version == resolveData.version else {
return hint
}

let typeLocation = try await lookupTypeDefinitionLocation(
snapshot: currentSnapshot,
position: resolveData.position
)

guard let typeLocation else {
return hint
}

if case .string(let labelText) = hint.label {
return InlayHint(
position: hint.position,
label: .parts([InlayHintLabelPart(value: labelText, location: typeLocation)]),
kind: hint.kind,
textEdits: hint.textEdits,
tooltip: hint.tooltip,
paddingLeft: hint.paddingLeft,
paddingRight: hint.paddingRight,
data: hint.data
)
}

return hint
}

/// Looks up the definition location for the type at the given position.
///
/// This is used by inlay hint resolution to enable go-to-definition on type hints.
/// For SDK types, this returns a location in the generated interface.
func lookupTypeDefinitionLocation(
snapshot: DocumentSnapshot,
position: Position
) async throws -> Location? {
let compileCommand = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)

let skreq = sourcekitd.dictionary([
keys.cancelOnSubsequentRequest: 0,
keys.offset: snapshot.utf8Offset(of: position),
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath,
keys.compilerArgs: compileCommand?.compilerArgs as [any SKDRequestValue]?,
])

let dict = try await send(sourcekitdRequest: \.cursorInfo, skreq, snapshot: snapshot)

guard let typeUsr: String = dict[keys.typeUsr] else {
return nil
}

guard let typeInfo = try await cursorInfoFromTypeUSR(typeUsr, in: snapshot) else {
return nil
}

// For local types, return the local declaration
if let location = typeInfo.symbolInfo.bestLocalDeclaration {
return location
}

// For SDK types, fall back to generated interface
if typeInfo.symbolInfo.isSystem ?? false,
let systemModule = typeInfo.symbolInfo.systemModule
{
let interfaceDetails = try await self.openGeneratedInterface(
document: snapshot.uri,
moduleName: systemModule.moduleName,
groupName: systemModule.groupName,
symbolUSR: typeInfo.symbolInfo.usr
)
if let details = interfaceDetails {
let position = details.position ?? Position(line: 0, utf16index: 0)
return Location(uri: details.uri, range: Range(position))
}
}

return nil
}
}
43 changes: 41 additions & 2 deletions Sources/SwiftLanguageService/InlayHints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,45 @@
//
//===----------------------------------------------------------------------===//

import Foundation
@_spi(SourceKitLSP) package import LanguageServerProtocol
import SourceKitLSP
import SwiftExtensions
import SwiftSyntax

package struct InlayHintResolveData: LSPAnyCodable {
package let uri: DocumentURI
package let position: Position
package let version: Int

package init(uri: DocumentURI, position: Position, version: Int) {
self.uri = uri
self.position = position
self.version = version
}

package init?(fromLSPDictionary dictionary: [String: LSPAny]) {
guard case .string(let uriString) = dictionary["uri"],
let uri = try? DocumentURI(string: uriString),
case .int(let version) = dictionary["version"],
let position = Position(fromLSPAny: dictionary["position"])
else {
return nil
}
self.uri = uri
self.position = position
self.version = version
}

package func encodeToLSPAny() -> LSPAny {
return .dictionary([
"uri": .string(uri.stringValue),
"position": position.encodeToLSPAny(),
"version": .int(version),
])
}
}

private class IfConfigCollector: SyntaxVisitor {
private var ifConfigDecls: [IfConfigDeclSyntax] = []

Expand All @@ -34,28 +68,33 @@ private class IfConfigCollector: SyntaxVisitor {
extension SwiftLanguageService {
package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] {
let uri = req.textDocument.uri
let snapshot = try await self.latestSnapshot(for: uri)
let version = snapshot.version

let infos = try await variableTypeInfos(uri, req.range)
let typeHints = infos
.lazy
.filter { !$0.hasExplicitType }
.map { info -> InlayHint in
let position = info.range.upperBound
let variableStart = info.range.lowerBound
let label = ": \(info.printedType)"
let textEdits: [TextEdit]?
if info.canBeFollowedByTypeAnnotation {
textEdits = [TextEdit(range: position..<position, newText: label)]
} else {
textEdits = nil
}
let resolveData = InlayHintResolveData(uri: uri, position: variableStart, version: version)
return InlayHint(
position: position,
label: .string(label),
kind: .type,
textEdits: textEdits
textEdits: textEdits,
data: resolveData.encodeToLSPAny()
)
}

let snapshot = try await self.latestSnapshot(for: uri)
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
let ifConfigDecls = IfConfigCollector.collectIfConfigDecls(in: syntaxTree)
let ifConfigHints = ifConfigDecls.compactMap { (ifConfigDecl) -> InlayHint? in
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftLanguageService/SwiftLanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ extension SwiftLanguageService {
range: .bool(true),
full: .bool(true)
),
inlayHintProvider: .value(InlayHintOptions(resolveProvider: false)),
inlayHintProvider: .value(InlayHintOptions(resolveProvider: true)),
diagnosticProvider: DiagnosticOptions(
interFileDependencies: true,
workspaceDiagnostics: false
Expand Down
Loading