Skip to content

Add server-side support for inlay hints using CollectExpressionType #406

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 2 commits into from
Jun 15, 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
2 changes: 2 additions & 0 deletions Sources/LanguageServerProtocol/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ add_library(LanguageServerProtocol
Requests/HoverRequest.swift
Requests/ImplementationRequest.swift
Requests/InitializeRequest.swift
Requests/InlayHintsRequest.swift
Requests/PollIndexRequest.swift
Requests/ReferencesRequest.swift
Requests/RegisterCapabilityRequest.swift
Expand All @@ -63,6 +64,7 @@ add_library(LanguageServerProtocol
SupportTypes/FileEvent.swift
SupportTypes/FileSystemWatcher.swift
SupportTypes/FoldingRangeKind.swift
SupportTypes/InlayHint.swift
SupportTypes/Language.swift
SupportTypes/Location.swift
SupportTypes/LocationLink.swift
Expand Down
1 change: 1 addition & 0 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public let builtinRequests: [_RequestType.Type] = [

SymbolInfoRequest.self,
PollIndexRequest.self,
InlayHintsRequest.self,
]

/// The set of known notifications.
Expand Down
47 changes: 47 additions & 0 deletions Sources/LanguageServerProtocol/Requests/InlayHintsRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Request for inline annotations to be displayed in the editor **(LSP Extension)**.
///
/// This implements the proposed `textDocument/inlayHints` API from
/// https://github.com/microsoft/language-server-protocol/pull/1249 (commit: `d55733d`)
///
/// - Parameters:
/// - textDocument: The document for which to provide the inlay hints.
///
/// - Returns: InlayHints for the entire document
public struct InlayHintsRequest: TextDocumentRequest, Hashable {
public static let method: String = "sourcekit-lsp/inlayHints"
public typealias Response = [InlayHint]

/// The document for which to provide the inlay hints.
public var textDocument: TextDocumentIdentifier

/// The range the inlay hints are requested for. If nil,
/// hints for the entire document are requested.
@CustomCodable<PositionRange?>
public var range: Range<Position>?

/// The categories of hints that are interesting to the client
/// and should be filtered.
public var only: [InlayHintCategory]?

public init(
textDocument: TextDocumentIdentifier,
range: Range<Position>? = nil,
only: [InlayHintCategory]? = nil
) {
self.textDocument = textDocument
self._range = CustomCodable(wrappedValue: range)
self.only = only
}
}
48 changes: 48 additions & 0 deletions Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Represents an inline annotation displayed by the editor in a source file.
public struct InlayHint: ResponseType, Codable, Hashable {
/// The position within the code that this hint is attached to.
public var position: Position

/// The hint's kind, used for more flexible client-side styling.
public let category: InlayHintCategory?

/// The hint's text, e.g. a printed type
public let label: String

public init(
position: Position,
category: InlayHintCategory? = nil,
label: String
) {
self.position = position
self.category = category
self.label = label
}
}

/// A hint's kind, used for more flexible client-side styling.
public struct InlayHintCategory: RawRepresentable, Codable, Hashable {
public var rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}

/// A parameter label. Note that this case is not used by
/// Swift, since Swift already has explicit parameter labels.
public static let parameter: InlayHintCategory = InlayHintCategory(rawValue: "parameter")
/// An inferred type.
public static let type: InlayHintCategory = InlayHintCategory(rawValue: "type")
}
10 changes: 10 additions & 0 deletions Sources/SourceKitD/sourcekitd_uids.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public struct sourcekitd_keys {
public let educational_note_paths: sourcekitd_uid_t
public let endcolumn: sourcekitd_uid_t
public let endline: sourcekitd_uid_t
public let expression_length: sourcekitd_uid_t
public let expression_offset: sourcekitd_uid_t
public let expression_type: sourcekitd_uid_t
public let expression_type_list: sourcekitd_uid_t
public let filepath: sourcekitd_uid_t
public let fixits: sourcekitd_uid_t
public let id: sourcekitd_uid_t
Expand Down Expand Up @@ -94,6 +98,10 @@ public struct sourcekitd_keys {
educational_note_paths = api.uid_get_from_cstr("key.educational_note_paths")!
endcolumn = api.uid_get_from_cstr("key.endcolumn")!
endline = api.uid_get_from_cstr("key.endline")!
expression_length = api.uid_get_from_cstr("key.expression_length")!
expression_offset = api.uid_get_from_cstr("key.expression_offset")!
expression_type = api.uid_get_from_cstr("key.expression_type")!
expression_type_list = api.uid_get_from_cstr("key.expression_type_list")!
filepath = api.uid_get_from_cstr("key.filepath")!
fixits = api.uid_get_from_cstr("key.fixits")!
id = api.uid_get_from_cstr("key.id")!
Expand Down Expand Up @@ -146,6 +154,7 @@ public struct sourcekitd_requests {
public let codecomplete_update: sourcekitd_uid_t
public let codecomplete_close: sourcekitd_uid_t
public let cursorinfo: sourcekitd_uid_t
public let expression_type: sourcekitd_uid_t
public let relatedidents: sourcekitd_uid_t
public let semantic_refactoring: sourcekitd_uid_t

Expand All @@ -159,6 +168,7 @@ public struct sourcekitd_requests {
codecomplete_update = api.uid_get_from_cstr("source.request.codecomplete.update")!
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")!
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 @@ -24,6 +24,7 @@ target_sources(SourceKitLSP PRIVATE
Swift/CursorInfo.swift
Swift/Diagnostic.swift
Swift/EditorPlaceholder.swift
Swift/ExpressionTypeInfo.swift
Swift/SemanticRefactorCommand.swift
Swift/SemanticRefactoring.swift
Swift/SourceKitD+ResponseError.swift
Expand Down
7 changes: 7 additions & 0 deletions Sources/SourceKitLSP/Clang/ClangLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,13 @@ extension ClangLanguageServerShim {
forwardRequestToClangdOnQueue(req)
}

func inlayHints(_ req: Request<InlayHintsRequest>) {
// FIXME: Currently a Swift-specific, non-standard request.
// Once inlay hints have been upstreamed to LSP, forward
// them to clangd.
req.reply(.success([]))
}

func foldingRange(_ req: Request<FoldingRangeRequest>) {
queue.async {
if self.capabilities?.foldingRangeProvider?.isSupported == true {
Expand Down
9 changes: 9 additions & 0 deletions Sources/SourceKitLSP/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public final class SourceKitServer: LanguageServer {
registerToolchainTextDocumentRequest(SourceKitServer.documentSemanticTokensRange, nil)
registerToolchainTextDocumentRequest(SourceKitServer.colorPresentation, [])
registerToolchainTextDocumentRequest(SourceKitServer.codeAction, nil)
registerToolchainTextDocumentRequest(SourceKitServer.inlayHints, [])
}

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

func inlayHints(
_ req: Request<InlayHintsRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) {
languageService.inlayHints(req)
}

func definition(
_ req: Request<DefinitionRequest>,
workspace: Workspace,
Expand Down
115 changes: 115 additions & 0 deletions Sources/SourceKitLSP/Swift/ExpressionTypeInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//===----------------------------------------------------------------------===//
//
// 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 expression as returned by sourcekitd's CollectExpressionType.
///
/// A detailed description of the structure returned by sourcekitd can be found
/// here: https://github.com/apple/swift/blob/main/tools/SourceKit/docs/Protocol.md#expression-type
struct ExpressionTypeInfo {
/// Range of the expression in the source file.
var range: Range<Position>
/// The printed type of the expression.
var printedType: String

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

guard let offset: Int = dict[keys.expression_offset],
let length: Int = dict[keys.expression_length],
let startIndex = snapshot.positionOf(utf8Offset: offset),
let endIndex = snapshot.positionOf(utf8Offset: offset + length),
let printedType: String = dict[keys.expression_type] else {
return nil
}

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

enum ExpressionTypeInfoError: 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 _expressionTypeInfos(
_ uri: DocumentURI,
_ completion: @escaping (Swift.Result<[ExpressionTypeInfo], ExpressionTypeInfoError>) -> 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.expression_type
skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath

// 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 skExpressionTypeInfos: SKDResponseArray = dict[keys.expression_type_list] else {
return completion(.success([]))
}

var expressionTypeInfos: [ExpressionTypeInfo] = []
expressionTypeInfos.reserveCapacity(skExpressionTypeInfos.count)

skExpressionTypeInfos.forEach { (_, skExpressionTypeInfo) -> Bool in
guard let info = ExpressionTypeInfo(skExpressionTypeInfo, in: snapshot) else {
assertionFailure("ExpressionTypeInfo failed to deserialize")
return true
}
expressionTypeInfos.append(info)
return true
}

completion(.success(expressionTypeInfos))
}

// FIXME: cancellation
_ = handle
}

/// Provides typed expressions 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 ExpressionTypeInfos, or error.
func expressionTypeInfos(
_ uri: DocumentURI,
_ completion: @escaping (Swift.Result<[ExpressionTypeInfo], ExpressionTypeInfoError>) -> Void
) {
queue.async {
self._expressionTypeInfos(uri, completion)
}
}
}
Loading