Skip to content

Implement server-side support for inlay hints using CollectVariableType #408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions Editors/vscode/src/inlayHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ const hintStyle: InlayHintStyle = {

makeDecoration: (hint, converter) => ({
range: converter.asRange({
start: hint.position,
end: { ...hint.position, character: hint.position.character + 1 } }
),
start: { ...hint.position, character: hint.position.character - 1 },
end: hint.position
}),
renderOptions: {
after: {
// U+200C is a zero-width non-joiner to prevent the editor from
Expand Down
3 changes: 3 additions & 0 deletions Sources/SourceKitD/SKDResponseDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public final class SKDResponseDictionary {
public subscript(key: sourcekitd_uid_t?) -> Int? {
return Int(sourcekitd.api.variant_dictionary_get_int64(dict, key))
}
public subscript(key: sourcekitd_uid_t?) -> Bool? {
return sourcekitd.api.variant_dictionary_get_bool(dict, key)
}
public subscript(key: sourcekitd_uid_t?) -> sourcekitd_uid_t? {
return sourcekitd.api.variant_dictionary_get_uid(dict, key)
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/SourceKitD/sourcekitd_uids.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public struct sourcekitd_keys {
public let text: sourcekitd_uid_t
public let typename: sourcekitd_uid_t
public let usr: sourcekitd_uid_t
public let variable_offset: sourcekitd_uid_t
public let variable_length: sourcekitd_uid_t
public let variable_type: sourcekitd_uid_t
public let variable_type_explicit: sourcekitd_uid_t
public let variable_type_list: sourcekitd_uid_t

// Code Completion options.
public let codecomplete_options: sourcekitd_uid_t
Expand Down Expand Up @@ -129,6 +134,11 @@ public struct sourcekitd_keys {
text = api.uid_get_from_cstr("key.text")!
typename = api.uid_get_from_cstr("key.typename")!
usr = api.uid_get_from_cstr("key.usr")!
variable_offset = api.uid_get_from_cstr("key.variable_offset")!
variable_length = api.uid_get_from_cstr("key.variable_length")!
variable_type = api.uid_get_from_cstr("key.variable_type")!
variable_type_explicit = api.uid_get_from_cstr("key.variable_type_explicit")!
variable_type_list = api.uid_get_from_cstr("key.variable_type_list")!

// Code Completion options
codecomplete_options = api.uid_get_from_cstr("key.codecomplete.options")!
Expand All @@ -155,6 +165,7 @@ public struct sourcekitd_requests {
public let codecomplete_close: sourcekitd_uid_t
public let cursorinfo: sourcekitd_uid_t
public let expression_type: sourcekitd_uid_t
public let variable_type: sourcekitd_uid_t
public let relatedidents: sourcekitd_uid_t
public let semantic_refactoring: sourcekitd_uid_t

Expand All @@ -169,6 +180,7 @@ public struct sourcekitd_requests {
codecomplete_close = api.uid_get_from_cstr("source.request.codecomplete.close")!
cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")!
expression_type = api.uid_get_from_cstr("source.request.expression.type")!
variable_type = api.uid_get_from_cstr("source.request.variable.type")!
relatedidents = api.uid_get_from_cstr("source.request.relatedidents")!
semantic_refactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")!
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SourceKitLSP/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ target_sources(SourceKitLSP PRIVATE
Swift/SourceKitD+ResponseError.swift
Swift/SwiftCommand.swift
Swift/SwiftLanguageServer.swift)
Swift/VariableTypeInfo.swift
set_target_properties(SourceKitLSP PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
# TODO(compnerd) reduce the exposure here, why is everything PUBLIC-ly linked?
Expand Down
76 changes: 14 additions & 62 deletions Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1055,77 +1055,29 @@ extension SwiftLanguageServer {
}

public func inlayHints(_ req: Request<InlayHintsRequest>) {
// TODO: Introduce a new SourceKit request for inlay hints
// instead of computing them from document symbols here.

guard req.params.only?.contains(.type) ?? true else {
req.reply([])
return
}

let uri = req.params.textDocument.uri
documentSymbols(uri) { symbolsResult in
variableTypeInfos(uri, req.params.range) { infosResult in
do {
/// Filters all the document symbols for which inlay type hints
/// should be displayed, i.e. variable bindings, fields and properties.
func bindings(_ symbols: [DocumentSymbol]) -> [DocumentSymbol] {
symbols
.flatMap { bindings($0.children ?? []) + ([.variable, .field, .property].contains($0.kind) ? [$0] : []) }
}

let symbols = try symbolsResult.get()
let bindingPositions = Set(bindings(symbols).map { $0.range.upperBound })

self.expressionTypeInfos(uri) { infosResult in
do {
let infos = try infosResult.get()

// The unfiltered infos may contain multiple infos for a single position
// as the ending position does not necessarily identify an expression uniquely.
// Consider the following example:
//
// var x = "abc" + "def"
//
// Both `"abc" + "def"` and `"def"` are matching expressions. Since we are only
// interested in the first expression, i.e. the one that corresponds to the
// bound expression, we have to do some pre-processing here. Note that this
// mechanism currently relies on the outermost expression being reported first.

var visitedPositions: Set<Position> = []
var processedInfos: [ExpressionTypeInfo] = []

// TODO: Compute inlay hints only for the requested range/categories
// instead of filtering them afterwards.

for info in infos {
let pos = info.range.upperBound
if (req.params.range?.contains(pos) ?? true)
&& bindingPositions.contains(pos)
&& !visitedPositions.contains(pos) {
processedInfos.append(info)
visitedPositions.insert(pos)
}
}

let hints = processedInfos
.lazy
.map { info in
InlayHint(
position: info.range.upperBound,
category: .type,
label: info.printedType
)
}

req.reply(.success(Array(hints)))
} catch {
let message = "expression types for inlay hints failed for \(uri): \(error)"
log(message, level: .warning)
req.reply(.failure(.unknown(message)))
let infos = try infosResult.get()
let hints = infos
.lazy
.filter { !$0.hasExplicitType }
.map { info in
InlayHint(
position: info.range.upperBound,
category: .type,
label: info.printedType
)
}
}

req.reply(.success(Array(hints)))
} catch {
let message = "document symbols for inlay hints failed for \(uri): \(error)"
let message = "variable types for inlay hints failed for \(uri): \(error)"
log(message, level: .warning)
req.reply(.failure(.unknown(message)))
}
Expand Down
125 changes: 125 additions & 0 deletions Sources/SourceKitLSP/Swift/VariableTypeInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2021 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 Dispatch
import LanguageServerProtocol
import SourceKitD

/// A typed variable as returned by sourcekitd's CollectVariableType.
struct VariableTypeInfo {
/// Range of the variable identifier in the source file.
var range: Range<Position>
/// The printed type of the variable.
var printedType: String
/// Whether the variable has an explicit type annotation in the source file.
var hasExplicitType: Bool

init?(_ dict: SKDResponseDictionary, in snapshot: DocumentSnapshot) {
let keys = dict.sourcekitd.keys

guard let offset: Int = dict[keys.variable_offset],
let length: Int = dict[keys.variable_length],
let startIndex = snapshot.positionOf(utf8Offset: offset),
let endIndex = snapshot.positionOf(utf8Offset: offset + length),
let printedType: String = dict[keys.variable_type],
let hasExplicitType: Bool = dict[keys.variable_type_explicit] else {
return nil
}

self.range = startIndex..<endIndex
self.printedType = printedType
self.hasExplicitType = hasExplicitType
}
}

enum VariableTypeInfoError: Error, Equatable {
/// The given URL is not a known document.
case unknownDocument(DocumentURI)

/// The underlying sourcekitd request failed with the given error.
case responseError(ResponseError)
}

extension SwiftLanguageServer {
/// Must be called on self.queue.
private func _variableTypeInfos(
_ uri: DocumentURI,
_ range: Range<Position>? = nil,
_ completion: @escaping (Swift.Result<[VariableTypeInfo], VariableTypeInfoError>) -> Void
) {
dispatchPrecondition(condition: .onQueue(queue))

guard let snapshot = documentManager.latestSnapshot(uri) else {
return completion(.failure(.unknownDocument(uri)))
}

let keys = self.keys

let skreq = SKDRequestDictionary(sourcekitd: sourcekitd)
skreq[keys.request] = requests.variable_type
skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath

if let range = range,
let start = snapshot.utf8Offset(of: range.lowerBound),
let end = snapshot.utf8Offset(of: range.upperBound) {
skreq[keys.offset] = start
skreq[keys.length] = end - start
}

// FIXME: SourceKit should probably cache this for us
if let compileCommand = self.commandsByFile[uri] {
skreq[keys.compilerargs] = compileCommand.compilerArgs
}

let handle = self.sourcekitd.send(skreq, self.queue) { result in
guard let dict = result.success else {
return completion(.failure(.responseError(ResponseError(result.failure!))))
}

guard let skVariableTypeInfos: SKDResponseArray = dict[keys.variable_type_list] else {
return completion(.success([]))
}

var variableTypeInfos: [VariableTypeInfo] = []
variableTypeInfos.reserveCapacity(skVariableTypeInfos.count)

skVariableTypeInfos.forEach { (_, skVariableTypeInfo) -> Bool in
guard let info = VariableTypeInfo(skVariableTypeInfo, in: snapshot) else {
assertionFailure("VariableTypeInfo failed to deserialize")
return true
}
variableTypeInfos.append(info)
return true
}

completion(.success(variableTypeInfos))
}

// FIXME: cancellation
_ = handle
}

/// Provides typed variable declarations in a document.
///
/// - Parameters:
/// - url: Document URL in which to perform the request. Must be an open document.
/// - completion: Completion block to asynchronously receive the VariableTypeInfos, or error.
func variableTypeInfos(
_ uri: DocumentURI,
_ range: Range<Position>? = nil,
_ completion: @escaping (Swift.Result<[VariableTypeInfo], VariableTypeInfoError>) -> Void
) {
queue.async {
self._variableTypeInfos(uri, range, completion)
}
}
}
Loading