mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-10-22 12:15:07 +02:00
163 lines
5.0 KiB
Swift
163 lines
5.0 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2023 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 SwiftParser
|
|
import SwiftRefactor
|
|
package import SwiftSyntax
|
|
|
|
/// Format a string literal by inserting or removing the appropriate number of
|
|
/// raw string `#` delimiters.
|
|
///
|
|
/// ## Before
|
|
///
|
|
/// ```swift
|
|
/// "The # of values is \(count)"
|
|
/// "Hello \#(world)"
|
|
/// ###"Hello World"###
|
|
/// ```
|
|
///
|
|
/// ## After
|
|
///
|
|
/// ```swift
|
|
/// ##"The # of values is \(count)"##
|
|
/// ##"Hello \#(world)"##
|
|
/// "Hello World"
|
|
/// ```
|
|
package struct FormatRawStringLiteral: SyntaxRefactoringProvider {
|
|
package static func refactor(syntax lit: StringLiteralExprSyntax, in context: Void) -> StringLiteralExprSyntax {
|
|
var maximumHashes = 0
|
|
for segment in lit.segments {
|
|
switch segment {
|
|
case .expressionSegment(let expr):
|
|
if let rawStringDelimiter = expr.pounds {
|
|
// Pick up any delimiters in interpolation segments \#...#(...)
|
|
maximumHashes = max(maximumHashes, rawStringDelimiter.text.longestRun(of: "#"))
|
|
}
|
|
case .stringSegment(let string):
|
|
// Find the longest run of # characters in the content of the literal.
|
|
maximumHashes = max(maximumHashes, string.content.text.longestRun(of: "#"))
|
|
#if RESILIENT_LIBRARIES
|
|
@unknown default:
|
|
fatalError()
|
|
#endif
|
|
}
|
|
}
|
|
|
|
let originalHashCount = lit.openingPounds?.text.count ?? 0
|
|
let maximumCandidateHashCount = max(maximumHashes + 1, originalHashCount)
|
|
|
|
for hashCount in 0...maximumCandidateHashCount {
|
|
let candidate = lit.withHashDelimiterCount(hashCount)
|
|
if candidate.isValidReplacement(for: lit) {
|
|
return candidate
|
|
}
|
|
}
|
|
|
|
return lit
|
|
}
|
|
}
|
|
|
|
private extension StringLiteralExprSyntax {
|
|
func withHashDelimiterCount(_ hashCount: Int) -> StringLiteralExprSyntax {
|
|
let delimiters = String(repeating: "#", count: hashCount)
|
|
let openingPounds = self.openingPounds
|
|
let closingPounds = self.closingPounds
|
|
let candidateOpeningPounds =
|
|
hashCount == 0
|
|
? nil
|
|
: TokenSyntax.rawStringPoundDelimiter(
|
|
delimiters,
|
|
leadingTrivia: openingPounds?.leadingTrivia ?? self.openingQuote.leadingTrivia
|
|
)
|
|
|
|
let candidateClosingPounds =
|
|
hashCount == 0
|
|
? nil
|
|
: TokenSyntax.rawStringPoundDelimiter(
|
|
delimiters,
|
|
trailingTrivia: closingPounds?.trailingTrivia ?? self.closingQuote.trailingTrivia
|
|
)
|
|
|
|
let candidateOpeningQuote =
|
|
hashCount == 0
|
|
? self.openingQuote.with(\.leadingTrivia, openingPounds?.leadingTrivia ?? self.openingQuote.leadingTrivia)
|
|
: self.openingQuote.with(\.leadingTrivia, [])
|
|
|
|
let candidateClosingQuote =
|
|
hashCount == 0
|
|
? self.closingQuote.with(\.trailingTrivia, closingPounds?.trailingTrivia ?? self.closingQuote.trailingTrivia)
|
|
: self.closingQuote.with(\.trailingTrivia, [])
|
|
|
|
let segments = StringLiteralSegmentListSyntax(
|
|
self.segments.map { segment in
|
|
if case let .expressionSegment(expressionSegment) = segment {
|
|
let candidatePounds =
|
|
hashCount == 0
|
|
? nil
|
|
: TokenSyntax.rawStringPoundDelimiter(
|
|
delimiters,
|
|
leadingTrivia: expressionSegment.pounds?.leadingTrivia ?? [],
|
|
trailingTrivia: expressionSegment.pounds?.trailingTrivia ?? []
|
|
)
|
|
return .expressionSegment(expressionSegment.with(\.pounds, candidatePounds))
|
|
}
|
|
return segment
|
|
}
|
|
)
|
|
|
|
return
|
|
self
|
|
.with(\.openingPounds, candidateOpeningPounds)
|
|
.with(\.openingQuote, candidateOpeningQuote)
|
|
.with(\.segments, segments)
|
|
.with(\.closingQuote, candidateClosingQuote)
|
|
.with(\.closingPounds, candidateClosingPounds)
|
|
}
|
|
|
|
func isValidReplacement(for original: StringLiteralExprSyntax) -> Bool {
|
|
let source = self.description
|
|
var parser = Parser(source)
|
|
let parsedExpr = ExprSyntax.parse(from: &parser)
|
|
|
|
guard !parsedExpr.hasError else {
|
|
return false
|
|
}
|
|
|
|
guard let parsedLiteral = parsedExpr.as(StringLiteralExprSyntax.self) else {
|
|
return false
|
|
}
|
|
|
|
return parsedLiteral.representedLiteralValue == original.representedLiteralValue
|
|
}
|
|
}
|
|
|
|
extension String {
|
|
fileprivate func longestRun(of needle: Character) -> Int {
|
|
var longest = 0
|
|
var it = self.makeIterator()
|
|
while let c = it.next() {
|
|
guard c == needle else {
|
|
continue
|
|
}
|
|
|
|
var localLongest = 1
|
|
while let c = it.next(), c == needle {
|
|
localLongest += 1
|
|
continue
|
|
}
|
|
|
|
longest = max(localLongest, longest)
|
|
}
|
|
return longest
|
|
}
|
|
}
|