Skip to content

Commit 054f108

Browse files
authored
Merge pull request #2878 from rintaro/raw-editor-placeholder
Implement editor placeholder parsing logic in SwiftSyntax
2 parents c00cc6a + d908703 commit 054f108

File tree

4 files changed

+140
-116
lines changed

4 files changed

+140
-116
lines changed

Sources/SwiftRefactor/ExpandEditorPlaceholder.swift

Lines changed: 30 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
#if swift(>=6)
1414
import SwiftBasicFormat
1515
import SwiftParser
16-
public import SwiftSyntax
16+
@_spi(RawSyntax) public import SwiftSyntax
1717
import SwiftSyntaxBuilder
1818
#else
1919
import SwiftBasicFormat
2020
import SwiftParser
21-
import SwiftSyntax
21+
@_spi(RawSyntax) import SwiftSyntax
2222
import SwiftSyntaxBuilder
2323
#endif
2424

@@ -88,31 +88,26 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
8888
}
8989

9090
static func textRefactor(syntax token: TokenSyntax, in context: Context = Context()) -> [SourceEdit] {
91-
guard let placeholder = EditorPlaceholderData(token: token) else {
91+
guard let placeholder = EditorPlaceholderExpansionData(token: token) else {
9292
return []
9393
}
9494

9595
let expanded: String
96-
switch placeholder {
97-
case let .basic(text):
98-
expanded = String(text)
99-
case let .typed(text, type):
100-
if let functionType = type.as(FunctionTypeSyntax.self) {
101-
let basicFormat = BasicFormat(
102-
indentationWidth: context.indentationWidth,
103-
initialIndentation: context.initialIndentation
104-
)
105-
var formattedExpansion = functionType.closureExpansion.formatted(using: basicFormat).description
106-
// Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
107-
// format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
108-
// that might be in the middle of a line.
109-
if formattedExpansion.hasPrefix(context.initialIndentation.description) {
110-
formattedExpansion = String(formattedExpansion.dropFirst(context.initialIndentation.description.count))
111-
}
112-
expanded = formattedExpansion
113-
} else {
114-
expanded = String(text)
96+
if let functionType = placeholder.typeForExpansion?.as(FunctionTypeSyntax.self) {
97+
let basicFormat = BasicFormat(
98+
indentationWidth: context.indentationWidth,
99+
initialIndentation: context.initialIndentation
100+
)
101+
var formattedExpansion = functionType.closureExpansion.formatted(using: basicFormat).description
102+
// Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
103+
// format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
104+
// that might be in the middle of a line.
105+
if formattedExpansion.hasPrefix(context.initialIndentation.description) {
106+
formattedExpansion = String(formattedExpansion.dropFirst(context.initialIndentation.description.count))
115107
}
108+
expanded = formattedExpansion
109+
} else {
110+
expanded = placeholder.displayText
116111
}
117112

118113
return [
@@ -325,8 +320,8 @@ extension FunctionCallExprSyntax {
325320
for arg in arguments.reversed() {
326321
guard let expr = arg.expression.as(DeclReferenceExprSyntax.self),
327322
expr.baseName.isEditorPlaceholder,
328-
let data = EditorPlaceholderData(token: expr.baseName),
329-
case let .typed(_, type) = data,
323+
let data = EditorPlaceholderExpansionData(token: expr.baseName),
324+
let type = data.typeForExpansion,
330325
type.is(FunctionTypeSyntax.self)
331326
else {
332327
break
@@ -371,87 +366,27 @@ extension FunctionCallExprSyntax {
371366
}
372367
}
373368

374-
/// Placeholder text must start with '<#' and end with
375-
/// '#>'. Placeholders can be one of the following formats:
376-
/// ```
377-
/// 'T##' display-string '##' type-string ('##' type-for-expansion-string)?
378-
/// 'T##' display-and-type-string
379-
/// display-string
380-
/// ```
381-
///
382-
/// NOTE: It is required that '##' is not a valid substring of display-string
383-
/// or type-string. If this ends up not the case for some reason, we can consider
384-
/// adding escaping for '##'.
385-
@_spi(SourceKitLSP)
386-
public enum EditorPlaceholderData {
387-
case basic(text: Substring)
388-
case typed(text: Substring, type: TypeSyntax)
369+
private struct EditorPlaceholderExpansionData {
370+
let displayText: String
371+
let typeForExpansion: TypeSyntax?
389372

390373
init?(token: TokenSyntax) {
391-
self.init(text: token.text)
392-
}
393-
394-
@_spi(SourceKitLSP)
395-
public init?(text: String) {
396-
guard isPlaceholder(text) else {
374+
guard let rawData = token.rawEditorPlaceHolderData else {
397375
return nil
398376
}
399-
var text = text.dropFirst(2).dropLast(2)
400-
401-
if !text.hasPrefix("T##") {
402-
// No type information
403-
self = .basic(text: text)
404-
return
405-
}
406-
407-
// Drop 'T##'
408-
text = text.dropFirst(3)
409377

410-
var typeText: Substring
411-
(text, typeText) = split(text, separatedBy: "##")
412-
if typeText.isEmpty {
413-
// Only type information present
414-
self.init(typeText: text)
415-
return
416-
}
417-
418-
// Have type text, see if we also have expansion text
419-
420-
let expansionText: Substring
421-
(typeText, expansionText) = split(typeText, separatedBy: "##")
422-
if expansionText.isEmpty {
423-
if typeText.isEmpty {
424-
// No type information
425-
self = .basic(text: text)
426-
} else {
427-
// Only have type text, use it for the placeholder expansion
428-
self.init(typeText: typeText)
429-
}
430-
431-
return
432-
}
433-
434-
// Have expansion type text, use it for the placeholder expansion
435-
self.init(typeText: expansionText)
436-
}
437-
438-
init(typeText: Substring) {
439-
var parser = Parser(String(typeText))
440-
441-
let type: TypeSyntax = TypeSyntax.parse(from: &parser)
442-
if type.hasError {
443-
self = .basic(text: typeText)
378+
if let typeText = rawData.typeForExpansionText, !typeText.isEmpty {
379+
self.displayText = String(syntaxText: typeText)
380+
var parser = Parser(UnsafeBufferPointer(start: typeText.baseAddress, count: typeText.count))
381+
let type: TypeSyntax = TypeSyntax.parse(from: &parser)
382+
self.typeForExpansion = type.hasError ? nil : type
444383
} else {
445-
self = .typed(text: typeText, type: type)
384+
self.displayText = String(syntaxText: rawData.displayText)
385+
self.typeForExpansion = nil
446386
}
447387
}
448388
}
449389

450-
@_spi(Testing)
451-
public func isPlaceholder(_ str: String) -> Bool {
452-
return str.hasPrefix(placeholderStart) && str.hasSuffix(placeholderEnd)
453-
}
454-
455390
@_spi(Testing)
456391
public func wrapInPlaceholder(_ str: String) -> String {
457392
return placeholderStart + str + placeholderEnd
@@ -462,16 +397,5 @@ public func wrapInTypePlaceholder(_ str: String, type: String) -> String {
462397
return wrapInPlaceholder("T##" + str + "##" + type)
463398
}
464399

465-
/// Split the given string into two components on the first instance of
466-
/// `separatedBy`. The second element is empty if `separatedBy` is missing
467-
/// from the initial string.
468-
fileprivate func split(_ text: Substring, separatedBy separator: String) -> (Substring, Substring) {
469-
var rest = text
470-
while !rest.isEmpty && !rest.hasPrefix(separator) {
471-
rest = rest.dropFirst()
472-
}
473-
return (text.dropLast(rest.count), rest.dropFirst(2))
474-
}
475-
476400
fileprivate let placeholderStart: String = "<#"
477401
fileprivate let placeholderEnd: String = "#>"

Sources/SwiftSyntax/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ add_swift_syntax_library(SwiftSyntax
1515
CommonAncestor.swift
1616
Convenience.swift
1717
CustomTraits.swift
18+
EditorPlaceholder.swift
1819
Identifier.swift
1920
MemoryLayout.swift
2021
MissingNodeInitializers.swift
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension TokenSyntax {
14+
/// Whether the token text is an editor placeholder or not.
15+
public var isEditorPlaceholder: Bool {
16+
rawTokenKind == .identifier && isPlaceholder(syntaxText: rawText)
17+
}
18+
19+
@_spi(RawSyntax)
20+
public var rawEditorPlaceHolderData: RawEditorPlaceholderData? {
21+
RawEditorPlaceholderData(syntaxText: rawText)
22+
}
23+
}
24+
25+
/// Placeholder text must start with '<#' and end with
26+
/// '#>'. Placeholders can be one of the following formats:
27+
///
28+
/// Typed:
29+
/// ```
30+
/// 'T##' display-string '##' type-string ('##' type-for-expansion-string)?
31+
/// 'T##' display-and-type-string
32+
/// ```
33+
///
34+
/// Basic:
35+
/// ```
36+
/// display-string
37+
/// ```
38+
///
39+
/// NOTE: It is required that '##' is not a valid substring of display-string
40+
/// or type-string. If this ends up not the case for some reason, we can consider
41+
/// adding escaping for '##'.
42+
@_spi(RawSyntax)
43+
public struct RawEditorPlaceholderData {
44+
/// The part that is displayed in the editor.
45+
public var displayText: SyntaxText
46+
47+
/// The type text for the placeholder.
48+
/// It can be same as `displayText`. `nil` if the placeholder is not "Typed".
49+
public var typeText: SyntaxText?
50+
51+
/// The type text to be considered for placeholder expansion.
52+
/// It can be same as `typeText`. `nil` if the placeholder is not "Typed".
53+
public var typeForExpansionText: SyntaxText?
54+
55+
public init(displayText: SyntaxText, typeText: SyntaxText? = nil, typeForExpansionText: SyntaxText? = nil) {
56+
self.displayText = displayText
57+
self.typeText = typeText
58+
self.typeForExpansionText = typeForExpansionText
59+
}
60+
61+
public init?(syntaxText: SyntaxText) {
62+
guard isPlaceholder(syntaxText: syntaxText) else {
63+
return nil
64+
}
65+
self = parseEditorPlaceholder(syntaxText: syntaxText)
66+
}
67+
}
68+
69+
private let placeholderStart: SyntaxText = "<#"
70+
private let placeholderEnd: SyntaxText = "#>"
71+
72+
private func isPlaceholder(syntaxText: SyntaxText) -> Bool {
73+
syntaxText.hasPrefix(placeholderStart) && syntaxText.hasSuffix(placeholderEnd)
74+
}
75+
76+
private func parseEditorPlaceholder(syntaxText: SyntaxText) -> RawEditorPlaceholderData {
77+
assert(isPlaceholder(syntaxText: syntaxText))
78+
var text = SyntaxText(rebasing: syntaxText.dropFirst(2).dropLast(2))
79+
80+
if !text.hasPrefix("T##") {
81+
// Basic, no type texts.
82+
return RawEditorPlaceholderData(displayText: text)
83+
}
84+
85+
// Typed, drop 'T##'
86+
text = SyntaxText(rebasing: text.dropFirst(3))
87+
88+
guard let sep = text.firstRange(of: "##") else {
89+
return RawEditorPlaceholderData(displayText: text, typeText: text, typeForExpansionText: text)
90+
}
91+
let displayText = SyntaxText(rebasing: text[..<sep.lowerBound])
92+
text = SyntaxText(rebasing: text[sep.upperBound...])
93+
94+
guard !text.isEmpty else {
95+
return RawEditorPlaceholderData(displayText: displayText, typeText: displayText, typeForExpansionText: displayText)
96+
}
97+
98+
guard let sep = text.firstRange(of: "##") else {
99+
return RawEditorPlaceholderData(displayText: displayText, typeText: text, typeForExpansionText: text)
100+
}
101+
let typeText = SyntaxText(rebasing: text[..<sep.lowerBound])
102+
text = SyntaxText(rebasing: text[sep.upperBound...])
103+
104+
guard !text.isEmpty else {
105+
return RawEditorPlaceholderData(displayText: displayText, typeText: typeText, typeForExpansionText: typeText)
106+
}
107+
108+
return RawEditorPlaceholderData(displayText: displayText, typeText: typeText, typeForExpansionText: text)
109+
}

Sources/SwiftSyntax/TokenSyntax.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,6 @@ public struct TokenSyntax: SyntaxProtocol, SyntaxHashable {
143143
return raw.totalLength
144144
}
145145

146-
/// Whether the token text is an editor placeholder or not.
147-
public var isEditorPlaceholder: Bool {
148-
switch self.tokenKind {
149-
case .identifier(let text):
150-
return text.hasPrefix("<#") && text.hasSuffix("#>")
151-
default:
152-
return false
153-
}
154-
}
155-
156146
/// An identifier created from `self`.
157147
public var identifier: Identifier? {
158148
switch self.tokenKind {

0 commit comments

Comments
 (0)