Files
sourcekit-lsp/Sources/SwiftLanguageService/InlayHints.swift
T
Steffeeen c5593c09c6 Cache inlay hints to avoid flickering
Previously, we always recomputed the inlay hints by calling into
SourceKit. If the document that we compute inlay hints for has recently
changed, SourceKit may need a bit of time to recompute the semantic
analysis. The inlay hints may thus need roughly 200-700ms to be
computed. This causes flickering in VSCode as it removes the old inlay
hints immediately and only displays them again when SourceKit-LSP
returned them.

With this commit we cache the inlay hints and recompute them in the
background. After the recompute is finished we use
`workspace/inlayHint/refresh` request to tell the client to refresh its
inlay hints. On each textDocument/didChange request the cached inlay
hints are shifted according to the text edits to ensure their positions
are still correct. This avoids the flickering as we can always return
the cached inlay hints immediately. The returned hints may however
temporarily show outdated type information until the background
recompute is finished.
2026-04-29 20:45:23 +02:00

111 lines
4.4 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 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
@_spi(SourceKitLSP) package import LanguageServerProtocol
import SKUtilities
import SourceKitLSP
import SwiftExtensions
import SwiftSyntax
extension SwiftLanguageService {
package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] {
let uri = req.textDocument.uri
let snapshot = try await latestSnapshot(for: uri)
if let sourceKitLSPServer = self.sourceKitLSPServer,
let clientCapabilities = await sourceKitLSPServer.capabilityRegistry?.clientCapabilities,
!(clientCapabilities.workspace?.inlayHint?.refreshSupport ?? false)
{
// The client does not support workspace/inlayHint/refresh.
// We have to compute inlay hints on every request, because we cannot trigger a refresh when the inlay hints have been recomputed in the background.
let snapshot = try await latestSnapshot(for: uri)
async let typeInlayHints = inlayHintManager.computeTypeInlayHints(
swiftLanguageService: self,
for: snapshot,
range: req.range
)
return try await typeInlayHints + computeIfConfigInlayHints(snapshot: snapshot, range: req.range)
}
if let hints = await inlayHintManager.getCachedInlayHints(
swiftLanguageService: self,
for: snapshot,
range: req.range
) {
return try await hints + computeIfConfigInlayHints(snapshot: snapshot, range: req.range)
}
// No cached hints are available. The inlay hint manager has scheduled a refresh task if needed, so we can just
// return an empty response here. The client will trigger another request after the refresh completes, because of
// the InlayHintRefreshRequest sent in the refresh task.
return []
}
private func computeIfConfigInlayHints(
snapshot: DocumentSnapshot,
range: Range<Position>?
) async throws -> [InlayHint] {
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
let absoluteRange = range.map { snapshot.absolutePositionRange(of: $0) }
let ifConfigDecls = IfConfigCollector.collectIfConfigDecls(in: syntaxTree, range: absoluteRange)
return ifConfigDecls.compactMap { (ifConfigDecl) -> InlayHint? in
// Do not show inlay hints for if config clauses that have a `#elseif` of `#else` clause since it is unclear which
// `#if`, `#elseif`, or `#else` clause the `#endif` now refers to.
guard let condition = ifConfigDecl.clauses.only?.condition else {
return nil
}
guard !ifConfigDecl.poundEndif.trailingTrivia.contains(where: { $0.isComment }) else {
// If a comment already exists (eg. because the user inserted it), don't show an inlay hint.
return nil
}
let hintPosition = snapshot.position(of: ifConfigDecl.poundEndif.endPositionBeforeTrailingTrivia)
let label = " // \(condition.trimmedDescription)"
return InlayHint(
position: hintPosition,
label: .string(label),
kind: .type, // For the lack of a better kind, pretend this comment is a type
textEdits: [TextEdit(range: Range(hintPosition), newText: label)],
tooltip: .string("Condition of this conditional compilation clause")
)
}
}
}
private class IfConfigCollector: SyntaxVisitor {
private var ifConfigDecls: [IfConfigDeclSyntax] = []
private let range: Range<AbsolutePosition>?
init(viewMode: SyntaxTreeViewMode, range: Range<AbsolutePosition>?) {
self.range = range
super.init(viewMode: viewMode)
}
override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
if let range, !range.overlaps(node.range) {
return .skipChildren
}
ifConfigDecls.append(node)
return .visitChildren
}
static func collectIfConfigDecls(
in tree: some SyntaxProtocol,
range: Range<AbsolutePosition>?
) -> [IfConfigDeclSyntax] {
let visitor = IfConfigCollector(viewMode: .sourceAccurate, range: range)
visitor.walk(tree)
return visitor.ifConfigDecls
}
}