diff --git a/Sources/LanguageServerProtocol/CMakeLists.txt b/Sources/LanguageServerProtocol/CMakeLists.txt index ba33f9b60..36fbe218e 100644 --- a/Sources/LanguageServerProtocol/CMakeLists.txt +++ b/Sources/LanguageServerProtocol/CMakeLists.txt @@ -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 diff --git a/Sources/LanguageServerProtocol/Messages.swift b/Sources/LanguageServerProtocol/Messages.swift index 3791b8c8f..240f9c242 100644 --- a/Sources/LanguageServerProtocol/Messages.swift +++ b/Sources/LanguageServerProtocol/Messages.swift @@ -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. diff --git a/Sources/LanguageServerProtocol/Requests/InlayHintsRequest.swift b/Sources/LanguageServerProtocol/Requests/InlayHintRequest.swift similarity index 83% rename from Sources/LanguageServerProtocol/Requests/InlayHintsRequest.swift rename to Sources/LanguageServerProtocol/Requests/InlayHintRequest.swift index ff4c732c5..a6c0ef1b3 100644 --- a/Sources/LanguageServerProtocol/Requests/InlayHintsRequest.swift +++ b/Sources/LanguageServerProtocol/Requests/InlayHintRequest.swift @@ -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. @@ -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? = nil, - only: [InlayHintCategory]? = nil + only: [InlayHintKind]? = nil ) { self.textDocument = textDocument self._range = CustomCodable(wrappedValue: range) diff --git a/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift b/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift index da958f48c..648c11653 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift @@ -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 @@ -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, @@ -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 @@ -593,5 +623,6 @@ public struct TextDocumentClientCapabilities: Hashable, Codable { self.foldingRange = foldingRange self.callHierarchy = callHierarchy self.semanticTokens = semanticTokens + self.inlayHint = inlayHint } } diff --git a/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift b/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift index fbfa1423c..239cb7307 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift @@ -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 + } } diff --git a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift index 99ec71945..cb1b19fd4 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift @@ -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. diff --git a/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift b/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift index b5fb6a15b..61caece52 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift @@ -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( @@ -117,6 +120,7 @@ public struct ServerCapabilities: Codable, Hashable { workspace: WorkspaceServerCapabilities? = nil, callHierarchyProvider: ValueOrBool? = nil, semanticTokensProvider: SemanticTokensOptions? = nil, + inlayHintProvider: InlayHintOptions? = nil, experimental: LSPAny? = nil ) { @@ -145,6 +149,7 @@ public struct ServerCapabilities: Codable, Hashable { self.workspace = workspace self.callHierarchyProvider = callHierarchyProvider self.semanticTokensProvider = semanticTokensProvider + self.inlayHintProvider = inlayHintProvider self.experimental = experimental } } @@ -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 diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index c6074397c..fa684b364 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -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? @@ -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 } @@ -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( diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift index 39646abb2..cee2f016b 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift @@ -493,11 +493,8 @@ extension ClangLanguageServerShim { forwardRequestToClangdOnQueue(req) } - func inlayHints(_ req: Request) { - // 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) { + forwardRequestToClangdOnQueue(req) } func foldingRange(_ req: Request) { diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 0b3ab07ec..44779f1b8 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -188,7 +188,7 @@ public final class SourceKitServer: LanguageServer { registerToolchainTextDocumentRequest(SourceKitServer.documentSemanticTokensRange, nil) registerToolchainTextDocumentRequest(SourceKitServer.colorPresentation, []) registerToolchainTextDocumentRequest(SourceKitServer.codeAction, nil) - registerToolchainTextDocumentRequest(SourceKitServer.inlayHints, []) + registerToolchainTextDocumentRequest(SourceKitServer.inlayHint, []) } /// Register a `TextDocumentRequest` that requires a valid `Workspace`, `ToolchainLanguageServer`, @@ -675,6 +675,11 @@ extension SourceKitServer { self.dynamicallyRegisterCapability($0, registry) } } + if let inlayHintOptions = server.inlayHintProvider { + registry.registerInlayHintIfNeeded(options: inlayHintOptions, for: languages) { + self.dynamicallyRegisterCapability($0, registry) + } + } if let commandOptions = server.executeCommandProvider { registry.registerExecuteCommandIfNeeded(commands: commandOptions.commands) { self.dynamicallyRegisterCapability($0, registry) @@ -1130,12 +1135,12 @@ extension SourceKitServer { languageService.codeAction(request) } - func inlayHints( - _ req: Request, + func inlayHint( + _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer ) { - languageService.inlayHints(req) + languageService.inlayHint(req) } func definition( diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index 71fbeb597..e4570a43a 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -382,7 +382,9 @@ extension SwiftLanguageServer { tokenTypes: SyntaxHighlightingToken.Kind.allCases.map(\.lspName), tokenModifiers: SyntaxHighlightingToken.Modifiers.allModifiers.map { $0.lspName! }), range: .bool(true), - full: .bool(true)) + full: .bool(true)), + inlayHintProvider: InlayHintOptions( + resolveProvider: false) )) } @@ -1220,7 +1222,7 @@ extension SwiftLanguageServer { completion(.success(codeActions)) } - public func inlayHints(_ req: Request) { + public func inlayHint(_ req: Request) { guard req.params.only?.contains(.type) ?? true else { req.reply([]) return @@ -1233,11 +1235,16 @@ extension SwiftLanguageServer { let hints = infos .lazy .filter { !$0.hasExplicitType } - .map { info in - InlayHint( - position: info.range.upperBound, - category: .type, - label: info.printedType + .map { info -> InlayHint in + let position = info.range.upperBound + let label = ": \(info.printedType)" + return InlayHint( + position: position, + kind: .type, + label: .string(label), + textEdits: [ + TextEdit(range: position..) func colorPresentation(_ req: Request) func codeAction(_ req: Request) - func inlayHints(_ req: Request) + func inlayHint(_ req: Request) // MARK: - Other diff --git a/Tests/SourceKitLSPTests/InlayHintsTests.swift b/Tests/SourceKitLSPTests/InlayHintTests.swift similarity index 69% rename from Tests/SourceKitLSPTests/InlayHintsTests.swift rename to Tests/SourceKitLSPTests/InlayHintTests.swift index 646da0dca..4d5a01328 100644 --- a/Tests/SourceKitLSPTests/InlayHintsTests.swift +++ b/Tests/SourceKitLSPTests/InlayHintTests.swift @@ -16,7 +16,7 @@ import SKTestSupport import SourceKitLSP import XCTest -final class InlayHintsTests: XCTestCase { +final class InlayHintTests: XCTestCase { /// Connection and lifetime management for the service. var connection: TestSourceKitServer! = nil @@ -42,7 +42,7 @@ final class InlayHintsTests: XCTestCase { )) } - func performInlayHintsRequest(text: String, range: Range? = nil) throws -> [InlayHint] { + func performInlayHintRequest(text: String, range: Range? = nil) throws -> [InlayHint] { let url = URL(fileURLWithPath: "/\(#function)/a.swift") sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( @@ -52,7 +52,7 @@ final class InlayHintsTests: XCTestCase { text: text ))) - let request = InlayHintsRequest(textDocument: TextDocumentIdentifier(url), range: range) + let request = InlayHintRequest(textDocument: TextDocumentIdentifier(url), range: range) do { return try sk.sendSync(request) @@ -61,9 +61,20 @@ final class InlayHintsTests: XCTestCase { } } + private func makeInlayHint(position: Position, kind: InlayHintKind, label: String) -> InlayHint { + InlayHint( + position: position, + kind: kind, + label: .string(label), + textEdits: [ + TextEdit(range: position.. ()" + kind: .type, + label: ": (Int) -> ()" ), - InlayHint( + makeInlayHint( position: Position(line: 3, utf16index: 31), - category: .type, - label: "String" + kind: .type, + label: ": String" ), - InlayHint( + makeInlayHint( position: Position(line: 4, utf16index: 40), - category: .type, - label: "Double" + kind: .type, + label: ": Double" ), - InlayHint( + makeInlayHint( position: Position(line: 4, utf16index: 43), - category: .type, - label: "Double" + kind: .type, + label: ": Double" ) ]) }