Skip to content

Commit

Permalink
Migrate to upstream LSP inlay hints
Browse files Browse the repository at this point in the history
- Use official textDocument/inlayHint request
- Rename InlayHintCategory to InlayHintKind
- Additionally, represent it using an Int, as in the proposed LSP API.
- Add inlay hint client capabilities
- Add inlay hint server capabilities
- Add dynamic registration of inlay hint request
- Rename InlayHintsRequest -> InlayHintRequest
  This is to be consistent with the request itself being named in singular
  in LSP and the other requests (e.g. DocumentSymbolRequest).
- Forward inlay hint requests to clangd
- Add colon before inlay hints
- Add other properties to InlayHint
- Add InlayHintLabel structures
- Conform InlayHintLabel to ExpressibleByStringX protocols
- Attach TextEdit to inlay hints for committing them
- Add InlayHint.data
- Fix InlayHintTests
  We need to include text edits in the expected inlay hints.
  • Loading branch information
fwcd committed May 24, 2022
1 parent 6fa0771 commit 818c44d
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 85 deletions.
2 changes: 1 addition & 1 deletion Sources/LanguageServerProtocol/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ add_library(LanguageServerProtocol STATIC
Requests/HoverRequest.swift
Requests/ImplementationRequest.swift
Requests/InitializeRequest.swift
Requests/InlayHintsRequest.swift
Requests/InlayHintRequest.swift
Requests/PollIndexRequest.swift
Requests/PrepareRenameRequest.swift
Requests/ReferencesRequest.swift
Expand Down
2 changes: 1 addition & 1 deletion Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ public let builtinRequests: [_RequestType.Type] = [
RenameRequest.self,
RegisterCapabilityRequest.self,
UnregisterCapabilityRequest.self,
InlayHintRequest.self,

// MARK: LSP Extension Requests

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

/// The set of known notifications.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
//
//===----------------------------------------------------------------------===//

/// Request for inline annotations to be displayed in the editor **(LSP Extension)**.
/// Request for inline annotations to be displayed in the editor.
///
/// This implements the proposed `textDocument/inlayHints` API from
/// This implements the proposed `textDocument/inlayHint` 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 struct InlayHintRequest: TextDocumentRequest, Hashable {
public static let method: String = "textDocument/inlayHint"
public typealias Response = [InlayHint]

/// The document for which to provide the inlay hints.
Expand All @@ -33,12 +33,12 @@ public struct InlayHintsRequest: TextDocumentRequest, Hashable {

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

public init(
textDocument: TextDocumentIdentifier,
range: Range<Position>? = nil,
only: [InlayHintCategory]? = nil
only: [InlayHintKind]? = nil
) {
self.textDocument = textDocument
self._range = CustomCodable(wrappedValue: range)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,33 @@ public struct TextDocumentClientCapabilities: Hashable, Codable {
}
}

/// Capabilities specific to 'textDocument/inlayHint'.
public struct InlayHint: Hashable, Codable {
/// Properties a client can resolve lazily.
public struct ResolveSupport: Hashable, Codable {
/// The properties that a client can resolve lazily.
public var properties: [String]

public init(properties: [String] = []) {
self.properties = properties
}
}

/// Whether inlay hints support dynamic registration.
public var dynamicRegistration: Bool?

/// Indicates which properties a client can resolve lazily on an inlay hint.
public var resolveSupport: ResolveSupport?

public init(
dynamicRegistration: Bool? = nil,
resolveSupport: ResolveSupport? = nil
) {
self.dynamicRegistration = dynamicRegistration
self.resolveSupport = resolveSupport
}
}

// MARK: Properties

public var synchronization: Synchronization? = nil
Expand Down Expand Up @@ -547,6 +574,8 @@ public struct TextDocumentClientCapabilities: Hashable, Codable {

public var semanticTokens: SemanticTokens? = nil

public var inlayHint: InlayHint? = nil

public init(synchronization: Synchronization? = nil,
completion: Completion? = nil,
hover: Hover? = nil,
Expand All @@ -569,7 +598,8 @@ public struct TextDocumentClientCapabilities: Hashable, Codable {
publishDiagnostics: PublishDiagnostics? = nil,
foldingRange: FoldingRange? = nil,
callHierarchy: DynamicRegistrationCapability? = nil,
semanticTokens: SemanticTokens? = nil) {
semanticTokens: SemanticTokens? = nil,
inlayHint: InlayHint? = nil) {
self.synchronization = synchronization
self.completion = completion
self.hover = hover
Expand All @@ -593,5 +623,6 @@ public struct TextDocumentClientCapabilities: Hashable, Codable {
self.foldingRange = foldingRange
self.callHierarchy = callHierarchy
self.semanticTokens = semanticTokens
self.inlayHint = inlayHint
}
}
115 changes: 104 additions & 11 deletions Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,126 @@ public struct InlayHint: ResponseType, Codable, Hashable {
public var position: Position

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

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

/// Optional text edits that are performed when accepting this inlay hint.
public let textEdits: [TextEdit]?

/// The tooltip text displayed when the inlay hint is hovered.
public let tooltip: MarkupContent?

/// Whether to render padding before the hint.
public let paddingLeft: Bool?

/// Whether to render padding after the hint.
public let paddingRight: Bool?

/// A data entry field that is present between a `textDocument/inlayHint`
/// and a `inlayHint/resolve` request.
public let data: LSPAny?

public init(
position: Position,
category: InlayHintCategory? = nil,
label: String
kind: InlayHintKind? = nil,
label: InlayHintLabel,
textEdits: [TextEdit]? = nil,
tooltip: MarkupContent? = nil,
paddingLeft: Bool? = nil,
paddingRight: Bool? = nil,
data: LSPAny? = nil
) {
self.position = position
self.category = category
self.kind = kind
self.label = label
self.textEdits = textEdits
self.tooltip = tooltip
self.paddingLeft = paddingLeft
self.paddingRight = paddingRight
self.data = data
}
}

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

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

/// A type annotation.
public static let type: InlayHintKind = InlayHintKind(rawValue: 1)
/// 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")
public static let parameter: InlayHintKind = InlayHintKind(rawValue: 2)
}

/// A hint's label, either being a single string or a composition of parts.
public enum InlayHintLabel: Codable, Hashable {
case parts([InlayHintLabelPart])
case string(String)

public init(from decoder: Decoder) throws {
if let parts = try? [InlayHintLabelPart](from: decoder) {
self = .parts(parts)
} else if let string = try? String(from: decoder) {
self = .string(string)
} else {
let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected [InlayHintLabelPart] or String")
throw DecodingError.dataCorrupted(context)
}
}

public func encode(to encoder: Encoder) throws {
switch self {
case let .parts(parts):
try parts.encode(to: encoder)
case let .string(string):
try string.encode(to: encoder)
}
}
}

extension InlayHintLabel: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .string(value)
}
}

extension InlayHintLabel: ExpressibleByStringInterpolation {
public init(stringInterpolation interpolation: DefaultStringInterpolation) {
self = .string(.init(stringInterpolation: interpolation))
}
}

/// A part of an interactive or composite inlay hint label.
public struct InlayHintLabelPart: Codable, Hashable {
/// The value of this label part.
public let value: String

/// The tooltip to show when the part is hovered.
public let tooltip: MarkupContent?

/// An optional source code location representing this part.
/// Used by the editor for hover and code navigation, e.g.
/// by making the part a clickable link to the given position.
public let location: Location?

/// An optional command for this label part.
public let command: Command?

public init(
value: String,
tooltip: MarkupContent? = nil,
location: Location? = nil,
command: Command? = nil
) {
self.value = value
self.tooltip = tooltip
self.location = location
self.command = command
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ public struct SemanticTokensRegistrationOptions: RegistrationOptions, TextDocume
}
}

public struct InlayHintRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol, Hashable {
public var textDocumentRegistrationOptions: TextDocumentRegistrationOptions
public var inlayHintOptions: InlayHintOptions

public init(
documentSelector: DocumentSelector? = nil,
inlayHintOptions: InlayHintOptions
) {
textDocumentRegistrationOptions = TextDocumentRegistrationOptions(documentSelector: documentSelector)
self.inlayHintOptions = inlayHintOptions
}

public func encodeIntoLSPAny(dict: inout [String: LSPAny]) {
textDocumentRegistrationOptions.encodeIntoLSPAny(dict: &dict)
if let resolveProvider = inlayHintOptions.resolveProvider {
dict["resolveProvider"] = .bool(resolveProvider)
}
}
}

/// Describe options to be used when registering for file system change events.
public struct DidChangeWatchedFilesRegistrationOptions: RegistrationOptions {
/// The watchers to register.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public struct ServerCapabilities: Codable, Hashable {
/// requests.
public var semanticTokensProvider: SemanticTokensOptions?

/// Whether the server supports the `textDocument/inlayHint` family of requests.
public var inlayHintProvider: InlayHintOptions?

public var experimental: LSPAny?

public init(
Expand Down Expand Up @@ -117,6 +120,7 @@ public struct ServerCapabilities: Codable, Hashable {
workspace: WorkspaceServerCapabilities? = nil,
callHierarchyProvider: ValueOrBool<TextDocumentAndStaticRegistrationOptions>? = nil,
semanticTokensProvider: SemanticTokensOptions? = nil,
inlayHintProvider: InlayHintOptions? = nil,
experimental: LSPAny? = nil
)
{
Expand Down Expand Up @@ -145,6 +149,7 @@ public struct ServerCapabilities: Codable, Hashable {
self.workspace = workspace
self.callHierarchyProvider = callHierarchyProvider
self.semanticTokensProvider = semanticTokensProvider
self.inlayHintProvider = inlayHintProvider
self.experimental = experimental
}
}
Expand Down Expand Up @@ -505,6 +510,16 @@ public struct SemanticTokensOptions: Codable, Hashable {
}
}

public struct InlayHintOptions: Codable, Hashable {
/// The server provides support to resolve additional information
/// for an inlay hint item.
public var resolveProvider: Bool?

public init(resolveProvider: Bool? = nil) {
self.resolveProvider = resolveProvider
}
}

public struct WorkspaceServerCapabilities: Codable, Hashable {
public struct WorkspaceFolders: Codable, Hashable {
/// The server has support for workspace folders
Expand Down
35 changes: 35 additions & 0 deletions Sources/SourceKitLSP/CapabilityRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public final class CapabilityRegistry {
/// Dynamically registered semantic tokens options.
private var semanticTokens: [CapabilityRegistration: SemanticTokensRegistrationOptions] = [:]

/// Dynamically registered inlay hint options.
private var inlayHint: [CapabilityRegistration: InlayHintRegistrationOptions] = [:]

/// Dynamically registered file watchers.
private var didChangeWatchedFiles: DidChangeWatchedFilesRegistrationOptions?

Expand All @@ -53,6 +56,10 @@ public final class CapabilityRegistry {
clientCapabilities.textDocument?.semanticTokens?.dynamicRegistration == true
}

public var clientHasDynamicInlayHintRegistration: Bool {
clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true
}

public var clientHasDynamicExecuteCommandRegistration: Bool {
clientCapabilities.workspace?.executeCommand?.dynamicRegistration == true
}
Expand Down Expand Up @@ -167,6 +174,34 @@ public final class CapabilityRegistry {
registerOnClient(registration)
}

/// Dynamically register inlay hint capabilities if the client supports
/// it and we haven't yet registered any inlay hint capabilities for the
/// given languages.
public func registerInlayHintIfNeeded(
options: InlayHintOptions,
for languages: [Language],
registerOnClient: ClientRegistrationHandler
) {
guard clientHasDynamicInlayHintRegistration else { return }
if let registration = registration(for: languages, in: inlayHint) {
if options != registration.inlayHintOptions {
log("Unable to register new inlay hint options \(options) for " +
"\(languages) due to pre-existing options \(registration.inlayHintOptions)", level: .warning)
}
return
}
let registrationOptions = InlayHintRegistrationOptions(
documentSelector: self.documentSelector(for: languages),
inlayHintOptions: options)
let registration = CapabilityRegistration(
method: InlayHintRequest.method,
registerOptions: self.encode(registrationOptions))

self.inlayHint[registration] = registrationOptions

registerOnClient(registration)
}

/// Dynamically register executeCommand with the given IDs if the client supports
/// it and we haven't yet registered the given command IDs yet.
public func registerExecuteCommandIfNeeded(
Expand Down
7 changes: 2 additions & 5 deletions Sources/SourceKitLSP/Clang/ClangLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,11 +493,8 @@ 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 inlayHint(_ req: Request<InlayHintRequest>) {
forwardRequestToClangdOnQueue(req)
}

func foldingRange(_ req: Request<FoldingRangeRequest>) {
Expand Down
Loading

0 comments on commit 818c44d

Please sign in to comment.