mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-06-24 12:21:58 +02:00
c5593c09c6
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.
111 lines
4.4 KiB
Swift
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
|
|
}
|
|
}
|