Skip to content

Commit

Permalink
Merge pull request #2186 from kimdv/kimdv/add-token-syntax-editor-pla…
Browse files Browse the repository at this point in the history
…ceholder

Add token syntax editor placeholder
  • Loading branch information
kimdv committed Sep 20, 2023
2 parents ad30a7b + 19d6393 commit 2c847f8
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 2 deletions.
12 changes: 11 additions & 1 deletion Sources/SwiftParser/Declarations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1040,16 +1040,25 @@ extension Parser {
) -> RawFunctionDeclSyntax {
let (unexpectedBeforeFuncKeyword, funcKeyword) = self.eat(handle)
let unexpectedBeforeIdentifier: RawUnexpectedNodesSyntax?
let unexpectedAfterIdentifier: RawUnexpectedNodesSyntax?
let identifier: RawTokenSyntax
if self.at(anyIn: Operator.self) != nil || self.at(.exclamationMark, .prefixAmpersand) {
var name = self.currentToken.tokenText
if name.count > 1 && name.hasSuffix("<") && self.peek(isAt: .identifier) {
if !currentToken.isEditorPlaceholder && name.count > 1 && name.hasSuffix("<") && self.peek(isAt: .identifier) {
name = SyntaxText(rebasing: name.dropLast())
}
unexpectedBeforeIdentifier = nil
identifier = self.consumePrefix(name, as: .binaryOperator)
unexpectedAfterIdentifier = nil
} else {
(unexpectedBeforeIdentifier, identifier) = self.expectIdentifier(keywordRecovery: true)

if currentToken.isEditorPlaceholder {
let editorPlaceholder = self.parseAnyIdentifier()
unexpectedAfterIdentifier = RawUnexpectedNodesSyntax([editorPlaceholder], arena: self.arena)
} else {
unexpectedAfterIdentifier = nil
}
}

let genericParams: RawGenericParameterClauseSyntax?
Expand All @@ -1076,6 +1085,7 @@ extension Parser {
funcKeyword: funcKeyword,
unexpectedBeforeIdentifier,
name: identifier,
unexpectedAfterIdentifier,
genericParameterClause: genericParams,
signature: signature,
genericWhereClause: generics,
Expand Down
4 changes: 3 additions & 1 deletion Sources/SwiftParser/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,9 @@ extension Parser {

extension Parser {
mutating func parseResultType() -> RawTypeSyntax {
if self.at(prefix: "<") {
if self.currentToken.isEditorPlaceholder {
return self.parseTypeIdentifier()
} else if self.at(prefix: "<") {
let generics = self.parseGenericParameters()
let baseType = self.parseType()
return RawTypeSyntax(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
]
)
} else if let firstToken = node.first?.as(TokenSyntax.self),
!firstToken.isEditorPlaceholder,
firstToken.tokenKind.isIdentifier == true,
firstToken.presence == .present,
let previousToken = node.previousToken(viewMode: .sourceAccurate),
Expand Down
10 changes: 10 additions & 0 deletions Sources/SwiftSyntax/TokenSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ public struct TokenSyntax: SyntaxProtocol, SyntaxHashable {
return raw.totalLength
}

/// Whether the token text is an editor placeholder or not.
public var isEditorPlaceholder: Bool {
switch self.tokenKind {
case .identifier(let text):
return text.hasPrefix("<#") && text.hasSuffix("#>")

This comment has been minimized.

Copy link
@HassanTaleb90

HassanTaleb90 Sep 20, 2023

Contributor

Since Xcode takes the text between <# and #> as a placeholder, I suggest breaking line 138 after && to be:

return text.hasPrefix("<#") &&
		text.hasSuffix("#>")

Or:

let start = "<#"
let end = "#>"
return text.hasPrefix(start) && text.hasSuffix(end)
Screenshot 2023-09-20 at 6 40 10 PM
default:
return false
}
}

/// A token by itself has no structure, so we represent its structure by an
/// empty layout node.
///
Expand Down
162 changes: 162 additions & 0 deletions Tests/SwiftParserTest/DeclarationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2550,4 +2550,166 @@ final class DeclarationTests: ParserTestCase {
"""
)
}

func testDeclarationsWithPlaceholders() {
assertParse(
"""
func 1️⃣<#name#>(2️⃣<#parameters#>3️⃣) -> 4️⃣<#return type#> {
5️⃣<#function body#>
}
""",
substructure: FunctionDeclSyntax(
funcKeyword: .keyword(.func),
name: .identifier("<#name#>"),
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(
parameters: FunctionParameterListSyntax([
FunctionParameterSyntax(
firstName: .identifier("<#parameters#>"),
colon: .colonToken(presence: .missing),
type: MissingTypeSyntax(
placeholder: .identifier("<#type#>", presence: .missing)
)
)
])
),
returnClause: ReturnClauseSyntax(
type: IdentifierTypeSyntax(
name: .identifier("<#return type#>")
)
)
),
body: CodeBlockSyntax(
statements: CodeBlockItemListSyntax([
CodeBlockItemSyntax(
item: .expr(
ExprSyntax(
EditorPlaceholderExprSyntax(
placeholder: .identifier("<#function body#>")
)
)
)
)
])
)
),
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "3️⃣", message: "expected ':' and type in parameter", fixIts: ["insert ':' and type"]),
DiagnosticSpec(locationMarker: "4️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "5️⃣", message: "editor placeholder in source file"),
],
fixedSource: """
func <#name#>(<#parameters#>: <#type#>) -> <#return type#> {
<#function body#>
}
"""
)

assertParse(
"""
func test1️⃣<#name#>() {
2️⃣<#function body#>
}
""",
substructure: FunctionDeclSyntax(
funcKeyword: .keyword(.func),
name: .identifier("test"),
UnexpectedNodesSyntax([
TokenSyntax.identifier("<#name#>")
]),
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(
parameters: FunctionParameterListSyntax([])
)
),
body: CodeBlockSyntax(
statements: CodeBlockItemListSyntax([
CodeBlockItemSyntax(
item: .expr(
ExprSyntax(
EditorPlaceholderExprSyntax(
placeholder: .identifier("<#function body#>")
)
)
)
)
])
)
),
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '<#name#>' in function"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
]
)

assertParse(
"""
class 1️⃣<#name#>: 2️⃣<#super class#> {
3️⃣<#code#>
}
""",
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "3️⃣", message: "editor placeholder in source file"),
]
)

assertParse(
"""
enum 1️⃣<#name#> {
case 2️⃣<#code#>
}
""",
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
]
)

assertParse(
"""
struct 1️⃣<#name#> {
2️⃣<#code#>
}
""",
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
]
)

assertParse(
"""
protocol 1️⃣<#name#> {
2️⃣<#code#>
}
""",
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
]
)

assertParse(
"""
import 1️⃣<#name#>
""",
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file")
]
)

assertParse(
"""
typealias 1️⃣<#name#> = 2️⃣<#code#>
""",
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
]
)
}
}
10 changes: 10 additions & 0 deletions Tests/SwiftParserTest/LexerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,16 @@ public class LexerTests: ParserTestCase {
LexemeSpec(.identifier, text: "<##>", trailing: "", diagnostic: "editor placeholder in source file")
]
)
assertLexemes(
"let 1️⃣<#name#> = 2️⃣<#value#>",
lexemes: [
LexemeSpec(.keyword, text: "let", trailing: " "),
LexemeSpec(.identifier, text: "<#name#>", trailing: " ", errorLocationMarker: "1️⃣", diagnostic: "editor placeholder in source file"),
LexemeSpec(.equal, text: "=", trailing: " "),
LexemeSpec(.identifier, text: "<#value#>", errorLocationMarker: "2️⃣", diagnostic: "editor placeholder in source file"),
]
)
}
func testCommentAttribution() {
Expand Down
25 changes: 25 additions & 0 deletions Tests/SwiftParserTest/PatternTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,29 @@ final class PatternTests: ParserTestCase {
substructure: subscriptWithBindingPattern
)
}

func testPatternAsPlaceholderExpr() {
assertParse(
"let 1️⃣<#name#> = 2️⃣<#value#>",
substructure: VariableDeclSyntax(
bindingSpecifier: .keyword(.let),
bindings: [
PatternBindingSyntax(
pattern: IdentifierPatternSyntax(
identifier: .identifier("<#name#>")
),
initializer: InitializerClauseSyntax(
value: EditorPlaceholderExprSyntax(
placeholder: .identifier("<#value#>")
)
)
)
]
),
diagnostics: [
DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
]
)
}
}
88 changes: 88 additions & 0 deletions Tests/SwiftParserTest/TypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,92 @@ final class TypeTests: ParserTestCase {
substructureAfterMarker: "1️⃣"
)
}

func testTypeWithPlaceholder() {
assertParse(
"let a: 1️⃣<#T#> = x",
substructure: VariableDeclSyntax(
bindingSpecifier: .keyword(.let),
bindings: [
PatternBindingSyntax(
pattern: IdentifierPatternSyntax(identifier: .identifier("a")),
typeAnnotation: TypeAnnotationSyntax(
type: IdentifierTypeSyntax(
name: .identifier("<#T#>")
)
),
initializer: InitializerClauseSyntax(
value: DeclReferenceExprSyntax(
baseName: .identifier("x")
)
)
)
]
),
diagnostics: [
DiagnosticSpec(message: "editor placeholder in source file")
]
)

assertParse(
"let a: 1️⃣<#T#><Foo> = x",
substructure: VariableDeclSyntax(
bindingSpecifier: .keyword(.let),
bindings: [
PatternBindingSyntax(
pattern: IdentifierPatternSyntax(identifier: .identifier("a")),
typeAnnotation: TypeAnnotationSyntax(
type: IdentifierTypeSyntax(
name: .identifier("<#T#>"),
genericArgumentClause: GenericArgumentClauseSyntax(
arguments: GenericArgumentListSyntax([
GenericArgumentSyntax(
argument: IdentifierTypeSyntax(
name: .identifier("Foo")
)
)
])
)
)
),
initializer: InitializerClauseSyntax(
value: DeclReferenceExprSyntax(
baseName: .identifier("x")
)
)
)
]
),
diagnostics: [
DiagnosticSpec(message: "editor placeholder in source file")
]
)

assertParse(
"let a: [1️⃣<#T#>] = x",
substructure: VariableDeclSyntax(
bindingSpecifier: .keyword(.let),
bindings: [
PatternBindingSyntax(
pattern: IdentifierPatternSyntax(identifier: .identifier("a")),
typeAnnotation: TypeAnnotationSyntax(
type: ArrayTypeSyntax(
element: IdentifierTypeSyntax(
name: .identifier("<#T#>")
)
)
),
initializer: InitializerClauseSyntax(
value: DeclReferenceExprSyntax(
baseName: .identifier("x")
)
)
)
]
),
diagnostics: [
DiagnosticSpec(message: "editor placeholder in source file")
]
)
}
}

0 comments on commit 2c847f8

Please sign in to comment.