diff --git a/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift b/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift index d1d520559..dedab3055 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/InlayHint.swift @@ -68,8 +68,8 @@ public struct InlayHintKind: RawRepresentable, Codable, Hashable, Sendable { /// 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. + + /// A parameter label. public static let parameter: InlayHintKind = InlayHintKind(rawValue: 2) } diff --git a/Sources/SwiftLanguageService/CMakeLists.txt b/Sources/SwiftLanguageService/CMakeLists.txt index 5d8f66ba1..09aa4b475 100644 --- a/Sources/SwiftLanguageService/CMakeLists.txt +++ b/Sources/SwiftLanguageService/CMakeLists.txt @@ -1,51 +1,51 @@ add_library(SwiftLanguageService STATIC - SemanticRefactoring.swift - SwiftTestingScanner.swift - FoldingRange.swift - CMakeLists.txt - SymbolInfo.swift - RefactoringEdit.swift - CodeActions/ConvertStringConcatenationToStringInterpolation.swift - CodeActions/SyntaxRefactoringCodeActionProvider.swift + AdjustPositionToStartOfArgument.swift + AdjustPositionToStartOfIdentifier.swift + ClosureCompletionFormat.swift + CodeActions/AddDocumentation.swift + CodeActions/ConvertIntegerLiteral.swift CodeActions/ConvertJSONToCodableStruct.swift + CodeActions/ConvertStringConcatenationToStringInterpolation.swift + CodeActions/PackageManifestEdits.swift CodeActions/SyntaxCodeActionProvider.swift CodeActions/SyntaxCodeActions.swift - CodeActions/ConvertIntegerLiteral.swift - CodeActions/PackageManifestEdits.swift - CodeActions/AddDocumentation.swift - RefactoringResponse.swift - SyntacticSwiftXCTestScanner.swift + CodeActions/SyntaxRefactoringCodeActionProvider.swift + CodeCompletion.swift + CodeCompletionSession.swift CommentXML.swift - SymbolGraph.swift - ClosureCompletionFormat.swift - SemanticRefactorCommand.swift - DocumentFormatting.swift + CursorInfo.swift + Diagnostic.swift DiagnosticReportManager.swift - SemanticTokens.swift + DocumentFormatting.swift + DocumentSymbols.swift ExpandMacroCommand.swift - Diagnostic.swift - CodeCompletion.swift - SwiftCodeLensScanner.swift - SwiftCommand.swift + FoldingRange.swift + GeneratedInterfaceManager.swift + InlayHints.swift + MacroExpansion.swift + OpenInterface.swift + RefactoringEdit.swift + RefactoringResponse.swift RelatedIdentifiers.swift - VariableTypeInfo.swift - CodeCompletionSession.swift - SyntaxTreeManager.swift Rename.swift RewriteSourceKitPlaceholders.swift + SemanticRefactorCommand.swift + SemanticRefactoring.swift + SemanticTokens.swift + SignatureHelp.swift + SwiftCodeLensScanner.swift + SwiftCommand.swift SwiftLanguageService.swift - TestDiscovery.swift - OpenInterface.swift + SwiftTestingScanner.swift + SymbolGraph.swift + SymbolInfo.swift + SyntacticSwiftXCTestScanner.swift SyntaxHighlightingToken.swift - MacroExpansion.swift - AdjustPositionToStartOfIdentifier.swift SyntaxHighlightingTokenParser.swift - CursorInfo.swift - DocumentSymbols.swift SyntaxHighlightingTokens.swift - GeneratedInterfaceManager.swift - SignatureHelp.swift - AdjustPositionToStartOfArgument.swift + SyntaxTreeManager.swift + TestDiscovery.swift + VariableTypeInfo.swift ) set_target_properties(SwiftLanguageService PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/SwiftLanguageService/InlayHints.swift b/Sources/SwiftLanguageService/InlayHints.swift new file mode 100644 index 000000000..12f1d0147 --- /dev/null +++ b/Sources/SwiftLanguageService/InlayHints.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 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 +// +//===----------------------------------------------------------------------===// + +package import LanguageServerProtocol +import SourceKitLSP +import SwiftExtensions +import SwiftSyntax + +private class IfConfigCollector: SyntaxVisitor { + private var ifConfigDecls: [IfConfigDeclSyntax] = [] + + override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { + ifConfigDecls.append(node) + + return .visitChildren + } + + static func collectIfConfigDecls(in tree: some SyntaxProtocol) -> [IfConfigDeclSyntax] { + let visitor = IfConfigCollector(viewMode: .sourceAccurate) + visitor.walk(tree) + return visitor.ifConfigDecls + } +} + +extension SwiftLanguageService { + package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { + let uri = req.textDocument.uri + let infos = try await variableTypeInfos(uri, req.range) + let typeHints = infos + .lazy + .filter { !$0.hasExplicitType } + .map { info -> InlayHint in + let position = info.range.upperBound + let label = ": \(info.printedType)" + let textEdits: [TextEdit]? + if info.canBeFollowedByTypeAnnotation { + textEdits = [TextEdit(range: position.. InlayHint? in + // Do not show inlay hints for if config clauses that have a `#elseif` of `#else` clause since it is unclear which + // `#if`, `#elseif`, or `#else` clause the `#endif` now refers to. + guard let condition = ifConfigDecl.clauses.only?.condition else { + return nil + } + guard !ifConfigDecl.poundEndif.trailingTrivia.contains(where: { $0.isComment }) else { + // If a comment already exists (eg. because the user inserted it), don't show an inlay hint. + return nil + } + let hintPosition = snapshot.position(of: ifConfigDecl.poundEndif.endPositionBeforeTrailingTrivia) + let label = " // \(condition.trimmedDescription)" + return InlayHint( + position: hintPosition, + label: .string(label), + kind: .type, // For the lack of a better kind, pretend this comment is a type + textEdits: [TextEdit(range: Range(hintPosition), newText: label)], + tooltip: .string("Condition of this conditional compilation clause") + ) + } + + return Array(typeHints + ifConfigHints) + } +} diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index d889d8335..da8427a32 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -1032,32 +1032,6 @@ extension SwiftLanguageService { return codeActions } - package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { - let uri = req.textDocument.uri - let infos = try await variableTypeInfos(uri, req.range) - let hints = infos - .lazy - .filter { !$0.hasExplicitType } - .map { info -> InlayHint in - let position = info.range.upperBound - let label = ": \(info.printedType)" - let textEdits: [TextEdit]? - if info.canBeFollowedByTypeAnnotation { - textEdits = [TextEdit(range: position.. [CodeLens] { let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) var targetDisplayName: String? = nil diff --git a/Tests/SourceKitLSPTests/InlayHintTests.swift b/Tests/SourceKitLSPTests/InlayHintTests.swift index 50e443d32..c3c8ae327 100644 --- a/Tests/SourceKitLSPTests/InlayHintTests.swift +++ b/Tests/SourceKitLSPTests/InlayHintTests.swift @@ -18,14 +18,24 @@ import XCTest final class InlayHintTests: XCTestCase { // MARK: - Helpers - func performInlayHintRequest(text: String, range: Range? = nil) async throws -> [InlayHint] { + func performInlayHintRequest( + markedText: String, + range: (fromMarker: String, toMarker: String)? = nil + ) async throws -> (DocumentPositions, [InlayHint]) { let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI(for: .swift) + let (positions, text) = DocumentPositions.extract(from: markedText) testClient.openDocument(text, uri: uri) + let range: Range? = + if let range { + positions[range.fromMarker].. Double { - let result = x * x - return result - } + let (positions, hints) = try await performInlayHintRequest( + markedText: """ + func square(_ x: Double) -> Double { + let result = x * x + return result + } - func collatz(_ n: Int) -> Int { - let even = n % 2 == 0 - let result = even ? (n / 2) : (3 * n + 1) - return result - } - """ - let range = Position(line: 6, utf16index: 0).. Int { + 1️⃣ let even2️⃣ = n % 2 == 0 + let result3️⃣ = even ? (n / 2) : (3 * n + 1) + return result + } 4️⃣ + """, + range: ("1️⃣", "4️⃣") + ) XCTAssertEqual( hints, [ makeInlayHint( - position: Position(line: 6, utf16index: 10), + position: positions["2️⃣"], kind: .type, label: ": Bool" ), makeInlayHint( - position: Position(line: 7, utf16index: 12), + position: positions["3️⃣"], kind: .type, label: ": Int" ), @@ -112,47 +123,48 @@ final class InlayHintTests: XCTestCase { } func testFields() async throws { - let text = """ - class X { - let instanceMember = 3 - static let staticMember = "abc" - } + let (positions, hints) = try await performInlayHintRequest( + markedText: """ + class X { + let instanceMember1️⃣ = 3 + static let staticMember2️⃣ = "abc" + } - struct Y { - var instanceMember = "def" + "ghi" - static let staticMember = 1 + 2 - } + struct Y { + var instanceMember3️⃣ = "def" + "ghi" + static let staticMember4️⃣ = 1 + 2 + } - enum Z { - static let staticMember = 3.0 - } - """ - let hints = try await performInlayHintRequest(text: text) + enum Z { + static let staticMember5️⃣ = 3.0 + } + """ + ) XCTAssertEqual( hints, [ makeInlayHint( - position: Position(line: 1, utf16index: 20), + position: positions["1️⃣"], kind: .type, label: ": Int" ), makeInlayHint( - position: Position(line: 2, utf16index: 25), + position: positions["2️⃣"], kind: .type, label: ": String" ), makeInlayHint( - position: Position(line: 6, utf16index: 20), + position: positions["3️⃣"], kind: .type, label: ": String" ), makeInlayHint( - position: Position(line: 7, utf16index: 25), + position: positions["4️⃣"], kind: .type, label: ": Int" ), makeInlayHint( - position: Position(line: 11, utf16index: 25), + position: positions["5️⃣"], kind: .type, label: ": Double" ), @@ -161,53 +173,97 @@ final class InlayHintTests: XCTestCase { } func testExplicitTypeAnnotation() async throws { - let text = """ - let x: String = "abc" + let (_, hints) = try await performInlayHintRequest( + markedText: """ + let x: String = "abc" - struct X { - var y: Int = 34 - } - """ - let hints = try await performInlayHintRequest(text: text) + struct X { + var y: Int = 34 + } + """ + ) XCTAssertEqual(hints, []) } func testClosureParams() async throws { - let text = """ - func f(x: Int) {} + let (positions, hints) = try await performInlayHintRequest( + markedText: """ + func f(x: Int) {} - let g = { (x: Int) in } - let h: (String) -> String = { x in x } - let i: (Double, Double) -> Double = { (x, y) in - x + y - } - """ - let hints = try await performInlayHintRequest(text: text) + let g1️⃣ = { (x: Int) in } + let h: (String) -> String = { x2️⃣ in x } + let i: (Double, Double) -> Double = { (x3️⃣, y4️⃣) in + x + y + } + """ + ) XCTAssertEqual( hints, [ makeInlayHint( - position: Position(line: 2, utf16index: 5), + position: positions["1️⃣"], kind: .type, label: ": (Int) -> ()" ), makeInlayHint( - position: Position(line: 3, utf16index: 31), + position: positions["2️⃣"], kind: .type, label: ": String", hasEdit: false ), makeInlayHint( - position: Position(line: 4, utf16index: 40), + position: positions["3️⃣"], kind: .type, label: ": Double" ), makeInlayHint( - position: Position(line: 4, utf16index: 43), + position: positions["4️⃣"], kind: .type, label: ": Double" ), ] ) } + + func testIfConfigHints() async throws { + let (positions, hints) = try await performInlayHintRequest( + markedText: """ + #if DEBUG + #endif1️⃣ + """ + ) + XCTAssertEqual( + hints, + [ + InlayHint( + position: positions["1️⃣"], + label: " // DEBUG", + kind: .type, + textEdits: [TextEdit(range: Range(positions["1️⃣"]), newText: " // DEBUG")], + tooltip: .string("Condition of this conditional compilation clause") + ) + ] + ) + } + + func testIfConfigHintDoesNotShowIfCommentExits() async throws { + let (_, hints) = try await performInlayHintRequest( + markedText: """ + #if DEBUG + #endif // DEBUG + """ + ) + XCTAssertEqual(hints, []) + } + + func testIfConfigHintDoesNotShowIfElseClauseExists() async throws { + let (_, hints) = try await performInlayHintRequest( + markedText: """ + #if DEBUG + #else + #endif + """ + ) + XCTAssertEqual(hints, []) + } }