Skip to content

Commit 7a2d760

Browse files
authored
Merge pull request #406 from fwcd/inlay-hints
Add server-side support for inlay hints using CollectExpressionType
2 parents 09da6a4 + 29ff751 commit 7a2d760

File tree

13 files changed

+606
-68
lines changed

13 files changed

+606
-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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
dispatchPrecondition(condition: .onQueue(queue))
58+
59+
guard let snapshot = documentManager.latestSnapshot(uri) else {
60+
return completion(.failure(.unknownDocument(uri)))
61+
}
62+
63+
let keys = self.keys
64+
65+
let skreq = SKDRequestDictionary(sourcekitd: sourcekitd)
66+
skreq[keys.request] = requests.expression_type
67+
skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath
68+
69+
// FIXME: SourceKit should probably cache this for us.
70+
if let compileCommand = self.commandsByFile[uri] {
71+
skreq[keys.compilerargs] = compileCommand.compilerArgs
72+
}
73+
74+
let handle = self.sourcekitd.send(skreq, self.queue) { result in
75+
guard let dict = result.success else {
76+
return completion(.failure(.responseError(ResponseError(result.failure!))))
77+
}
78+
79+
guard let skExpressionTypeInfos: SKDResponseArray = dict[keys.expression_type_list] else {
80+
return completion(.success([]))
81+
}
82+
83+
var expressionTypeInfos: [ExpressionTypeInfo] = []
84+
expressionTypeInfos.reserveCapacity(skExpressionTypeInfos.count)
85+
86+
skExpressionTypeInfos.forEach { (_, skExpressionTypeInfo) -> Bool in
87+
guard let info = ExpressionTypeInfo(skExpressionTypeInfo, in: snapshot) else {
88+
assertionFailure("ExpressionTypeInfo failed to deserialize")
89+
return true
90+
}
91+
expressionTypeInfos.append(info)
92+
return true
93+
}
94+
95+
completion(.success(expressionTypeInfos))
96+
}
97+
98+
// FIXME: cancellation
99+
_ = handle
100+
}
101+
102+
/// Provides typed expressions in a document.
103+
///
104+
/// - Parameters:
105+
/// - url: Document URL in which to perform the request. Must be an open document.
106+
/// - completion: Completion block to asynchronously receive the ExpressionTypeInfos, or error.
107+
func expressionTypeInfos(
108+
_ uri: DocumentURI,
109+
_ completion: @escaping (Swift.Result<[ExpressionTypeInfo], ExpressionTypeInfoError>) -> Void
110+
) {
111+
queue.async {
112+
self._expressionTypeInfos(uri, completion)
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)