Skip to content
Open
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
3 changes: 2 additions & 1 deletion Sources/SwiftLanguageService/CursorInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ struct CursorInfo {
isDynamic: dict[keys.isDynamic] ?? false,
isSystem: dict[keys.isSystem] ?? false,
receiverUsrs: dict[keys.receivers]?.compactMap { $0[keys.usr] as String? } ?? [],
systemModule: module
systemModule: module,
typeName: dict[keys.typeName]
),
annotatedDeclaration: dict[keys.annotatedDecl],
documentation: dict[keys.docComment]
Expand Down
9 changes: 7 additions & 2 deletions Sources/SwiftLanguageService/RemoveUnusedImports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,13 @@ extension SwiftLanguageService {

let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
guard
let node = SyntaxCodeActionScope(snapshot: snapshot, syntaxTree: syntaxTree, request: request)?
.innermostNodeContainingRange,
let node = SyntaxCodeActionScope(
resolveSupport: nil,
snapshot: snapshot,
syntaxTree: syntaxTree,
requestedRange: request.range
)?
.innermostNodeContainingRange,
node.findParentOfSelf(ofType: ImportDeclSyntax.self, stoppingIf: { _ in false }) != nil
else {
// Only offer the remove unused imports code action on an import statement.
Expand Down
45 changes: 42 additions & 3 deletions Sources/SwiftLanguageService/SwiftLanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1001,16 +1001,55 @@ extension SwiftLanguageService {
let snapshot = try documentManager.latestSnapshot(uri)

let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
guard let scope = SyntaxCodeActionScope(snapshot: snapshot, syntaxTree: syntaxTree, request: request) else {
guard
let scope = SyntaxCodeActionScope(
resolveSupport: capabilityRegistry.clientCapabilities.textDocument?.codeAction?.resolveSupport,
snapshot: snapshot,
syntaxTree: syntaxTree,
requestedRange: request.range
)
else {
return []
}
return await allSyntaxCodeActions.concurrentMap { provider in
return await allSyntaxCodeActionProviders.concurrentMap { provider in
return provider.codeActions(in: scope)
}.flatMap { $0 }
}

package func codeActionResolve(_ req: CodeActionResolveRequest) async throws -> CodeAction {
return req.codeAction
guard let data = UnresolvedCodeActionData(fromLSPAny: req.codeAction.data) else {
Comment thread
rintaro marked this conversation as resolved.
// We don't have any data to resolve the code action.
return req.codeAction
}
guard let provider = allSyntaxCodeActionProviders.filter({ "\($0)" == data.action }).only else {
throw ResponseError.unknown("Could not find syntax action '\(data.action)' to resolve code action")
}
let snapshot = try documentManager.latestSnapshot(data.document.uri)
guard snapshot.version == data.document.version else {
throw ResponseError.unknown("Document was modified since between code action and resolve request")
}
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)

guard
let scope = SyntaxCodeActionScope(
resolveSupport: capabilityRegistry.clientCapabilities.textDocument?.codeAction?.resolveSupport,
snapshot: snapshot,
syntaxTree: syntaxTree,
requestedRange: data.range
)
else {
throw ResponseError.unknown("Unable to re-create code action scope")
}
return try await provider.resolve(
req.codeAction,
in: scope,
unresolvedData: data.data,
symbolInfo: { position in
try await self.symbolInfo(
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: position)
)
}
)
}

func retrieveRefactorCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ extension ConvertCommentToDocComment: SyntaxRefactoringCodeActionProvider {
static let title = "Convert Comment to Doc Comment"

static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> DeclSyntax? {
let cursorPosition = scope.snapshot.absolutePosition(of: scope.request.range.lowerBound)
let cursorPosition = scope.snapshot.absolutePosition(of: scope.requestedRange.lowerBound)
guard let token = scope.file.token(at: cursorPosition) else {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,75 @@
//
//===----------------------------------------------------------------------===//

package import LanguageServerProtocol
import SourceKitLSP
import SwiftRefactor
package import SwiftSyntax
import SwiftSyntaxBuilder
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions

package struct ConvertStoredPropertyToComputed: SyntaxRefactoringProvider, ResolvableSyntaxRefactoringCodeActionProvider
{
package typealias Input = VariableDeclSyntax

static let title: String = "Convert Stored Property to Computed Property"

static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> VariableDeclSyntax? {
return scope.innermostNodeContainingRange?.findParentOfSelf(
ofType: VariableDeclSyntax.self,
stoppingIf: {
$0.is(CodeBlockItemSyntax.self) || $0.is(MemberBlockItemSyntax.self) || $0.is(InitializerClauseSyntax.self)
}
)
}

package struct ConvertStoredPropertyToComputed: SyntaxRefactoringProvider {
package struct Context {
package let type: TypeSyntax?

package init(type: TypeSyntax? = nil) {
self.type = type
}
}

package struct UnresolvedData: Codable, LSPAnyCodable {
package let position: Position
}

static func refactoringContext(
for node: Input,
in scope: SyntaxCodeActionScope
) -> RefactoringContext<Context, UnresolvedData> {
guard scope.resolveSupport?.canResolveEdit ?? false else {
// If the editor doesn't have resolve support, fall back to a syntactic action that introduces an editor placeholder for the type, similar to
// if the type cannot be inferred.
return .context(Context())
}
guard node.bindings.contains(where: { $0.typeAnnotation?.type == nil }) else {
// All types are syntactically specified, we don't need to resolve the semantic type
return .context(Context())
}
guard let binding = node.bindings.only,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier
else {
// We can only resolve type information for a single variable binding at the moment. If this is variable decl with multiple bindings, still
// offer the refactoring action and introduce placeholders for the type annotation.
return .context(Context())
}
return .unresolved(UnresolvedData(position: scope.snapshot.position(of: identifier.position)))
}

static func resolveContext(
for data: UnresolvedData,
in scope: SyntaxCodeActionScope,
symbolInfo: (_ position: Position) async throws -> [SymbolDetails]
) async throws -> Context {
guard let symbolInfo = try await symbolInfo(data.position).only, let typeName = symbolInfo.typeName, typeName != "_"
else {
return Context()
}
return Context(type: "\(raw: typeName)")
}

package static func refactor(syntax: VariableDeclSyntax, in context: Context) throws -> VariableDeclSyntax {
guard syntax.bindings.count == 1, let binding = syntax.bindings.first, let initializer = binding.initializer else {
throw RefactoringNotApplicableError("unsupported variable declaration")
Expand Down
6 changes: 1 addition & 5 deletions Sources/SwiftSyntaxCodeActions/SwapBinaryOperands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,11 @@ struct SwapBinaryOperands: SyntaxRefactoringCodeActionProvider {
return nil
}

let startPos = scope.snapshot.absolutePosition(of: scope.request.range.lowerBound)
let endPos = scope.snapshot.absolutePosition(of: scope.request.range.upperBound)
let selectionRange = startPos..<endPos

// Only offer the refactoring when the cursor or selection targets the
// operator token. This prevents offering the action when the cursor is placed on
// either operand or in surrounding trivia.
let tokenRange = opExpr.operator.trimmedRange
guard selectionRange.overlapsOrTouches(tokenRange) else {
guard scope.range.overlapsOrTouches(tokenRange) else {
return nil
}

Expand Down
78 changes: 70 additions & 8 deletions Sources/SwiftSyntaxCodeActions/SyntaxCodeActionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,87 @@ package import SourceKitLSP
import SwiftRefactor
package import SwiftSyntax

/// Data that is included in a `CodeAction` response for which the client should resolve the edit lazily using a `codeAction/resolve` request.
///
/// This data allows us to re-construct the `SyntaxCodeActionScope`.
package struct UnresolvedCodeActionData: Codable, LSPAnyCodable {
/// A string representation of the syntax refactoring action's type.
package let action: String

/// The document on which the code action should be applied.
package let document: VersionedTextDocumentIdentifier

/// The range at which the code action was originally requested.
package let range: Range<Position>

/// Action-specific data describing what data needs to be resolved asynchronously during the resolve request.
package let data: LSPAny

init<Metatype: ResolvableSyntaxRefactoringCodeActionProvider>(
actionType: Metatype.Type,
document: VersionedTextDocumentIdentifier,
range: Range<Position>,
data: LSPAny
) {
self.action = "\(Metatype.self)"
self.document = document
self.range = range
self.data = data
}
}

/// Describes types that provide one or more code actions based on purely
/// syntactic information.
package protocol SyntaxCodeActionProvider: SendableMetatype {
/// 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]

/// Resolve semantic information for a code action.
static func resolve(
_ codeAction: CodeAction,
in scope: SyntaxCodeActionScope,
unresolvedData: LSPAny,
symbolInfo: (_ position: Position) async throws -> [SymbolDetails]
) async throws -> CodeAction
}

extension SyntaxCodeActionProvider {
package static func resolve(
_ codeAction: CodeAction,
in scope: SyntaxCodeActionScope,
unresolvedData: LSPAny,
symbolInfo: (_ position: Position) async throws -> [SymbolDetails]
) async throws -> CodeAction {
throw ResponseError.internalError("Resolve not implemented for '\(Self.self)'")

@rintaro rintaro Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return codeAction as-is. Clients may request codeAction/resolve regardless.

Or, make resolve() a requirement of ResolvableSyntaxRefactoringCodeActionProvider and cast the provider type to it. If it's not a resolvable provider, just return codeAction.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to return codeAction. I wanted to keep ResolvableSyntaxRefactoringCodeActionProvider as an internal type of SwiftSyntaxCodeActions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this change (i.e. this is still throwing an error).

}
}

extension TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties {
var canResolveEdit: Bool {
return self.properties.contains("edit")
}
}

/// Defines the scope in which a syntactic code action occurs.
package struct SyntaxCodeActionScope {
/// Whether the client supports the codeAction/resolve request.
///
/// This allows code actions to use a syntactic fallback if semantic information cannot be resolved using the `codeAction/resolve` request.
package var resolveSupport: TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties?

/// The snapshot of the document on which the code actions will be evaluated.
package var snapshot: DocumentSnapshot

/// The actual code action request, which can specify additional parameters
/// to guide the code actions.
package var request: CodeActionRequest

/// The source file in which the syntactic code action will operate.
package var file: SourceFileSyntax

/// The originally requested range in the original code action request.
///
/// Generally, `range` should be preferred because it performs useful adjustments to extend the range to the start and end of tokens.
var requestedRange: Range<Position>

/// The UTF-8 byte range in the source file in which code actions should be
/// considered, i.e., where the cursor or selection is.
package var range: Range<AbsolutePosition>
Expand All @@ -45,16 +105,18 @@ package struct SyntaxCodeActionScope {
package var innermostNodeContainingRange: Syntax?

package init?(
resolveSupport: TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties?,
snapshot: DocumentSnapshot,
syntaxTree file: SourceFileSyntax,
request: CodeActionRequest
requestedRange: Range<Position>,
) {
self.resolveSupport = resolveSupport
self.snapshot = snapshot
self.request = request
self.requestedRange = requestedRange
self.file = file

guard let left = tokenForRefactoring(at: request.range.lowerBound, snapshot: snapshot, syntaxTree: file),
let right = tokenForRefactoring(at: request.range.upperBound, snapshot: snapshot, syntaxTree: file)
guard let left = tokenForRefactoring(at: requestedRange.lowerBound, snapshot: snapshot, syntaxTree: file),
let right = tokenForRefactoring(at: requestedRange.upperBound, snapshot: snapshot, syntaxTree: file)
else {
return nil
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/SwiftSyntaxCodeActions/SyntaxCodeActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ import SwiftRefactor

/// List of all of the syntactic code action providers, which can be used
/// to produce code actions using only the swift-syntax tree of a file.
package let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = {
package let allSyntaxCodeActionProviders: [any SyntaxCodeActionProvider.Type] = {
var result: [any SyntaxCodeActionProvider.Type] = [
AddDocumentation.self,
AddExplicitEnumRawValues.self,
AddSeparatorsToIntegerLiteral.self,
ApplyDeMorganLaw.self,
ConvertCommentToDocComment.self,
ConvertCommentToDocComment.self,
ConvertComputedPropertyToStored.self,
ConvertComputedPropertyToZeroParameterFunction.self,
ConvertIfLetToGuard.self,
ConvertIntegerLiteral.self,
ConvertJSONToCodableStruct.self,
ConvertStoredPropertyToComputed.self,
ConvertStringConcatenationToStringInterpolation.self,
ConvertZeroParameterFunctionToComputedProperty.self,
FormatRawStringLiteral.self,
Expand All @@ -34,7 +37,6 @@ package let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = {
OpaqueParameterToGeneric.self,
RemoveRedundantParentheses.self,
RemoveSeparatorsFromIntegerLiteral.self,
ConvertCommentToDocComment.self,
SwapBinaryOperands.self,
]
#if !NO_SWIFTPM_DEPENDENCY
Expand Down
Loading