Skip to content

Commit 7f4df43

Browse files
authored
Add support for Swift 5.5's concurrency features (#126)
This patch adds syntax highlighting support for the new concurrency keywords introduced in Swift 5.5 - `actor`, `async`, and `await`. It also includes supporting changes to make sure that usages of these new features/keywords are highlighted correctly, and to protect against regressions within existing Splash-highlighted code.
1 parent 7f87f19 commit 7f4df43

File tree

3 files changed

+297
-13
lines changed

3 files changed

+297
-13
lines changed

Sources/Splash/Grammar/SwiftGrammar.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ private extension SwiftGrammar {
8181
"lazy", "subscript", "defer", "inout", "while",
8282
"continue", "fallthrough", "repeat", "indirect",
8383
"deinit", "is", "#file", "#line", "#function",
84-
"dynamic", "some", "#available", "convenience", "unowned"
84+
"dynamic", "some", "#available", "convenience", "unowned",
85+
"async", "await", "actor"
8586
] as Set<String>).union(accessControlKeywords)
8687

8788
static let accessControlKeywords: Set<String> = [
@@ -91,7 +92,8 @@ private extension SwiftGrammar {
9192
static let declarationKeywords: Set<String> = [
9293
"class", "struct", "enum", "func",
9394
"protocol", "typealias", "import",
94-
"associatedtype", "subscript", "init"
95+
"associatedtype", "subscript", "init",
96+
"actor"
9597
]
9698

9799
struct PreprocessingRule: SyntaxRule {
@@ -252,6 +254,7 @@ private extension SwiftGrammar {
252254
keywordsToAvoid.remove("throw")
253255
keywordsToAvoid.remove("if")
254256
keywordsToAvoid.remove("in")
257+
keywordsToAvoid.remove("await")
255258
self.keywordsToAvoid = keywordsToAvoid
256259

257260
var callLikeKeywords = accessControlKeywords
@@ -351,14 +354,32 @@ private extension SwiftGrammar {
351354
}
352355
}
353356

354-
if let previousToken = segment.tokens.previous {
355-
// Don't highlight variables with the same name as a keyword
356-
// when used in optional binding, such as if let, guard let:
357-
if !segment.tokens.onSameLine.isEmpty, segment.tokens.current != "self" {
358-
guard !previousToken.isAny(of: "let", "var") else {
357+
if segment.trailingWhitespace == nil {
358+
if !segment.tokens.current.isAny(of: "self", "super") {
359+
guard segment.tokens.next != "." else {
359360
return false
360361
}
361362
}
363+
}
364+
365+
if let previousToken = segment.tokens.previous {
366+
if !segment.tokens.onSameLine.isEmpty {
367+
// Don't highlight variables with the same name as a keyword
368+
// when used in optional binding, such as if let, guard let:
369+
if segment.tokens.current != "self" {
370+
guard !previousToken.isAny(of: "let", "var") else {
371+
return false
372+
}
373+
374+
if segment.tokens.current == "actor" {
375+
if accessControlKeywords.contains(previousToken) {
376+
return true
377+
}
378+
379+
return previousToken.first == "@"
380+
}
381+
}
382+
}
362383

363384
if !declarationKeywords.contains(segment.tokens.current) {
364385
// Highlight the '(set)' part of setter access modifiers
@@ -376,7 +397,7 @@ private extension SwiftGrammar {
376397
}
377398

378399
// Don't highlight most keywords when used as a parameter label
379-
if !segment.tokens.current.isAny(of: "self", "let", "var", "true", "false", "inout", "nil", "try") {
400+
if !segment.tokens.current.isAny(of: "self", "let", "var", "true", "false", "inout", "nil", "try", "actor") {
380401
guard !previousToken.isAny(of: "(", ",", ">(") else {
381402
return false
382403
}
@@ -451,11 +472,7 @@ private extension SwiftGrammar {
451472
return !foundOpeningBracket
452473
}
453474

454-
guard !keywords.contains(token) else {
455-
return true
456-
}
457-
458-
if token.isAny(of: "=", "==", "(") {
475+
if token.isAny(of: "=", "==", "(", "_", "@escaping") {
459476
return true
460477
}
461478
}

Tests/SplashTests/Tests/DeclarationTests.swift

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,4 +1326,183 @@ final class DeclarationTests: SyntaxHighlighterTestCase {
13261326
.plainText("}")
13271327
])
13281328
}
1329+
1330+
func testNonThrowingAsyncFunctionDeclaration() {
1331+
let components = highlighter.highlight("func test() async {}")
1332+
1333+
XCTAssertEqual(components, [
1334+
.token("func", .keyword),
1335+
.whitespace(" "),
1336+
.plainText("test()"),
1337+
.whitespace(" "),
1338+
.token("async", .keyword),
1339+
.whitespace(" "),
1340+
.plainText("{}")
1341+
])
1342+
}
1343+
1344+
func testNonThrowingAsyncFunctionDeclarationWithReturnValue() {
1345+
let components = highlighter.highlight("func test() async -> Int { 0 }")
1346+
1347+
XCTAssertEqual(components, [
1348+
.token("func", .keyword),
1349+
.whitespace(" "),
1350+
.plainText("test()"),
1351+
.whitespace(" "),
1352+
.token("async", .keyword),
1353+
.whitespace(" "),
1354+
.plainText("->"),
1355+
.whitespace(" "),
1356+
.token("Int", .type),
1357+
.whitespace(" "),
1358+
.plainText("{"),
1359+
.whitespace(" "),
1360+
.token("0", .number),
1361+
.whitespace(" "),
1362+
.plainText("}")
1363+
])
1364+
}
1365+
1366+
func testThrowingAsyncFunctionDeclaration() {
1367+
let components = highlighter.highlight("func test() async throws {}")
1368+
1369+
XCTAssertEqual(components, [
1370+
.token("func", .keyword),
1371+
.whitespace(" "),
1372+
.plainText("test()"),
1373+
.whitespace(" "),
1374+
.token("async", .keyword),
1375+
.whitespace(" "),
1376+
.token("throws", .keyword),
1377+
.whitespace(" "),
1378+
.plainText("{}")
1379+
])
1380+
}
1381+
1382+
func testDeclaringGenericFunctionNamedAwait() {
1383+
let components = highlighter.highlight("""
1384+
func await<T>(_ function: () -> T) {}
1385+
""")
1386+
1387+
XCTAssertEqual(components, [
1388+
.token("func", .keyword),
1389+
.whitespace(" "),
1390+
.plainText("await<T>("),
1391+
.token("_", .keyword),
1392+
.whitespace(" "),
1393+
.plainText("function:"),
1394+
.whitespace(" "),
1395+
.plainText("()"),
1396+
.whitespace(" "),
1397+
.plainText("->"),
1398+
.whitespace(" "),
1399+
.token("T", .type),
1400+
.plainText(")"),
1401+
.whitespace(" "),
1402+
.plainText("{}")
1403+
])
1404+
}
1405+
1406+
func testActorDeclaration() {
1407+
let components = highlighter.highlight("""
1408+
actor MyActor {
1409+
var value = 0
1410+
func action() {}
1411+
}
1412+
""")
1413+
1414+
XCTAssertEqual(components, [
1415+
.token("actor", .keyword),
1416+
.whitespace(" "),
1417+
.plainText("MyActor"),
1418+
.whitespace(" "),
1419+
.plainText("{"),
1420+
.whitespace("\n "),
1421+
.token("var", .keyword),
1422+
.whitespace(" "),
1423+
.plainText("value"),
1424+
.whitespace(" "),
1425+
.plainText("="),
1426+
.whitespace(" "),
1427+
.token("0", .number),
1428+
.whitespace("\n "),
1429+
.token("func", .keyword),
1430+
.whitespace(" "),
1431+
.plainText("action()"),
1432+
.whitespace(" "),
1433+
.plainText("{}"),
1434+
.whitespace("\n"),
1435+
.plainText("}")
1436+
])
1437+
}
1438+
1439+
func testPublicActorDeclaration() {
1440+
let components = highlighter.highlight("public actor MyActor {}")
1441+
1442+
XCTAssertEqual(components, [
1443+
.token("public", .keyword),
1444+
.whitespace(" "),
1445+
.token("actor", .keyword),
1446+
.whitespace(" "),
1447+
.plainText("MyActor"),
1448+
.whitespace(" "),
1449+
.plainText("{}")
1450+
])
1451+
}
1452+
1453+
func testDeclaringAndMutatingLocalVariableNamedActor() {
1454+
let components = highlighter.highlight("""
1455+
let actor = Actor()
1456+
actor.position = scene.center
1457+
""")
1458+
1459+
XCTAssertEqual(components, [
1460+
.token("let", .keyword),
1461+
.whitespace(" "),
1462+
.plainText("actor"),
1463+
.whitespace(" "),
1464+
.plainText("="),
1465+
.whitespace(" "),
1466+
.token("Actor", .type),
1467+
.plainText("()"),
1468+
.whitespace("\n"),
1469+
.plainText("actor."),
1470+
.token("position", .property),
1471+
.whitespace(" "),
1472+
.plainText("="),
1473+
.whitespace(" "),
1474+
.plainText("scene."),
1475+
.token("center", .property)
1476+
])
1477+
}
1478+
1479+
func testPassingAndReferencingLocalVariableNamedActor() {
1480+
let components = highlighter.highlight("""
1481+
prepare(actor: actor)
1482+
scene.add(actor)
1483+
latestActor = actor
1484+
return actor
1485+
""")
1486+
1487+
XCTAssertEqual(components, [
1488+
.token("prepare", .call),
1489+
.plainText("(actor:"),
1490+
.whitespace(" "),
1491+
.plainText("actor)"),
1492+
.whitespace("\n"),
1493+
.plainText("scene."),
1494+
.token("add", .call),
1495+
.plainText("(actor)"),
1496+
.whitespace("\n"),
1497+
.plainText("latestActor"),
1498+
.whitespace(" "),
1499+
.plainText("="),
1500+
.whitespace(" "),
1501+
.plainText("actor"),
1502+
.whitespace("\n"),
1503+
.token("return", .keyword),
1504+
.whitespace(" "),
1505+
.plainText("actor")
1506+
])
1507+
}
13291508
}

Tests/SplashTests/Tests/StatementTests.swift

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,4 +467,92 @@ final class StatementTests: SyntaxHighlighterTestCase {
467467
.plainText("queryItems")
468468
])
469469
}
470+
471+
func testAwaitingFunctionCall() {
472+
let components = highlighter.highlight("let result = await call()")
473+
474+
XCTAssertEqual(components, [
475+
.token("let", .keyword),
476+
.whitespace(" "),
477+
.plainText("result"),
478+
.whitespace(" "),
479+
.plainText("="),
480+
.whitespace(" "),
481+
.token("await", .keyword),
482+
.whitespace(" "),
483+
.token("call", .call),
484+
.plainText("()")
485+
])
486+
}
487+
488+
func testAwaitingVariable() {
489+
let components = highlighter.highlight("let result = await value")
490+
491+
XCTAssertEqual(components, [
492+
.token("let", .keyword),
493+
.whitespace(" "),
494+
.plainText("result"),
495+
.whitespace(" "),
496+
.plainText("="),
497+
.whitespace(" "),
498+
.token("await", .keyword),
499+
.whitespace(" "),
500+
.plainText("value")
501+
])
502+
}
503+
504+
func testAwaitingAsyncSequenceElement() {
505+
let components = highlighter.highlight("for await value in sequence {}")
506+
507+
XCTAssertEqual(components, [
508+
.token("for", .keyword),
509+
.whitespace(" "),
510+
.token("await", .keyword),
511+
.whitespace(" "),
512+
.plainText("value"),
513+
.whitespace(" "),
514+
.token("in", .keyword),
515+
.whitespace(" "),
516+
.plainText("sequence"),
517+
.whitespace(" "),
518+
.plainText("{}")
519+
])
520+
}
521+
522+
func testAwaitingThrowingAsyncSequenceElement() {
523+
let components = highlighter.highlight("for try await value in sequence {}")
524+
525+
XCTAssertEqual(components, [
526+
.token("for", .keyword),
527+
.whitespace(" "),
528+
.token("try", .keyword),
529+
.whitespace(" "),
530+
.token("await", .keyword),
531+
.whitespace(" "),
532+
.plainText("value"),
533+
.whitespace(" "),
534+
.token("in", .keyword),
535+
.whitespace(" "),
536+
.plainText("sequence"),
537+
.whitespace(" "),
538+
.plainText("{}")
539+
])
540+
}
541+
542+
func testAsyncLetExpression() {
543+
let components = highlighter.highlight("async let result = call()")
544+
545+
XCTAssertEqual(components, [
546+
.token("async", .keyword),
547+
.whitespace(" "),
548+
.token("let", .keyword),
549+
.whitespace(" "),
550+
.plainText("result"),
551+
.whitespace(" "),
552+
.plainText("="),
553+
.whitespace(" "),
554+
.token("call", .call),
555+
.plainText("()")
556+
])
557+
}
470558
}

0 commit comments

Comments
 (0)