diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index 270baa908..3112e3638 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -20,6 +20,10 @@ target_sources(SourceKitLSP PRIVATE Clang/ClangLanguageService.swift) target_sources(SourceKitLSP PRIVATE Swift/AdjustPositionToStartOfIdentifier.swift + Swift/CodeActions/ConvertIntegerLiteral.swift + Swift/CodeActions/SyntaxCodeActionProvider.swift + Swift/CodeActions/SyntaxCodeActions.swift + Swift/CodeActions/SyntaxRefactoringCodeActionProvider.swift Swift/CodeCompletion.swift Swift/CodeCompletionSession.swift Swift/CommentXML.swift diff --git a/Sources/SourceKitLSP/Swift/CodeActions/ConvertIntegerLiteral.swift b/Sources/SourceKitLSP/Swift/CodeActions/ConvertIntegerLiteral.swift new file mode 100644 index 000000000..b541f1c91 --- /dev/null +++ b/Sources/SourceKitLSP/Swift/CodeActions/ConvertIntegerLiteral.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 LanguageServerProtocol +import SwiftRefactor +import SwiftSyntax + +// TODO: Make the type IntegerLiteralExprSyntax.Radix conform to CaseEnumerable +// in swift-syntax. + +extension IntegerLiteralExprSyntax.Radix { + static var allCases: [Self] = [.binary, .octal, .decimal, .hex] +} + +/// Syntactic code action provider to convert integer literals between +/// different bases. +struct ConvertIntegerLiteral: SyntaxCodeActionProvider { + static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] { + guard + let token = scope.firstToken, + let integerExpr = token.parent?.as(IntegerLiteralExprSyntax.self), + let integerValue = Int( + integerExpr.split().value.filter { $0 != "_" }, + radix: integerExpr.radix.size + ) + else { + return [] + } + + var actions = [CodeAction]() + let currentRadix = integerExpr.radix + for radix in IntegerLiteralExprSyntax.Radix.allCases { + guard radix != currentRadix else { + continue + } + + //TODO: Add this to swift-syntax? + let prefix: String + switch radix { + case .binary: + prefix = "0b" + case .octal: + prefix = "0o" + case .hex: + prefix = "0x" + case .decimal: + prefix = "" + } + + let convertedValue: ExprSyntax = + "\(raw: prefix)\(raw: String(integerValue, radix: radix.size))" + let edit = TextEdit( + range: scope.snapshot.range(of: integerExpr), + newText: convertedValue.description + ) + actions.append( + CodeAction( + title: "Convert \(integerExpr) to \(convertedValue)", + kind: .refactorInline, + edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]]) + ) + ) + } + + return actions + } +} diff --git a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActionProvider.swift b/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActionProvider.swift new file mode 100644 index 000000000..45bebc378 --- /dev/null +++ b/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActionProvider.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 LSPLogging +import LanguageServerProtocol +import SwiftRefactor +import SwiftSyntax + +/// Describes types that provide one or more code actions based on purely +/// syntactic information. +protocol SyntaxCodeActionProvider { + /// Produce code actions within the given scope. Each code action + /// corresponds to one syntactic transformation that can be performed, such + /// as adding or removing separators from an integer literal. + static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] +} + +/// Defines the scope in which a syntactic code action occurs. +struct SyntaxCodeActionScope { + /// The snapshot of the document on which the code actions will be evaluated. + var snapshot: DocumentSnapshot + + /// The actual code action request, which can specify additional parameters + /// to guide the code actions. + var request: CodeActionRequest + + /// The source file in which the syntactic code action will operate. + var file: SourceFileSyntax + + /// The UTF-8 byte range in the source file in which code actions should be + /// considered, i.e., where the cursor or selection is. + var range: Range + + init( + snapshot: DocumentSnapshot, + syntaxTree tree: SourceFileSyntax, + request: CodeActionRequest + ) throws { + self.snapshot = snapshot + self.request = request + self.file = tree + + let start = snapshot.absolutePosition(of: request.range.lowerBound) + let end = snapshot.absolutePosition(of: request.range.upperBound) + let left = file.token(at: start) + let right = file.token(at: end) + let leftOff = left?.position ?? AbsolutePosition(utf8Offset: 0) + let rightOff = right?.endPosition ?? leftOff + self.range = leftOff.. [CodeAction] { + guard + let token = scope.firstToken, + let node = token.parent?.as(Input.self) + else { + return [] + } + + guard let refactored = Self.refactor(syntax: node) else { + return [] + } + + let edit = TextEdit( + range: scope.snapshot.range(of: node), + newText: refactored.description + ) + + return [ + CodeAction( + title: Self.title, + kind: .refactorInline, + edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]]) + ) + ] + } +} + +// Adapters for specific refactoring provides in swift-syntax. + +extension AddSeparatorsToIntegerLiteral: SyntaxRefactoringCodeActionProvider { + public static var title: String { "Add digit separators" } +} + +extension FormatRawStringLiteral: SyntaxRefactoringCodeActionProvider { + public static var title: String { + "Convert string literal to minimal number of '#'s" + } +} + +extension MigrateToNewIfLetSyntax: SyntaxRefactoringCodeActionProvider { + public static var title: String { "Migrate to shorthand 'if let' syntax" } +} + +extension OpaqueParameterToGeneric: SyntaxRefactoringCodeActionProvider { + public static var title: String { "Expand 'some' parameters to generic parameters" } +} + +extension RemoveSeparatorsFromIntegerLiteral: SyntaxRefactoringCodeActionProvider { + public static var title: String { "Remove digit separators" } +} diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift index 878dc0000..07e41a89c 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift @@ -699,14 +699,21 @@ extension SwiftLanguageService { } public func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? { - let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind)] = [ + let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind?)] = [ + (retrieveSyntaxCodeActions, nil), (retrieveRefactorCodeActions, .refactor), (retrieveQuickFixCodeActions, .quickFix), ] let wantedActionKinds = req.context.only - let providers = providersAndKinds.filter { wantedActionKinds?.contains($0.1) != false } + let providers: [CodeActionProvider] = providersAndKinds.compactMap { + if let wantedActionKinds, let kind = $0.1, !wantedActionKinds.contains(kind) { + return nil + } + + return $0.provider + } let codeActionCapabilities = capabilityRegistry.clientCapabilities.textDocument?.codeAction - let codeActions = try await retrieveCodeActions(req, providers: providers.map { $0.provider }) + let codeActions = try await retrieveCodeActions(req, providers: providers) let response = CodeActionRequestResponse( codeActions: codeActions, clientCapabilities: codeActionCapabilities @@ -714,7 +721,9 @@ extension SwiftLanguageService { return response } - func retrieveCodeActions(_ req: CodeActionRequest, providers: [CodeActionProvider]) async throws -> [CodeAction] { + func retrieveCodeActions(_ req: CodeActionRequest, providers: [CodeActionProvider]) async throws + -> [CodeAction] + { guard providers.isEmpty == false else { return [] } @@ -725,6 +734,17 @@ extension SwiftLanguageService { // Ignore any providers that failed to provide refactoring actions. return [] } + }.flatMap { $0 }.sorted { $0.title < $1.title } + } + + func retrieveSyntaxCodeActions(_ request: CodeActionRequest) async throws -> [CodeAction] { + let uri = request.textDocument.uri + let snapshot = try documentManager.latestSnapshot(uri) + + let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) + let scope = try SyntaxCodeActionScope(snapshot: snapshot, syntaxTree: syntaxTree, request: request) + return await allSyntaxCodeActions.concurrentMap { provider in + return provider.codeActions(in: scope) }.flatMap { $0 } } @@ -1091,6 +1111,18 @@ extension DocumentSnapshot { return lowerBound.. Range { + let lowerBound = self.position(of: node.position) + let upperBound = self.position(of: node.endPosition) + return lowerBound..