Repository: apple/swift-syntax
Pull request #2511 (2024-02-25 17:50:51)

Fix trivia handling issues in editor placeholder expansion
rdar://123287930


Patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2474bc12..758afe16 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -63,12 +63,23 @@ endif()
 
 # Determine the module triple.
 if("${SWIFT_HOST_MODULE_TRIPLE}" STREQUAL "")
-  set(module_triple_command "${CMAKE_Swift_COMPILER}" -print-target-info)
+  # FIXME: This is a hack. It's all a hack. Windows isn't setting
+  # CMAKE_Swift_COMPILER_TARGET.
   if(CMAKE_Swift_COMPILER_TARGET)
-    list(APPEND module_triple_command -target ${CMAKE_Swift_COMPILER_TARGET})
+    string(REGEX REPLACE "macosx[0-9]+([.][0-9]+)?" "macos" SWIFT_HOST_MODULE_TRIPLE
+      ${CMAKE_Swift_COMPILER_TARGET})
+  elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
+    if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
+      set(SWIFT_HOST_MODULE_TRIPLE "x86_64-unknown-windows-msvc")
+    elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64|arm64")
+      set(SWIFT_HOST_MODULE_TRIPLE "aarch64-unknown-windows-msvc")
+    else()
+      message(FATAL_ERROR "Unrecognized architecture for Windows host")
+    endif()
+  else()
+    execute_process(COMMAND ${CMAKE_Swift_COMPILER} -print-target-info OUTPUT_VARIABLE target_info)
+    string(JSON SWIFT_HOST_MODULE_TRIPLE GET ${target_info} target moduleTriple)
   endif()
-  execute_process(COMMAND ${module_triple_command} OUTPUT_VARIABLE target_info_json)
-  string(JSON SWIFT_HOST_MODULE_TRIPLE GET "${target_info_json}" "target" "moduleTriple")
 endif()
 message(STATUS "Module triple: ${SWIFT_HOST_MODULE_TRIPLE}")
 
diff --git a/Release Notes/600.md b/Release Notes/600.md
index 4d05a0f1..72990213 100644
--- a/Release Notes/600.md
+++ b/Release Notes/600.md
@@ -54,10 +54,6 @@
   - Issue: https://github.com/apple/swift-syntax/issues/2031
   - Pull Request: https://github.com/apple/swift-syntax/pull/2327
 
-- `BasicFormat.inferIndentation(of:)`
-  - Description: Uses heuristics to infer the indentation width used in a syntax tree.
-  - Pull Request: https://github.com/apple/swift-syntax/pull/2514
-
 ## API Behavior Changes
 
 ## Deprecations
diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt
index f5df62c7..b7fdd54a 100644
--- a/Sources/CMakeLists.txt
+++ b/Sources/CMakeLists.txt
@@ -18,4 +18,3 @@ add_subdirectory(SwiftSyntaxMacros)
 add_subdirectory(SwiftSyntaxMacroExpansion)
 add_subdirectory(SwiftCompilerPluginMessageHandling)
 add_subdirectory(SwiftIDEUtils)
-add_subdirectory(SwiftCompilerPlugin)
diff --git a/Sources/SwiftBasicFormat/BasicFormat.swift b/Sources/SwiftBasicFormat/BasicFormat.swift
index b4a21d4c..ce4d79d2 100644
--- a/Sources/SwiftBasicFormat/BasicFormat.swift
+++ b/Sources/SwiftBasicFormat/BasicFormat.swift
@@ -69,6 +69,16 @@ open class BasicFormat: SyntaxRewriter {
     super.init(viewMode: viewMode)
   }
 
+  /// Clears all stateful data from this `BasicFormat`.
+  ///
+  /// This needs to be called between multiple `rewrite` calls to a syntax tree.
+  func reset() {
+    indentationStack = [indentationStack.first!]
+    anchorPoints = [:]
+    previousToken = nil
+    stringLiteralNestingLevel = 0
+  }
+
   // MARK: - Updating indentation level
 
   public func increaseIndentationLevel(to userDefinedIndentation: Trivia? = nil) {
@@ -145,7 +155,7 @@ open class BasicFormat: SyntaxRewriter {
 
   /// Find the indentation of the nearest ancestor whose first token is an
   /// anchor point (see `anchorPoints`).
-  private func anchorPointIndentation(for token: TokenSyntax) -> Trivia? {
+  private func anchorPointIndentation(for token: TokenSyntax) -> Trivia {
     var ancestor: Syntax = Syntax(token)
     while let parent = ancestor.parent {
       ancestor = parent
@@ -155,7 +165,7 @@ open class BasicFormat: SyntaxRewriter {
         return anchorPointIndentation
       }
     }
-    return nil
+    return Trivia()
   }
 
   // MARK: - Customization points
@@ -575,23 +585,24 @@ open class BasicFormat: SyntaxRewriter {
       trailingTrivia += .space
     }
 
-    var leadingTriviaIndentation = self.currentIndentationLevel
-    var trailingTriviaIndentation = self.currentIndentationLevel
+    let leadingTriviaIndentation: Trivia
+    let trailingTriviaIndentation: Trivia
 
     // If the trivia contains user-defined indentation, find their anchor point
     // and indent the token relative to that anchor point.
+    //
     // Always indent string literals relative to their anchor point because
     // their indentation has structural meaning and we just want to maintain
     // what the user wrote.
-    if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
-      let anchorPointIndentation = self.anchorPointIndentation(for: token)
-    {
-      leadingTriviaIndentation = anchorPointIndentation
+    if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral {
+      leadingTriviaIndentation = anchorPointIndentation(for: token)
+    } else {
+      leadingTriviaIndentation = currentIndentationLevel
     }
-    if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
-      let anchorPointIndentation = self.anchorPointIndentation(for: token)
-    {
-      trailingTriviaIndentation = anchorPointIndentation
+    if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) {
+      trailingTriviaIndentation = anchorPointIndentation(for: token)
+    } else {
+      trailingTriviaIndentation = currentIndentationLevel
     }
 
     leadingTrivia = leadingTrivia.indented(
diff --git a/Sources/SwiftBasicFormat/CMakeLists.txt b/Sources/SwiftBasicFormat/CMakeLists.txt
index 3a3f7bc7..05859828 100644
--- a/Sources/SwiftBasicFormat/CMakeLists.txt
+++ b/Sources/SwiftBasicFormat/CMakeLists.txt
@@ -8,7 +8,6 @@
 
 add_swift_syntax_library(SwiftBasicFormat
   BasicFormat.swift
-  InferIndentation.swift
   Syntax+Extensions.swift
   SyntaxProtocol+Formatted.swift
   Trivia+FormatExtensions.swift
diff --git a/Sources/SwiftBasicFormat/InferIndentation.swift b/Sources/SwiftBasicFormat/InferIndentation.swift
deleted file mode 100644
index 01e7a537..00000000
--- a/Sources/SwiftBasicFormat/InferIndentation.swift
+++ /dev/null
@@ -1,127 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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 SwiftSyntax
-
-extension BasicFormat {
-  /// Uses heuristics to infer the indentation width used in the given syntax tree.
-  ///
-  /// Returns `nil` if the indentation could not be inferred, eg. because it is inconsistent or there are not enough
-  /// indented lines to infer the indentation with sufficient accuracy.
-  public static func inferIndentation(of tree: some SyntaxProtocol) -> Trivia? {
-    return IndentationInferrer.inferIndentation(of: tree)
-  }
-}
-
-private class IndentationInferrer: SyntaxVisitor {
-  /// The trivia of the previous visited token.
-  ///
-  /// The previous token's trailing trivia will be concatenated with the current token's leading trivia to infer
-  /// indentation.
-  ///
-  /// We start with .newline to indicate that the first token starts on a newline, even if it technically doesn't have
-  /// a leading newline character.
-  private var previousTokenTrailingTrivia: Trivia = .newline
-
-  /// Counts how many lines had how many spaces of indentation.
-  ///
-  /// For example, spaceIndentedLines[2] = 4 means that for lines had exactly 2 spaces of indentation.
-  private var spaceIndentedLines: [Int: Int] = [:]
-
-  /// See `spaceIndentedLines`
-  private var tabIndentedLines: [Int: Int] = [:]
-
-  /// The number of lines that were processed for indentation inference.
-  ///
-  /// This will be lower than the actual number of lines in the syntax node because
-  ///  - It does not count lines without indentation
-  //// - It does not count newlines in block doc comments (because we don't process the comment's contents)
-  private var linesProcessed = 0
-
-  override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
-    defer { previousTokenTrailingTrivia = token.trailingTrivia }
-    let triviaAtStartOfLine =
-      (previousTokenTrailingTrivia + token.leadingTrivia)
-      .drop(while: { !$0.isNewline })  // Ignore any trivia that's on the previous line
-      .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline)  // Split trivia into the lines it occurs on
-      .dropFirst()  // Drop the first empty array; exists because we dropped non-newline prefix and newline is separator
-
-    LINE_TRIVIA_LOOP: for lineTrivia in triviaAtStartOfLine {
-      switch lineTrivia.first {
-      case .spaces(var spaces):
-        linesProcessed += 1
-        for triviaPiece in lineTrivia.dropFirst() {
-          switch triviaPiece {
-          case .spaces(let followupSpaces): spaces += followupSpaces
-          case .tabs: break LINE_TRIVIA_LOOP  // Count as processed line but don't add to any indentation count
-          default: break
-          }
-        }
-        spaceIndentedLines[spaces, default: 0] += 1
-      case .tabs(var tabs):
-        linesProcessed += 1
-        for triviaPiece in lineTrivia.dropFirst() {
-          switch triviaPiece {
-          case .tabs(let followupTabs): tabs += followupTabs
-          case .spaces: break LINE_TRIVIA_LOOP  // Count as processed line but don't add to any indentation count
-          default: break
-          }
-        }
-        tabIndentedLines[tabs, default: 0] += 1
-      default:
-        break
-      }
-    }
-    return .skipChildren
-  }
-
-  static func inferIndentation(of tree: some SyntaxProtocol) -> Trivia? {
-    let visitor = IndentationInferrer(viewMode: .sourceAccurate)
-    visitor.walk(tree)
-    if visitor.linesProcessed < 3 {
-      // We don't have enough lines to infer indentation reliably
-      return nil
-    }
-
-    // Pick biggest indentation that encompasses at least 90% of the source lines.
-    let threshold = Int(Double(visitor.linesProcessed) * 0.9)
-
-    for spaceIndentation in [8, 4, 2] {
-      let linesMatchingIndentation = visitor
-        .spaceIndentedLines
-        .filter { $0.key.isMultiple(of: spaceIndentation) }
-        .map { $0.value }
-        .sum
-      if linesMatchingIndentation > threshold {
-        return .spaces(spaceIndentation)
-      }
-    }
-
-    for tabIndentation in [2, 1] {
-      let linesMatchingIndentation = visitor
-        .tabIndentedLines
-        .filter { $0.key.isMultiple(of: tabIndentation) }
-        .map { $0.value }
-        .sum
-      if linesMatchingIndentation > threshold {
-        return .tabs(tabIndentation)
-      }
-    }
-    return nil
-  }
-}
-
-fileprivate extension Array<Int> {
-  var sum: Int {
-    return self.reduce(0) { return $0 + $1 }
-  }
-}
diff --git a/Sources/SwiftBasicFormat/Syntax+Extensions.swift b/Sources/SwiftBasicFormat/Syntax+Extensions.swift
index ec48e36a..2bdacf87 100644
--- a/Sources/SwiftBasicFormat/Syntax+Extensions.swift
+++ b/Sources/SwiftBasicFormat/Syntax+Extensions.swift
@@ -19,7 +19,7 @@ extension TokenSyntax {
   /// find the indentation of the line this token occurs on.
   private var indentation: Trivia? {
     let previous = self.previousToken(viewMode: .sourceAccurate)
-    return ((previous?.trailingTrivia ?? []) + leadingTrivia).indentation(isOnNewline: false)
+    return ((previous?.trailingTrivia ?? []) + leadingTrivia).indentation(isOnNewline: previous == nil)
   }
 
   /// Returns the indentation of the line this token occurs on
diff --git a/Sources/SwiftBasicFormat/SyntaxProtocol+Formatted.swift b/Sources/SwiftBasicFormat/SyntaxProtocol+Formatted.swift
index 2da3dc99..4d17df48 100644
--- a/Sources/SwiftBasicFormat/SyntaxProtocol+Formatted.swift
+++ b/Sources/SwiftBasicFormat/SyntaxProtocol+Formatted.swift
@@ -15,6 +15,7 @@ import SwiftSyntax
 public extension SyntaxProtocol {
   /// Build a syntax node from this `Buildable` and format it with the given format.
   func formatted(using format: BasicFormat = BasicFormat()) -> Syntax {
+    format.reset()
     return format.rewrite(self)
   }
 }
diff --git a/Sources/SwiftCompilerPlugin/CMakeLists.txt b/Sources/SwiftCompilerPlugin/CMakeLists.txt
deleted file mode 100644
index f6e5daf6..00000000
--- a/Sources/SwiftCompilerPlugin/CMakeLists.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-# 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 http://swift.org/LICENSE.txt for license information
-# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
-
-add_swift_syntax_library(SwiftCompilerPlugin
-  CompilerPlugin.swift
-)
-
-target_link_swift_syntax_libraries(SwiftCompilerPlugin PUBLIC
-  SwiftSyntaxMacros
-  SwiftCompilerPluginMessageHandling)
diff --git a/Sources/SwiftDiagnostics/DiagnosticsFormatter.swift b/Sources/SwiftDiagnostics/DiagnosticsFormatter.swift
index e214d8e8..0cf9bcd9 100644
--- a/Sources/SwiftDiagnostics/DiagnosticsFormatter.swift
+++ b/Sources/SwiftDiagnostics/DiagnosticsFormatter.swift
@@ -68,22 +68,6 @@ public struct DiagnosticsFormatter {
     var isFreeOfAnnotations: Bool {
       return diagnostics.isEmpty && suffixText.isEmpty
     }
-
-    /// Converts a UTF-8 column index into an index that considers each character as a single column, not each UTF-8
-    /// byte.
-    ///
-    /// For example the 👨‍👩‍👧‍👦 character is considered as a single character, not 25 bytes.
-    ///
-    /// Both the input and the output column are 1-based.
-    func characterColumn(ofUtf8Column utf8Column: Int) -> Int {
-      let index =
-        sourceString.utf8.index(
-          sourceString.utf8.startIndex,
-          offsetBy: utf8Column - 1,
-          limitedBy: sourceString.utf8.endIndex
-        ) ?? sourceString.utf8.endIndex
-      return sourceString.distance(from: sourceString.startIndex, to: index) + 1
-    }
   }
 
   /// Number of lines which should be printed before and after the diagnostic message
@@ -155,7 +139,7 @@ public struct DiagnosticsFormatter {
 
       let endColumn: Int
       if endLine > lineNumber {
-        endColumn = annotatedLine.sourceString.utf8.count
+        endColumn = annotatedLine.sourceString.count
       } else if endLine == lineNumber {
         endColumn = endLoc.column
       } else {
@@ -290,13 +274,9 @@ public struct DiagnosticsFormatter {
         annotatedSource.append("\n")
       }
 
-      let columnsWithDiagnostics = Set(
-        annotatedLine.diagnostics.map {
-          annotatedLine.characterColumn(ofUtf8Column: $0.location(converter: slc).column)
-        }
-      )
+      let columnsWithDiagnostics = Set(annotatedLine.diagnostics.map { $0.location(converter: slc).column })
       let diagsPerColumn = Dictionary(grouping: annotatedLine.diagnostics) { diag in
-        annotatedLine.characterColumn(ofUtf8Column: diag.location(converter: slc).column)
+        diag.location(converter: slc).column
       }.sorted { lhs, rhs in
         lhs.key > rhs.key
       }
diff --git a/Sources/SwiftRefactor/CallToTrailingClosures.swift b/Sources/SwiftRefactor/CallToTrailingClosures.swift
index 96bbbdcd..6cb19968 100644
--- a/Sources/SwiftRefactor/CallToTrailingClosures.swift
+++ b/Sources/SwiftRefactor/CallToTrailingClosures.swift
@@ -48,7 +48,8 @@ public struct CallToTrailingClosures: SyntaxRefactoringProvider {
   // TODO: Rather than returning nil, we should consider throwing errors with
   // appropriate messages instead.
   public static func refactor(syntax call: FunctionCallExprSyntax, in context: Context = Context()) -> FunctionCallExprSyntax? {
-    return call.convertToTrailingClosures(from: context.startAtArgument)?.formatted().as(FunctionCallExprSyntax.self)
+    let converted = call.convertToTrailingClosures(from: context.startAtArgument)
+    return converted?.formatted().as(FunctionCallExprSyntax.self)
   }
 }
 
@@ -69,7 +70,7 @@ extension FunctionCallExprSyntax {
       // Trailing comma won't exist any more, move its trivia to the end of
       // the closure instead
       if let comma = arg.trailingComma {
-        closure = closure.with(\.trailingTrivia, closure.trailingTrivia.merging(triviaOf: comma))
+        closure.trailingTrivia = closure.trailingTrivia.merging(triviaOf: comma)
       }
       closures.append((arg, closure))
     }
@@ -80,13 +81,12 @@ extension FunctionCallExprSyntax {
 
     // First trailing closure won't have label/colon. Transfer their trivia.
     var trailingClosure = closures.first!.closure
-      .with(
-        \.leadingTrivia,
-        Trivia()
-          .merging(triviaOf: closures.first!.original.label)
-          .merging(triviaOf: closures.first!.original.colon)
-          .merging(closures.first!.closure.leadingTrivia)
-      )
+    trailingClosure.leadingTrivia =
+      Trivia()
+      .merging(triviaOf: closures.first!.original.label)
+      .merging(triviaOf: closures.first!.original.colon)
+      .merging(closures.first!.closure.leadingTrivia)
+      .droppingLeadingWhitespace
     let additionalTrailingClosures = closures.dropFirst().map {
       MultipleTrailingClosureElementSyntax(
         label: $0.original.label ?? .wildcardToken(),
@@ -97,53 +97,61 @@ extension FunctionCallExprSyntax {
 
     var converted = self.detached
 
+    // Trivia that should be attached to the end of the converted call.
+    var additionalTriviaAtEndOfCall: Trivia? = nil
+
     // Remove parens if there's no non-closure arguments left and remove the
     // last comma otherwise. Makes sure to keep the trivia of any removed node.
     var argList = Array(arguments.dropLast(closures.count))
     if argList.isEmpty {
-      converted =
-        converted
-        .with(\.leftParen, nil)
-        .with(\.rightParen, nil)
+      converted.leftParen = nil
+      converted.rightParen = nil
 
       // No left paren any more, right paren is handled below since it makes
       // sense to keep its trivia of the end of the call, regardless of whether
       // it was removed or not.
       if let leftParen = leftParen {
-        trailingClosure = trailingClosure.with(
-          \.leadingTrivia,
-          Trivia()
-            .merging(triviaOf: leftParen)
-            .merging(trailingClosure.leadingTrivia)
-        )
+        trailingClosure.leadingTrivia = Trivia()
+          .merging(triviaOf: leftParen)
+          .merging(trailingClosure.leadingTrivia)
+      }
+      // No right paren anymore. Attach its trivia to the end of the call.
+      if let rightParen = rightParen {
+        additionalTriviaAtEndOfCall = Trivia().merging(triviaOf: rightParen)
       }
     } else {
       let last = argList.last!
-      if let comma = last.trailingComma {
-        converted =
-          converted
-          .with(\.rightParen, TokenSyntax.rightParenToken(trailingTrivia: Trivia().merging(triviaOf: comma)))
-      }
-      argList[argList.count - 1] =
-        last
-        .with(\.trailingComma, nil)
+      // Move the trailing trivia of the closing parenthesis to the end of the call after the last trailing, instead of
+      // keeping it in the middle of the call where the new closing parenthesis lives.
+      // Also ensure that we don't drop trivia from any comma we remove.
+      converted.rightParen?.trailingTrivia = Trivia().merging(triviaOf: last.trailingComma)
+      additionalTriviaAtEndOfCall = rightParen?.trailingTrivia
+      argList[argList.count - 1] = last.with(\.trailingComma, nil)
     }
 
     // Update arguments and trailing closures
-    converted =
-      converted
-      .with(\.arguments, LabeledExprListSyntax(argList))
-      .with(\.trailingClosure, trailingClosure)
+    converted.arguments = LabeledExprListSyntax(argList)
+    converted.trailingClosure = trailingClosure
     if !additionalTrailingClosures.isEmpty {
-      converted = converted.with(\.additionalTrailingClosures, MultipleTrailingClosureElementListSyntax(additionalTrailingClosures))
+      converted.additionalTrailingClosures = MultipleTrailingClosureElementListSyntax(additionalTrailingClosures)
     }
 
-    // The right paren either doesn't exist any more, or is before all the
-    // trailing closures. Moves its trivia to the end of the converted call.
-    if let rightParen = rightParen {
-      converted = converted.with(\.trailingTrivia, converted.trailingTrivia.merging(triviaOf: rightParen))
+    if let additionalTriviaAtEndOfCall {
+      converted.trailingTrivia = converted.trailingTrivia.merging(additionalTriviaAtEndOfCall.droppingLeadingWhitespace)
     }
 
     return converted
   }
 }
+
+fileprivate extension Trivia {
+  var droppingLeadingWhitespace: Trivia {
+    return Trivia(pieces: self.drop(while: \.isWhitespace))
+  }
+}
+
+fileprivate extension Sequence {
+  func dropSuffix(while predicate: (Element) -> Bool) -> [Element] {
+    self.reversed().drop(while: predicate).reversed()
+  }
+}
diff --git a/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift b/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift
index b3413aea..f8c5acf5 100644
--- a/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift
+++ b/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift
@@ -70,7 +70,17 @@ import SwiftSyntaxBuilder
 /// anything here
 /// ```
 struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
-  static func textRefactor(syntax token: TokenSyntax, in context: Void) -> [SourceEdit] {
+  struct Context {
+    let indentationWidth: Trivia?
+    let initialIndentation: Trivia
+
+    init(indentationWidth: Trivia? = nil, initialIndentation: Trivia = []) {
+      self.indentationWidth = indentationWidth
+      self.initialIndentation = initialIndentation
+    }
+  }
+
+  static func textRefactor(syntax token: TokenSyntax, in context: Context = Context()) -> [SourceEdit] {
     guard let placeholder = EditorPlaceholderData(token: token) else {
       return []
     }
@@ -81,7 +91,18 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
       expanded = String(text)
     case let .typed(text, type):
       if let functionType = type.as(FunctionTypeSyntax.self) {
-        expanded = functionType.closureExpansion.formatted().description
+        let basicFormat = BasicFormat(
+          indentationWidth: context.indentationWidth,
+          initialIndentation: context.initialIndentation
+        )
+        var formattedExpansion = functionType.closureExpansion.formatted(using: basicFormat).description
+        // Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
+        // format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
+        // that might be in the middle of a line.
+        if formattedExpansion.hasPrefix(context.initialIndentation.description) {
+          formattedExpansion = String(formattedExpansion.dropFirst(context.initialIndentation.description.count))
+        }
+        expanded = formattedExpansion
       } else {
         expanded = String(text)
       }
@@ -122,13 +143,25 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
 ///
 /// Expansion on `closure1` and `normalArg` is the same as `ExpandSingleEditorPlaceholder`.
 public struct ExpandEditorPlaceholder: EditRefactoringProvider {
-  public static func textRefactor(syntax token: TokenSyntax, in context: Void) -> [SourceEdit] {
+  public struct Context {
+    public let indentationWidth: Trivia?
+
+    public init(indentationWidth: Trivia? = nil) {
+      self.indentationWidth = indentationWidth
+    }
+  }
+
+  public static func textRefactor(syntax token: TokenSyntax, in context: Context = Context()) -> [SourceEdit] {
     guard let placeholder = token.parent?.as(DeclReferenceExprSyntax.self),
       placeholder.baseName.isEditorPlaceholder,
       let arg = placeholder.parent?.as(LabeledExprSyntax.self),
       let argList = arg.parent?.as(LabeledExprListSyntax.self),
       let call = argList.parent?.as(FunctionCallExprSyntax.self),
-      let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(in: call, ifIncluded: arg)
+      let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(
+        in: call,
+        ifIncluded: arg,
+        indentationWidth: context.indentationWidth
+      )
     else {
       return ExpandSingleEditorPlaceholder.textRefactor(syntax: token)
     }
@@ -159,8 +192,16 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
 /// }
 /// ```
 public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
-  public static func refactor(syntax call: FunctionCallExprSyntax, in context: Void = ()) -> FunctionCallExprSyntax? {
-    return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil)
+  public struct Context {
+    public let indentationWidth: Trivia?
+
+    public init(indentationWidth: Trivia? = nil) {
+      self.indentationWidth = indentationWidth
+    }
+  }
+
+  public static func refactor(syntax call: FunctionCallExprSyntax, in context: Context = Context()) -> FunctionCallExprSyntax? {
+    return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil, indentationWidth: context.indentationWidth)
   }
 
   /// If the given argument is `nil` or one of the last arguments that are all
@@ -170,9 +211,10 @@ public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvi
   /// Otherwise return nil.
   fileprivate static func expandTrailingClosurePlaceholders(
     in call: FunctionCallExprSyntax,
-    ifIncluded arg: LabeledExprSyntax?
+    ifIncluded arg: LabeledExprSyntax?,
+    indentationWidth: Trivia?
   ) -> FunctionCallExprSyntax? {
-    guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg) else {
+    guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth) else {
       return nil
     }
 
@@ -260,7 +302,8 @@ extension FunctionCallExprSyntax {
   /// closures based on the function types provided by each editor placeholder.
   /// Otherwise return nil.
   fileprivate func expandTrailingClosurePlaceholders(
-    ifIncluded: LabeledExprSyntax?
+    ifIncluded: LabeledExprSyntax?,
+    indentationWidth: Trivia?
   ) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
     var includedArg = false
     var argsToExpand = 0
@@ -283,9 +326,14 @@ extension FunctionCallExprSyntax {
       return nil
     }
 
+    let lineIndentation = self.firstToken(viewMode: .sourceAccurate)?.indentationOfLine ?? []
+
     var expandedArgs = [LabeledExprSyntax]()
     for arg in arguments.suffix(argsToExpand) {
-      let edits = ExpandSingleEditorPlaceholder.textRefactor(syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName)
+      let edits = ExpandSingleEditorPlaceholder.textRefactor(
+        syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName,
+        in: ExpandSingleEditorPlaceholder.Context(indentationWidth: indentationWidth, initialIndentation: lineIndentation)
+      )
       guard edits.count == 1, let edit = edits.first, !edit.replacement.isEmpty else {
         return nil
       }


Test patch:
diff --git a/Tests/SwiftBasicFormatTest/BasicFormatTests.swift b/Tests/SwiftBasicFormatTest/BasicFormatTests.swift
index 15e5fa1d..b0dfbb87 100644
--- a/Tests/SwiftBasicFormatTest/BasicFormatTests.swift
+++ b/Tests/SwiftBasicFormatTest/BasicFormatTests.swift
@@ -610,4 +610,39 @@ final class BasicFormatTest: XCTestCase {
     )
   }
 
+  func testFormatClosureWithInitialIndentation() throws {
+    assertFormatted(
+      tree: ClosureExprSyntax(
+        statements: CodeBlockItemListSyntax([
+          CodeBlockItemSyntax(item: CodeBlockItemSyntax.Item(IntegerLiteralExprSyntax(integerLiteral: 2)))
+        ])
+      ),
+      expected: """
+            {
+                2
+            }
+        """,
+      using: BasicFormat(initialIndentation: .spaces(4))
+    )
+  }
+
+  func testIndentedStandaloneClosureRoundTrips() throws {
+    let source = """
+          foo {
+              "abc"
+          }
+      """
+    assertFormatted(source: source, expected: source)
+  }
+
+  func testIndentedStandaloneClosureRoundTrips2() throws {
+    let source = """
+          foo {
+              if true {
+                  print("test")
+              }
+          }
+      """
+    assertFormatted(source: source, expected: source)
+  }
 }
diff --git a/Tests/SwiftBasicFormatTest/InferIndentationTests.swift b/Tests/SwiftBasicFormatTest/InferIndentationTests.swift
deleted file mode 100644
index 0aabde4c..00000000
--- a/Tests/SwiftBasicFormatTest/InferIndentationTests.swift
+++ /dev/null
@@ -1,196 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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 SwiftBasicFormat
-import SwiftSyntax
-import SwiftSyntaxBuilder
-import XCTest
-
-fileprivate func assertIndentation(
-  of sourceFile: SourceFileSyntax,
-  _ expected: Trivia?,
-  file: StaticString = #file,
-  line: UInt = #line
-) {
-  let inferred = BasicFormat.inferIndentation(of: sourceFile)
-  XCTAssertEqual(inferred, expected, file: file, line: line)
-}
-
-final class InferIndentationTests: XCTestCase {
-  func testTwoSpaceIndentation() {
-    assertIndentation(
-      of: """
-        class Foo {
-          func bar() {
-            print("hello world")
-          }
-        }
-        """,
-      .spaces(2)
-    )
-  }
-
-  func testTwoSpaceIndentationWithMultipleFourSpaceLines() {
-    assertIndentation(
-      of: """
-        class Foo {
-          func bar() {
-            print("hello world")
-            print("test")
-            print("another")
-          }
-        }
-        """,
-      .spaces(2)
-    )
-  }
-
-  func testTabIndentation() {
-    assertIndentation(
-      of: """
-        class Foo {
-        \tfunc bar() {
-        \tprint("hello world")
-        \t}
-        }
-        """,
-      .tabs(1)
-    )
-  }
-
-  func testUseLineCommentsForInference() {
-    assertIndentation(
-      of: """
-        class Foo {
-          /// Some doc comment
-          /// And another
-          func bar() {
-          }
-        }
-        """,
-      .spaces(2)
-    )
-  }
-
-  func testBlockCommentContentsDontCountForInference() {
-    assertIndentation(
-      of: """
-        class Foo {
-          /*
-           * Test abc 
-           */
-          func bar() {}
-        }
-        """,
-      nil
-    )
-  }
-
-  func testWithBlockComment() {
-    assertIndentation(
-      of: """
-        class Foo {
-          /*
-           * Test abc 
-           */
-          func bar() {
-            print("test")
-          }
-        }
-        """,
-      .spaces(2)
-    )
-  }
-
-  func testDontGetConfusedBySingleIncorrectIndentation() {
-    assertIndentation(
-      of: """
-        class Foo {
-            func bar() {
-                print("1")
-                print("2")
-                print("3")
-                print("4")
-                print("5")
-                print("6")
-                print("7")
-                print("8")
-                print("9")
-              print("10")
-            }
-        }
-        """,
-      .spaces(4)
-    )
-  }
-
-  func testMixedTwoAndFourSpaceIndentation() {
-    assertIndentation(
-      of: """
-        class Foo {
-            func bar() {
-                print("1")
-                print("2")
-                print("3")
-                print("4")
-                print("5")
-              print("6")
-              print("7")
-              print("8")
-              print("9")
-              print("10")
-            }
-        }
-        """,
-      .spaces(2)
-    )
-  }
-
-  func testMixedSpaceAndTabIndentation() {
-    assertIndentation(
-      of: """
-        class Foo {
-          func bar() {
-              print("1")
-              print("2")
-              print("3")
-              print("4")
-              print("5")
-        \t\tprint("6")
-        \t\tprint("7")
-        \t\tprint("8")
-        \t\tprint("9")
-        \t\tprint("10")
-        \t}
-        }
-        """,
-      nil
-    )
-  }
-
-  func testMultilineStringLiteralWithInternalIndentation() {
-    assertIndentation(
-      of: #"""
-        class Foo {
-          func bar() {
-            print("""
-            To Do:
-             - Ignore string literals
-             - Infer indentation
-            """)
-          }
-        }
-        """#,
-      .spaces(2)
-    )
-  }
-}
diff --git a/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift b/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift
index bd373df4..eb5922a7 100644
--- a/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift
+++ b/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift
@@ -150,37 +150,4 @@ final class ParserDiagnosticsFormatterIntegrationTests: XCTestCase {
 
     assertStringsEqualWithDiff(annotate(source: source), expectedOutput)
   }
-
-  func testDontCrashIfFullLineHighlightContainsEmoji() {
-    let source = """
-      func o() {
-      }👨‍👩‍👧‍👦}
-      }
-      """
-
-    let expectedOutput = """
-      1 │ func o() {
-      2 │ }👨‍👩‍👧‍👦}
-        │  │╰─ error: extraneous braces at top level
-        │  ╰─ error: consecutive statements on a line must be separated by newline or ';'
-      3 │ }
-
-      """
-
-    assertStringsEqualWithDiff(annotate(source: source), expectedOutput)
-  }
-
-  func testEmojiInSourceCode() {
-    let source = """
-      let 👨‍👩‍👧‍👦 = ;
-      """
-
-    let expectedOutput = """
-      1 │ let 👨‍👩‍👧‍👦 = ;
-        │         ╰─ error: expected expression in variable
-
-      """
-
-    assertStringsEqualWithDiff(annotate(source: source), expectedOutput)
-  }
 }
diff --git a/Tests/SwiftRefactorTest/CallToTrailingClosureTests.swift b/Tests/SwiftRefactorTest/CallToTrailingClosureTests.swift
index e372401b..bff9dc96 100644
--- a/Tests/SwiftRefactorTest/CallToTrailingClosureTests.swift
+++ b/Tests/SwiftRefactorTest/CallToTrailingClosureTests.swift
@@ -217,6 +217,36 @@ final class CallToTrailingClosuresTest: XCTestCase {
 
     try assertRefactorCall(baseline, expected: expected)
   }
+
+  func testClosureWithArgumentLabel() throws {
+    try assertRefactorCall(
+      """
+      foo(arg: 1, closure: { someInt in
+
+      })
+      """,
+      expected: """
+        foo(arg: 1) { someInt in
+
+        }
+        """
+    )
+  }
+
+  func testCallHasInitialIndentationIndentation() throws {
+    try assertRefactorCall(
+      """
+          foo(arg: 1, closure: { someInt in
+              "abc"
+          })
+      """,
+      expected: """
+            foo(arg: 1) { someInt in
+                "abc"
+            }
+        """
+    )
+  }
 }
 
 fileprivate func assertRefactorCall(
diff --git a/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift b/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift
index fd63db3c..3075bf51 100644
--- a/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift
+++ b/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift
@@ -193,7 +193,7 @@ final class ExpandEditorPlaceholderTests: XCTestCase {
       foo(arg: \(intPlaceholder), closure: \(closureWithArgPlaceholder))
       """,
       expected: """
-        foo(arg: \(intPlaceholder))  { someInt in
+        foo(arg: \(intPlaceholder)) { someInt in
             \(stringPlaceholder)
         }
         """
@@ -206,7 +206,7 @@ final class ExpandEditorPlaceholderTests: XCTestCase {
       foo(arg: \(intPlaceholder), firstClosure: \(closureWithArgPlaceholder), secondClosure: \(closureWithArgPlaceholder))
       """,
       expected: """
-        foo(arg: \(intPlaceholder))  { someInt in
+        foo(arg: \(intPlaceholder)) { someInt in
             \(stringPlaceholder)
         } secondClosure: { someInt in
             \(stringPlaceholder)
@@ -221,7 +221,99 @@ final class ExpandEditorPlaceholderTests: XCTestCase {
       foo(pre: \(closurePlaceholder), arg: \(intPlaceholder), closure: \(closureWithArgPlaceholder))
       """,
       expected: """
-        foo(pre: \(closurePlaceholder), arg: \(intPlaceholder))  { someInt in
+        foo(pre: \(closurePlaceholder), arg: \(intPlaceholder)) { someInt in
+            \(stringPlaceholder)
+        }
+        """
+    )
+  }
+
+  func testCallHasInitialIndentation() throws {
+    try assertExpandEditorPlaceholdersToTrailingClosures(
+      """
+          foo(arg: 1, closure: \(closureWithArgPlaceholder))
+      """,
+      expected: """
+            foo(arg: 1) { someInt in
+                \(stringPlaceholder)
+            }
+        """
+    )
+  }
+
+  func testCustomIndentationWidth() throws {
+    try assertExpandEditorPlaceholdersToTrailingClosures(
+      """
+      foo(arg: 1, closure: \(closureWithArgPlaceholder))
+      """,
+      expected: """
+        foo(arg: 1) { someInt in
+          \(stringPlaceholder)
+        }
+        """,
+      indentationWidth: .spaces(2)
+    )
+  }
+
+  func testCustomIndentationWidthWithInitialIndentation() throws {
+    try assertExpandEditorPlaceholdersToTrailingClosures(
+      """
+        foo(arg: 1, closure: \(closureWithArgPlaceholder))
+      """,
+      expected: """
+          foo(arg: 1) { someInt in
+            \(stringPlaceholder)
+          }
+        """,
+      indentationWidth: .spaces(2)
+    )
+  }
+
+  func testMultilineCall() throws {
+    try assertExpandEditorPlaceholdersToTrailingClosures(
+      """
+      foo(
+          arg: 1,
+          closure: \(closureWithArgPlaceholder)
+      )
+      """,
+      expected: """
+        foo(
+            arg: 1
+        ) { someInt in
+            \(stringPlaceholder)
+        }
+        """
+    )
+  }
+
+  func testMultilineIndentedCall() throws {
+    try assertExpandEditorPlaceholdersToTrailingClosures(
+      """
+          foo(
+              arg: 1,
+              closure: \(closureWithArgPlaceholder)
+          )
+      """,
+      expected: """
+            foo(
+                arg: 1
+            ) { someInt in
+                \(stringPlaceholder)
+            }
+        """
+    )
+  }
+
+  func testMultilineCallWithNoAdditionalArguments() throws {
+    try assertExpandEditorPlaceholdersToTrailingClosures(
+      """
+      foo(
+          closure: \(closureWithArgPlaceholder)
+      )
+      """,
+      expected: """
+        foo { someInt in
             \(stringPlaceholder)
         }
         """
@@ -233,6 +325,7 @@ fileprivate func assertRefactorPlaceholder(
   _ placeholder: String,
   wrap: Bool = true,
   expected: String,
+  indentationWidth: Trivia? = nil,
   file: StaticString = #file,
   line: UInt = #line
 ) throws {
@@ -245,13 +338,21 @@ fileprivate func assertRefactorPlaceholder(
     token = try XCTUnwrap(expr.as(DeclReferenceExprSyntax.self)?.baseName, file: file, line: line)
   }
 
-  try assertRefactor(token, context: (), provider: ExpandEditorPlaceholder.self, expected: [SourceEdit.replace(token, with: expected)], file: file, line: line)
+  try assertRefactor(
+    token,
+    context: ExpandEditorPlaceholder.Context(indentationWidth: indentationWidth),
+    provider: ExpandEditorPlaceholder.self,
+    expected: [SourceEdit.replace(token, with: expected)],
+    file: file,
+    line: line
+  )
 }
 
 fileprivate func assertRefactorPlaceholderCall(
   _ expr: String,
   placeholder: Int = 0,
   expected: String,
+  indentationWidth: Trivia? = nil,
   file: StaticString = #file,
   line: UInt = #line
 ) throws {
@@ -260,13 +361,21 @@ fileprivate func assertRefactorPlaceholderCall(
   let arg = call.arguments[call.arguments.index(at: placeholder)]
   let token: TokenSyntax = try XCTUnwrap(arg.expression.as(DeclReferenceExprSyntax.self), file: file, line: line).baseName
 
-  try assertRefactor(token, context: (), provider: ExpandEditorPlaceholder.self, expected: [SourceEdit.replace(call, with: expected)], file: file, line: line)
+  try assertRefactor(
+    token,
+    context: ExpandEditorPlaceholder.Context(indentationWidth: indentationWidth),
+    provider: ExpandEditorPlaceholder.self,
+    expected: [SourceEdit.replace(call, with: expected)],
+    file: file,
+    line: line
+  )
 }
 
 fileprivate func assertRefactorPlaceholderToken(
   _ expr: String,
   placeholder: Int = 0,
   expected: String,
+  indentationWidth: Trivia? = nil,
   file: StaticString = #file,
   line: UInt = #line
 ) throws {
@@ -275,12 +384,20 @@ fileprivate func assertRefactorPlaceholderToken(
   let arg = call.arguments[call.arguments.index(at: placeholder)]
   let token: TokenSyntax = try XCTUnwrap(arg.expression.as(DeclReferenceExprSyntax.self), file: file, line: line).baseName
 
-  try assertRefactor(token, context: (), provider: ExpandEditorPlaceholder.self, expected: [SourceEdit.replace(token, with: expected)], file: file, line: line)
+  try assertRefactor(
+    token,
+    context: ExpandEditorPlaceholder.Context(indentationWidth: indentationWidth),
+    provider: ExpandEditorPlaceholder.self,
+    expected: [SourceEdit.replace(token, with: expected)],
+    file: file,
+    line: line
+  )
 }
 
 fileprivate func assertExpandEditorPlaceholdersToTrailingClosures(
   _ expr: String,
   expected: String,
+  indentationWidth: Trivia? = nil,
   file: StaticString = #file,
   line: UInt = #line
 ) throws {
@@ -289,7 +406,7 @@ fileprivate func assertExpandEditorPlaceholdersToTrailingClosures(
 
   try assertRefactor(
     call,
-    context: (),
+    context: ExpandEditorPlaceholdersToTrailingClosures.Context(indentationWidth: indentationWidth),
     provider: ExpandEditorPlaceholdersToTrailingClosures.self,
     expected: [SourceEdit.replace(call, with: expected)],
     file: file,