Skip to content

Commit 3fef514

Browse files
committed
Add support for inlay hints using CollectExpressionType
- Add UID for CollectExpressionType request - Add ExpressionTypeInfo structure - Add keys to support sourcekitd's CollectExpressionType - Implement CollectExpressionType request - Add SwiftLanguageServer.expressionTypeInfos - Add InlayHint and supporting types - Add InlayHintsRequest - Add inlayHints handler stub - Implement inlay hints request - Update InlayHint to follow the current proposal - # This is the commit message #11: - ...as described in the LSP proposal - Update doc comment on InlayHintsRequest - Map inlay hints lazily - Fix minor style issue - Add new files to CMakeLists.txt - Specify commit of the current inlay hints proposal state - Add public, memberwise initializer for InlayHintsRequest - assert(false) if deserializing ExpressionTypeInfos fails - Add dispatch precondition to _expressionTypeInfos - Add InlayHintsRequest to the builtinRequests - Factor out function for querying document symbols for URI - Only render inlay hints after variable bindings - Test inlay hints on empty document - Test inlay hints for some simple bindings - Test ranged inlay hint requests - Make sure that inlay hints are unique per position - Test inlay hints for fields - Apply various PR suggestions regarding inlay hints - Update inlay hint tests and add case with explicit type annotations - Continue iterating if an ExpressionTypeInfo fails to deserialize
1 parent 09da6a4 commit 3fef514

File tree

13 files changed

+610
-68
lines changed

13 files changed

+610
-68
lines changed

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ add_library(LanguageServerProtocol
4141
Requests/HoverRequest.swift
4242
Requests/ImplementationRequest.swift
4343
Requests/InitializeRequest.swift
44+
Requests/InlayHintsRequest.swift
4445
Requests/PollIndexRequest.swift
4546
Requests/ReferencesRequest.swift
4647
Requests/RegisterCapabilityRequest.swift
@@ -63,6 +64,7 @@ add_library(LanguageServerProtocol
6364
SupportTypes/FileEvent.swift
6465
SupportTypes/FileSystemWatcher.swift
6566
SupportTypes/FoldingRangeKind.swift
67+
SupportTypes/InlayHint.swift
6668
SupportTypes/Language.swift
6769
SupportTypes/Location.swift
6870
SupportTypes/LocationLink.swift

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public let builtinRequests: [_RequestType.Type] = [
4747

4848
SymbolInfoRequest.self,
4949
PollIndexRequest.self,
50+
InlayHintsRequest.self,
5051
]
5152

5253
/// The set of known notifications.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
/// Request for inline annotations to be displayed in the editor **(LSP Extension)**.
14+
///
15+
/// This implements the proposed `textDocument/inlayHints` API from
16+
/// https://github.com/microsoft/language-server-protocol/pull/1249 (commit: `d55733d`)
17+
///
18+
/// - Parameters:
19+
/// - textDocument: The document for which to provide the inlay hints.
20+
///
21+
/// - Returns: InlayHints for the entire document
22+
public struct InlayHintsRequest: TextDocumentRequest, Hashable {
23+
public static let method: String = "sourcekit-lsp/inlayHints"
24+
public typealias Response = [InlayHint]
25+
26+
/// The document for which to provide the inlay hints.
27+
public var textDocument: TextDocumentIdentifier
28+
29+
/// The range the inlay hints are requested for. If nil,
30+
/// hints for the entire document are requested.
31+
@CustomCodable<PositionRange?>
32+
public var range: Range<Position>?
33+
34+
/// The categories of hints that are interesting to the client
35+
/// and should be filtered.
36+
public var only: [InlayHintCategory]?
37+
38+
public init(
39+
textDocument: TextDocumentIdentifier,
40+
range: Range<Position>? = nil,
41+
only: [InlayHintCategory]? = nil
42+
) {
43+
self.textDocument = textDocument
44+
self._range = CustomCodable(wrappedValue: range)
45+
self.only = only
46+
}
47+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
/// Represents an inline annotation displayed by the editor in a source file.
14+
public struct InlayHint: ResponseType, Codable, Hashable {
15+
/// The position within the code that this hint is attached to.
16+
public var position: Position
17+
18+
/// The hint's kind, used for more flexible client-side styling.
19+
public let category: InlayHintCategory?
20+
21+
/// The hint's text, e.g. a printed type
22+
public let label: String
23+
24+
public init(
25+
position: Position,
26+
category: InlayHintCategory? = nil,
27+
label: String
28+
) {
29+
self.position = position
30+
self.category = category
31+
self.label = label
32+
}
33+
}
34+
35+
/// A hint's kind, used for more flexible client-side styling.
36+
public struct InlayHintCategory: RawRepresentable, Codable, Hashable {
37+
public var rawValue: String
38+
39+
public init(rawValue: String) {
40+
self.rawValue = rawValue
41+
}
42+
43+
/// A parameter label. Note that this case is not used by
44+
/// Swift, since Swift already has explicit parameter labels.
45+
public static let parameter: InlayHintCategory = InlayHintCategory(rawValue: "parameter")
46+
/// An inferred type.
47+
public static let type: InlayHintCategory = InlayHintCategory(rawValue: "type")
48+
}

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public struct sourcekitd_keys {
3333
public let educational_note_paths: sourcekitd_uid_t
3434
public let endcolumn: sourcekitd_uid_t
3535
public let endline: sourcekitd_uid_t
36+
public let expression_length: sourcekitd_uid_t
37+
public let expression_offset: sourcekitd_uid_t
38+
public let expression_type: sourcekitd_uid_t
39+
public let expression_type_list: sourcekitd_uid_t
3640
public let filepath: sourcekitd_uid_t
3741
public let fixits: sourcekitd_uid_t
3842
public let id: sourcekitd_uid_t
@@ -94,6 +98,10 @@ public struct sourcekitd_keys {
9498
educational_note_paths = api.uid_get_from_cstr("key.educational_note_paths")!
9599
endcolumn = api.uid_get_from_cstr("key.endcolumn")!
96100
endline = api.uid_get_from_cstr("key.endline")!
101+
expression_length = api.uid_get_from_cstr("key.expression_length")!
102+
expression_offset = api.uid_get_from_cstr("key.expression_offset")!
103+
expression_type = api.uid_get_from_cstr("key.expression_type")!
104+
expression_type_list = api.uid_get_from_cstr("key.expression_type_list")!
97105
filepath = api.uid_get_from_cstr("key.filepath")!
98106
fixits = api.uid_get_from_cstr("key.fixits")!
99107
id = api.uid_get_from_cstr("key.id")!
@@ -146,6 +154,7 @@ public struct sourcekitd_requests {
146154
public let codecomplete_update: sourcekitd_uid_t
147155
public let codecomplete_close: sourcekitd_uid_t
148156
public let cursorinfo: sourcekitd_uid_t
157+
public let expression_type: sourcekitd_uid_t
149158
public let relatedidents: sourcekitd_uid_t
150159
public let semantic_refactoring: sourcekitd_uid_t
151160

@@ -159,6 +168,7 @@ public struct sourcekitd_requests {
159168
codecomplete_update = api.uid_get_from_cstr("source.request.codecomplete.update")!
160169
codecomplete_close = api.uid_get_from_cstr("source.request.codecomplete.close")!
161170
cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")!
171+
expression_type = api.uid_get_from_cstr("source.request.expression.type")!
162172
relatedidents = api.uid_get_from_cstr("source.request.relatedidents")!
163173
semantic_refactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")!
164174
}

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ target_sources(SourceKitLSP PRIVATE
2424
Swift/CursorInfo.swift
2525
Swift/Diagnostic.swift
2626
Swift/EditorPlaceholder.swift
27+
Swift/ExpressionTypeInfo.swift
2728
Swift/SemanticRefactorCommand.swift
2829
Swift/SemanticRefactoring.swift
2930
Swift/SourceKitD+ResponseError.swift

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,13 @@ extension ClangLanguageServerShim {
490490
forwardRequestToClangdOnQueue(req)
491491
}
492492

493+
func inlayHints(_ req: Request<InlayHintsRequest>) {
494+
// FIXME: Currently a Swift-specific, non-standard request.
495+
// Once inlay hints have been upstreamed to LSP, forward
496+
// them to clangd.
497+
req.reply(.success([]))
498+
}
499+
493500
func foldingRange(_ req: Request<FoldingRangeRequest>) {
494501
queue.async {
495502
if self.capabilities?.foldingRangeProvider?.isSupported == true {

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public final class SourceKitServer: LanguageServer {
9999
registerToolchainTextDocumentRequest(SourceKitServer.documentSemanticTokensRange, nil)
100100
registerToolchainTextDocumentRequest(SourceKitServer.colorPresentation, [])
101101
registerToolchainTextDocumentRequest(SourceKitServer.codeAction, nil)
102+
registerToolchainTextDocumentRequest(SourceKitServer.inlayHints, [])
102103
}
103104

104105
/// Register a `TextDocumentRequest` that requires a valid `Workspace`, `ToolchainLanguageServer`,
@@ -942,6 +943,14 @@ extension SourceKitServer {
942943
languageService.codeAction(request)
943944
}
944945

946+
func inlayHints(
947+
_ req: Request<InlayHintsRequest>,
948+
workspace: Workspace,
949+
languageService: ToolchainLanguageServer
950+
) {
951+
languageService.inlayHints(req)
952+
}
953+
945954
func definition(
946955
_ req: Request<DefinitionRequest>,
947956
workspace: Workspace,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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 expression as returned by sourcekitd's CollectExpressionType.
18+
///
19+
/// A detailed description of the structure returned by sourcekitd can be found
20+
/// here: https://github.com/apple/swift/blob/main/tools/SourceKit/docs/Protocol.md#expression-type
21+
struct ExpressionTypeInfo {
22+
/// Range of the expression in the source file.
23+
var range: Range<Position>
24+
/// The printed type of the expression.
25+
var printedType: String
26+
27+
init?(_ dict: SKDResponseDictionary, in snapshot: DocumentSnapshot) {
28+
let keys = dict.sourcekitd.keys
29+
30+
guard let offset: Int = dict[keys.expression_offset],
31+
let length: Int = dict[keys.expression_length],
32+
let startIndex = snapshot.positionOf(utf8Offset: offset),
33+
let endIndex = snapshot.positionOf(utf8Offset: offset + length),
34+
let printedType: String = dict[keys.expression_type] else {
35+
return nil
36+
}
37+
38+
self.range = startIndex..<endIndex
39+
self.printedType = printedType
40+
}
41+
}
42+
43+
enum ExpressionTypeInfoError: Error, Equatable {
44+
/// The given URL is not a known document.
45+
case unknownDocument(DocumentURI)
46+
47+
/// The underlying sourcekitd request failed with the given error.
48+
case responseError(ResponseError)
49+
}
50+
51+
extension SwiftLanguageServer {
52+
/// Must be called on self.queue.
53+
private func _expressionTypeInfos(
54+
_ uri: DocumentURI,
55+
_ completion: @escaping (Swift.Result<[ExpressionTypeInfo], ExpressionTypeInfoError>) -> Void
56+
) {
57+
if #available(macOS 10.12, *) {
58+
dispatchPrecondition(condition: .onQueue(queue))
59+
}
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.expression_type
69+
skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath
70+
71+
// FIXME: SourceKit should probably cache this for us.
72+
if let compileCommand = self.commandsByFile[uri] {
73+
skreq[keys.compilerargs] = compileCommand.compilerArgs
74+
}
75+
76+
let handle = self.sourcekitd.send(skreq, self.queue) { result in
77+
guard let dict = result.success else {
78+
return completion(.failure(.responseError(ResponseError(result.failure!))))
79+
}
80+
81+
guard let skExpressionTypeInfos: SKDResponseArray = dict[keys.expression_type_list] else {
82+
return completion(.success([]))
83+
}
84+
85+
var expressionTypeInfos: [ExpressionTypeInfo] = []
86+
expressionTypeInfos.reserveCapacity(skExpressionTypeInfos.count)
87+
88+
skExpressionTypeInfos.forEach { (_, skExpressionTypeInfo) -> Bool in
89+
guard let info = ExpressionTypeInfo(skExpressionTypeInfo, in: snapshot) else {
90+
assert(false, "ExpressionTypeInfo failed to deserialize")
91+
return true
92+
}
93+
expressionTypeInfos.append(info)
94+
return true
95+
}
96+
97+
completion(.success(expressionTypeInfos))
98+
}
99+
100+
// FIXME: cancellation
101+
_ = handle
102+
}
103+
104+
/// Provides typed expressions in a document.
105+
///
106+
/// - Parameters:
107+
/// - url: Document URL in which to perform the request. Must be an open document.
108+
/// - completion: Completion block to asynchronously receive the ExpressionTypeInfos, or error.
109+
func expressionTypeInfos(
110+
_ uri: DocumentURI,
111+
_ completion: @escaping (Swift.Result<[ExpressionTypeInfo], ExpressionTypeInfoError>) -> Void
112+
) {
113+
queue.async {
114+
self._expressionTypeInfos(uri, completion)
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)