Skip to content

Commit bc8d336

Browse files
authored
Merge pull request #408 from fwcd/inlay-hints-collect-var-type
Implement server-side support for inlay hints using CollectVariableType
2 parents 5030c27 + e77b567 commit bc8d336

File tree

8 files changed

+195
-83
lines changed

8 files changed

+195
-83
lines changed

Editors/vscode/src/inlayHints.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ const hintStyle: InlayHintStyle = {
5353

5454
makeDecoration: (hint, converter) => ({
5555
range: converter.asRange({
56-
start: hint.position,
57-
end: { ...hint.position, character: hint.position.character + 1 } }
58-
),
56+
start: { ...hint.position, character: hint.position.character - 1 },
57+
end: hint.position
58+
}),
5959
renderOptions: {
6060
after: {
6161
// U+200C is a zero-width non-joiner to prevent the editor from

Sources/SourceKitD/SKDResponseDictionary.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public final class SKDResponseDictionary {
3232
public subscript(key: sourcekitd_uid_t?) -> Int? {
3333
return Int(sourcekitd.api.variant_dictionary_get_int64(dict, key))
3434
}
35+
public subscript(key: sourcekitd_uid_t?) -> Bool? {
36+
return sourcekitd.api.variant_dictionary_get_bool(dict, key)
37+
}
3538
public subscript(key: sourcekitd_uid_t?) -> sourcekitd_uid_t? {
3639
return sourcekitd.api.variant_dictionary_get_uid(dict, key)
3740
}

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public struct sourcekitd_keys {
6464
public let text: sourcekitd_uid_t
6565
public let typename: sourcekitd_uid_t
6666
public let usr: sourcekitd_uid_t
67+
public let variable_offset: sourcekitd_uid_t
68+
public let variable_length: sourcekitd_uid_t
69+
public let variable_type: sourcekitd_uid_t
70+
public let variable_type_explicit: sourcekitd_uid_t
71+
public let variable_type_list: sourcekitd_uid_t
6772

6873
// Code Completion options.
6974
public let codecomplete_options: sourcekitd_uid_t
@@ -129,6 +134,11 @@ public struct sourcekitd_keys {
129134
text = api.uid_get_from_cstr("key.text")!
130135
typename = api.uid_get_from_cstr("key.typename")!
131136
usr = api.uid_get_from_cstr("key.usr")!
137+
variable_offset = api.uid_get_from_cstr("key.variable_offset")!
138+
variable_length = api.uid_get_from_cstr("key.variable_length")!
139+
variable_type = api.uid_get_from_cstr("key.variable_type")!
140+
variable_type_explicit = api.uid_get_from_cstr("key.variable_type_explicit")!
141+
variable_type_list = api.uid_get_from_cstr("key.variable_type_list")!
132142

133143
// Code Completion options
134144
codecomplete_options = api.uid_get_from_cstr("key.codecomplete.options")!
@@ -155,6 +165,7 @@ public struct sourcekitd_requests {
155165
public let codecomplete_close: sourcekitd_uid_t
156166
public let cursorinfo: sourcekitd_uid_t
157167
public let expression_type: sourcekitd_uid_t
168+
public let variable_type: sourcekitd_uid_t
158169
public let relatedidents: sourcekitd_uid_t
159170
public let semantic_refactoring: sourcekitd_uid_t
160171

@@ -169,6 +180,7 @@ public struct sourcekitd_requests {
169180
codecomplete_close = api.uid_get_from_cstr("source.request.codecomplete.close")!
170181
cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")!
171182
expression_type = api.uid_get_from_cstr("source.request.expression.type")!
183+
variable_type = api.uid_get_from_cstr("source.request.variable.type")!
172184
relatedidents = api.uid_get_from_cstr("source.request.relatedidents")!
173185
semantic_refactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")!
174186
}

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ target_sources(SourceKitLSP PRIVATE
3030
Swift/SourceKitD+ResponseError.swift
3131
Swift/SwiftCommand.swift
3232
Swift/SwiftLanguageServer.swift)
33+
Swift/VariableTypeInfo.swift
3334
set_target_properties(SourceKitLSP PROPERTIES
3435
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
3536
# TODO(compnerd) reduce the exposure here, why is everything PUBLIC-ly linked?

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,77 +1055,29 @@ extension SwiftLanguageServer {
10551055
}
10561056

10571057
public func inlayHints(_ req: Request<InlayHintsRequest>) {
1058-
// TODO: Introduce a new SourceKit request for inlay hints
1059-
// instead of computing them from document symbols here.
1060-
10611058
guard req.params.only?.contains(.type) ?? true else {
10621059
req.reply([])
10631060
return
10641061
}
10651062

10661063
let uri = req.params.textDocument.uri
1067-
documentSymbols(uri) { symbolsResult in
1064+
variableTypeInfos(uri, req.params.range) { infosResult in
10681065
do {
1069-
/// Filters all the document symbols for which inlay type hints
1070-
/// should be displayed, i.e. variable bindings, fields and properties.
1071-
func bindings(_ symbols: [DocumentSymbol]) -> [DocumentSymbol] {
1072-
symbols
1073-
.flatMap { bindings($0.children ?? []) + ([.variable, .field, .property].contains($0.kind) ? [$0] : []) }
1074-
}
1075-
1076-
let symbols = try symbolsResult.get()
1077-
let bindingPositions = Set(bindings(symbols).map { $0.range.upperBound })
1078-
1079-
self.expressionTypeInfos(uri) { infosResult in
1080-
do {
1081-
let infos = try infosResult.get()
1082-
1083-
// The unfiltered infos may contain multiple infos for a single position
1084-
// as the ending position does not necessarily identify an expression uniquely.
1085-
// Consider the following example:
1086-
//
1087-
// var x = "abc" + "def"
1088-
//
1089-
// Both `"abc" + "def"` and `"def"` are matching expressions. Since we are only
1090-
// interested in the first expression, i.e. the one that corresponds to the
1091-
// bound expression, we have to do some pre-processing here. Note that this
1092-
// mechanism currently relies on the outermost expression being reported first.
1093-
1094-
var visitedPositions: Set<Position> = []
1095-
var processedInfos: [ExpressionTypeInfo] = []
1096-
1097-
// TODO: Compute inlay hints only for the requested range/categories
1098-
// instead of filtering them afterwards.
1099-
1100-
for info in infos {
1101-
let pos = info.range.upperBound
1102-
if (req.params.range?.contains(pos) ?? true)
1103-
&& bindingPositions.contains(pos)
1104-
&& !visitedPositions.contains(pos) {
1105-
processedInfos.append(info)
1106-
visitedPositions.insert(pos)
1107-
}
1108-
}
1109-
1110-
let hints = processedInfos
1111-
.lazy
1112-
.map { info in
1113-
InlayHint(
1114-
position: info.range.upperBound,
1115-
category: .type,
1116-
label: info.printedType
1117-
)
1118-
}
1119-
1120-
req.reply(.success(Array(hints)))
1121-
} catch {
1122-
let message = "expression types for inlay hints failed for \(uri): \(error)"
1123-
log(message, level: .warning)
1124-
req.reply(.failure(.unknown(message)))
1066+
let infos = try infosResult.get()
1067+
let hints = infos
1068+
.lazy
1069+
.filter { !$0.hasExplicitType }
1070+
.map { info in
1071+
InlayHint(
1072+
position: info.range.upperBound,
1073+
category: .type,
1074+
label: info.printedType
1075+
)
11251076
}
1126-
}
1077+
1078+
req.reply(.success(Array(hints)))
11271079
} catch {
1128-
let message = "document symbols for inlay hints failed for \(uri): \(error)"
1080+
let message = "variable types for inlay hints failed for \(uri): \(error)"
11291081
log(message, level: .warning)
11301082
req.reply(.failure(.unknown(message)))
11311083
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Dispatch
14+
import LanguageServerProtocol
15+
import SourceKitD
16+
17+
/// A typed variable as returned by sourcekitd's CollectVariableType.
18+
struct VariableTypeInfo {
19+
/// Range of the variable identifier in the source file.
20+
var range: Range<Position>
21+
/// The printed type of the variable.
22+
var printedType: String
23+
/// Whether the variable has an explicit type annotation in the source file.
24+
var hasExplicitType: Bool
25+
26+
init?(_ dict: SKDResponseDictionary, in snapshot: DocumentSnapshot) {
27+
let keys = dict.sourcekitd.keys
28+
29+
guard let offset: Int = dict[keys.variable_offset],
30+
let length: Int = dict[keys.variable_length],
31+
let startIndex = snapshot.positionOf(utf8Offset: offset),
32+
let endIndex = snapshot.positionOf(utf8Offset: offset + length),
33+
let printedType: String = dict[keys.variable_type],
34+
let hasExplicitType: Bool = dict[keys.variable_type_explicit] else {
35+
return nil
36+
}
37+
38+
self.range = startIndex..<endIndex
39+
self.printedType = printedType
40+
self.hasExplicitType = hasExplicitType
41+
}
42+
}
43+
44+
enum VariableTypeInfoError: Error, Equatable {
45+
/// The given URL is not a known document.
46+
case unknownDocument(DocumentURI)
47+
48+
/// The underlying sourcekitd request failed with the given error.
49+
case responseError(ResponseError)
50+
}
51+
52+
extension SwiftLanguageServer {
53+
/// Must be called on self.queue.
54+
private func _variableTypeInfos(
55+
_ uri: DocumentURI,
56+
_ range: Range<Position>? = nil,
57+
_ completion: @escaping (Swift.Result<[VariableTypeInfo], VariableTypeInfoError>) -> Void
58+
) {
59+
dispatchPrecondition(condition: .onQueue(queue))
60+
61+
guard let snapshot = documentManager.latestSnapshot(uri) else {
62+
return completion(.failure(.unknownDocument(uri)))
63+
}
64+
65+
let keys = self.keys
66+
67+
let skreq = SKDRequestDictionary(sourcekitd: sourcekitd)
68+
skreq[keys.request] = requests.variable_type
69+
skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath
70+
71+
if let range = range,
72+
let start = snapshot.utf8Offset(of: range.lowerBound),
73+
let end = snapshot.utf8Offset(of: range.upperBound) {
74+
skreq[keys.offset] = start
75+
skreq[keys.length] = end - start
76+
}
77+
78+
// FIXME: SourceKit should probably cache this for us
79+
if let compileCommand = self.commandsByFile[uri] {
80+
skreq[keys.compilerargs] = compileCommand.compilerArgs
81+
}
82+
83+
let handle = self.sourcekitd.send(skreq, self.queue) { result in
84+
guard let dict = result.success else {
85+
return completion(.failure(.responseError(ResponseError(result.failure!))))
86+
}
87+
88+
guard let skVariableTypeInfos: SKDResponseArray = dict[keys.variable_type_list] else {
89+
return completion(.success([]))
90+
}
91+
92+
var variableTypeInfos: [VariableTypeInfo] = []
93+
variableTypeInfos.reserveCapacity(skVariableTypeInfos.count)
94+
95+
skVariableTypeInfos.forEach { (_, skVariableTypeInfo) -> Bool in
96+
guard let info = VariableTypeInfo(skVariableTypeInfo, in: snapshot) else {
97+
assertionFailure("VariableTypeInfo failed to deserialize")
98+
return true
99+
}
100+
variableTypeInfos.append(info)
101+
return true
102+
}
103+
104+
completion(.success(variableTypeInfos))
105+
}
106+
107+
// FIXME: cancellation
108+
_ = handle
109+
}
110+
111+
/// Provides typed variable declarations in a document.
112+
///
113+
/// - Parameters:
114+
/// - url: Document URL in which to perform the request. Must be an open document.
115+
/// - completion: Completion block to asynchronously receive the VariableTypeInfos, or error.
116+
func variableTypeInfos(
117+
_ uri: DocumentURI,
118+
_ range: Range<Position>? = nil,
119+
_ completion: @escaping (Swift.Result<[VariableTypeInfo], VariableTypeInfoError>) -> Void
120+
) {
121+
queue.async {
122+
self._variableTypeInfos(uri, range, completion)
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)