diff --git a/CHANGES.md b/CHANGES.md
index c9694462fa..f77bd85cde 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -27,6 +27,7 @@ Core Grammars:
- fix(swift) ensure keyword attributes highlight correctly [Bradley Mackey][]
- fix(types) fix interface LanguageDetail > keywords [Patrick Chiu]
- enh(java) add `goto` to be recognized as a keyword in Java [Alvin Joy][]
+- enh(swift) highlight function and macro call usage [Bradley Mackey][]
New Grammars:
diff --git a/src/languages/swift.js b/src/languages/swift.js
index 0d8481a416..12aaf49aaa 100644
--- a/src/languages/swift.js
+++ b/src/languages/swift.js
@@ -11,11 +11,17 @@ import * as Swift from './lib/kws_swift.js';
import {
concat,
either,
- lookahead
+ lookahead,
+ negativeLookahead
} from '../lib/regex.js';
/** @type LanguageFn */
export default function(hljs) {
+ /**
+ * Regex for detecting a function call following an identifier.
+ */
+ const TRAILING_PAREN_REGEX = /[^\S\r\n]*\(/;
+
const WHITESPACE = {
match: /\s+/,
relevance: 0
@@ -40,9 +46,9 @@ export default function(hljs) {
],
className: { 2: "keyword" }
};
- const KEYWORD_GUARD = {
- // Consume .keyword to prevent highlighting properties and methods as keywords.
- match: concat(/\./, either(...Swift.keywords)),
+ const KEYWORD_PROP_GUARD = {
+ // Consume .keyword to prevent highlighting properties as keywords. .methods are highlighted seperately
+ match: concat(/\./, either(...Swift.keywords), negativeLookahead(TRAILING_PAREN_REGEX)),
relevance: 0
};
const PLAIN_KEYWORDS = Swift.keywords
@@ -70,24 +76,14 @@ export default function(hljs) {
};
const KEYWORD_MODES = [
DOT_KEYWORD,
- KEYWORD_GUARD,
+ KEYWORD_PROP_GUARD,
KEYWORD
];
- // https://github.com/apple/swift/tree/main/stdlib/public/core
- const BUILT_IN_GUARD = {
- // Consume .built_in to prevent highlighting properties and methods.
- match: concat(/\./, either(...Swift.builtIns)),
- relevance: 0
- };
const BUILT_IN = {
- className: 'built_in',
- match: concat(/\b/, either(...Swift.builtIns), /(?=\()/)
+ scope: 'built_in',
+ match: concat(/\b/, either(...Swift.builtIns), lookahead(TRAILING_PAREN_REGEX)),
};
- const BUILT_INS = [
- BUILT_IN_GUARD,
- BUILT_IN
- ];
// https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID418
const OPERATOR_GUARD = {
@@ -335,7 +331,7 @@ export default function(hljs) {
...COMMENTS,
REGEXP,
...KEYWORD_MODES,
- ...BUILT_INS,
+ BUILT_IN,
...OPERATORS,
NUMBER,
STRING,
@@ -390,13 +386,16 @@ export default function(hljs) {
endsParent: true,
illegal: /["']/
};
+
+ const FUNCTION_IDENT = either(QUOTED_IDENTIFIER.match, Swift.identifier, Swift.operator);
+
// https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID362
// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/#Macro-Declaration
const FUNCTION_OR_MACRO = {
match: [
/(func|macro)/,
/\s+/,
- either(QUOTED_IDENTIFIER.match, Swift.identifier, Swift.operator)
+ FUNCTION_IDENT,
],
className: {
1: "keyword",
@@ -491,6 +490,48 @@ export default function(hljs) {
]
};
+ function noneOf(list) {
+ return negativeLookahead(either(...list));
+ }
+
+ const METHODS_ONLY = [...Swift.keywords, ...Swift.builtIns];
+ const FUNCTION_CALL = {
+ relevance: 0,
+ variants: [
+ {
+ // Functions and macro calls
+ scope: "title.function",
+ keywords: KEYWORDS,
+ match: concat(
+ either(/\b/, /#/),
+ noneOf(METHODS_ONLY.map(x => concat(x, TRAILING_PAREN_REGEX))),
+ FUNCTION_IDENT,
+ lookahead(TRAILING_PAREN_REGEX),
+ ),
+ },
+ {
+ // Keywords/built-ins that only can appear as a method call
+ // e.g. foo.if()
+ match: [
+ /\./,
+ either(...METHODS_ONLY),
+ TRAILING_PAREN_REGEX,
+ ],
+ scope: {
+ 2: "title.function",
+ }
+ },
+ {
+ // Quoted methods calls, e.g. `foo`()
+ scope: "title.function",
+ match: concat(
+ QUOTED_IDENTIFIER.match,
+ lookahead(TRAILING_PAREN_REGEX),
+ )
+ }
+ ]
+ };
+
// Add supported submodes to string interpolation.
for (const variant of STRING.variants) {
const interpolation = variant.contains.find(mode => mode.label === "interpol");
@@ -498,7 +539,7 @@ export default function(hljs) {
interpolation.keywords = KEYWORDS;
const submodes = [
...KEYWORD_MODES,
- ...BUILT_INS,
+ BUILT_IN,
...OPERATORS,
NUMBER,
STRING,
@@ -535,14 +576,15 @@ export default function(hljs) {
},
REGEXP,
...KEYWORD_MODES,
- ...BUILT_INS,
+ BUILT_IN,
...OPERATORS,
NUMBER,
STRING,
- ...IDENTIFIERS,
...ATTRIBUTES,
TYPE,
- TUPLE
+ TUPLE,
+ FUNCTION_CALL,
+ ...IDENTIFIERS,
]
};
}
diff --git a/src/lib/regex.js b/src/lib/regex.js
index da6cc457c8..bea55ac767 100644
--- a/src/lib/regex.js
+++ b/src/lib/regex.js
@@ -25,6 +25,14 @@ export function lookahead(re) {
return concat('(?=', re, ')');
}
+/**
+ * @param {RegExp | string } re
+ * @returns {string}
+ */
+export function negativeLookahead(re) {
+ return concat('(?!', re, ')');
+}
+
/**
* @param {RegExp | string } re
* @returns {string}
diff --git a/test/markup/swift/functions.expect.txt b/test/markup/swift/functions.expect.txt
index 2398aaef43..70c6387990 100644
--- a/test/markup/swift/functions.expect.txt
+++ b/test/markup/swift/functions.expect.txt
@@ -17,6 +17,9 @@
p5: @attribute String? = "text"
) { }
+func `escaped`() {}
+func `if`() {}
+
init<X: A>(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
init?(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
init! (_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
@@ -30,3 +33,64 @@
static func > (lhs: Self, rhs: Self) -> Bool
static func >= (lhs: Self, rhs: Self) -> Bool
}
+
+
+obj.fn(1)
+obj.fn (1)
+obj.prop
+(1)
+
+
+abs(1)
+swap(&a, &b)
+zip(a, b)
+obj.abs(1)
+obj.swap(&a, &b)
+obj.zip(a, b)
+obj.abs (1)
+obj.abs
+(1)
+
+
+method()
+method(1)
+method(param: 1)
+obj.method()
+obj .method()
+obj.method(1)
+obj.method(param: 1)
+obj.prop.method()
+obj.prop .method()
+obj.prop.method(1)
+obj.prop.method(param: 1)
+obj.prop.method(
+ param: 1
+)
+obj.prop
+ .method()
+
+
+obj.if(condition: true)
+obj.if
+obj .if
+`if`()
+obj.`if`()
+obj.`if` ()
+`notKeyword`()
+obj.`notKeyword`()
+obj.`notKeyword` ()
+
+
+column()
+keyPath()
+sourceLocation()
+obj.column()
+obj.keyPath()
+obj.sourceLocation()
+
+
+frozen()
+discardableResult()
+obj.frozen()
+obj.discardableResult()
+
diff --git a/test/markup/swift/functions.txt b/test/markup/swift/functions.txt
index 1f8807d3c2..1849372aed 100644
--- a/test/markup/swift/functions.txt
+++ b/test/markup/swift/functions.txt
@@ -17,6 +17,9 @@ func f3(
p5: @attribute String? = "text"
) { }
+func `escaped`() {}
+func `if`() {}
+
init(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
init?(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
init! (_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
@@ -30,3 +33,64 @@ protocol Comparable: Equatable {
static func > (lhs: Self, rhs: Self) -> Bool
static func >= (lhs: Self, rhs: Self) -> Bool
}
+
+// paren spacing
+obj.fn(1)
+obj.fn (1)
+obj.prop
+(1) // newline break, this is no longer a function
+
+// builtins
+abs(1)
+swap(&a, &b)
+zip(a, b)
+obj.abs(1)
+obj.swap(&a, &b)
+obj.zip(a, b)
+obj.abs (1)
+obj.abs
+(1)
+
+// methods
+method()
+method(1)
+method(param: 1)
+obj.method()
+obj .method()
+obj.method(1)
+obj.method(param: 1)
+obj.prop.method()
+obj.prop .method()
+obj.prop.method(1)
+obj.prop.method(param: 1)
+obj.prop.method(
+ param: 1
+)
+obj.prop
+ .method()
+
+// keywords
+obj.if(condition: true)
+obj.if // variable
+obj .if // variable
+`if`()
+obj.`if`()
+obj.`if` ()
+`notKeyword`()
+obj.`notKeyword`()
+obj.`notKeyword` ()
+
+// number sign keywords are fine
+column()
+keyPath()
+sourceLocation()
+obj.column()
+obj.keyPath()
+obj.sourceLocation()
+
+// attribute keywords are fine
+frozen()
+discardableResult()
+obj.frozen()
+obj.discardableResult()
+
diff --git a/test/markup/swift/keywords.expect.txt b/test/markup/swift/keywords.expect.txt
index d9f90858e0..b8a204b0e5 100644
--- a/test/markup/swift/keywords.expect.txt
+++ b/test/markup/swift/keywords.expect.txt
@@ -19,10 +19,23 @@ x is String
isolated nonisolated
public private fileprivate package internal open
-#if
-#error("Error")
+#if DEBUG
+#error("Error")
+#elseif os(macOS)
+#error("Error")
+#elseif arch(arm64)
+#error("Error")
+#elseif compiler(>=5.0)
+#error("Error")
+#elseif canImport(UIKit)
+#error("Error")
+#elseif targetEnvironment(simulator)
+#error("Error")
#endif
-x.as(y)
-x.for(y)
+#imageLiteral(resourceName: expression)
+#fileLiteral(resourceName: expression)
+
+x.as(y)
+x.for(y)
#notAKeyword
diff --git a/test/markup/swift/keywords.txt b/test/markup/swift/keywords.txt
index 09b7da607f..528f22d8df 100644
--- a/test/markup/swift/keywords.txt
+++ b/test/markup/swift/keywords.txt
@@ -19,10 +19,23 @@ async await
isolated nonisolated
public private fileprivate package internal open
-#if
+#if DEBUG
+#error("Error")
+#elseif os(macOS)
+#error("Error")
+#elseif arch(arm64)
+#error("Error")
+#elseif compiler(>=5.0)
+#error("Error")
+#elseif canImport(UIKit)
+#error("Error")
+#elseif targetEnvironment(simulator)
#error("Error")
#endif
+#imageLiteral(resourceName: expression)
+#fileLiteral(resourceName: expression)
+
x.as(y)
x.for(y)
#notAKeyword
diff --git a/test/markup/swift/macro.expect.txt b/test/markup/swift/macro.expect.txt
index a4e780f1b1..82267107aa 100644
--- a/test/markup/swift/macro.expect.txt
+++ b/test/markup/swift/macro.expect.txt
@@ -1,7 +1,7 @@
-macro warning(_ message: String) = #externalMacro(module: "MyMacros", type: "WarningMacro")
+macro warning(_ message: String) = #externalMacro(module: "MyMacros", type: "WarningMacro")
@freestanding(declaration)
-macro error(_ message: String) = #externalMacro(module: "MyMacros", type: "ErrorMacro")
+macro error(_ message: String) = #externalMacro(module: "MyMacros", type: "ErrorMacro")
@attached(member)
macro OptionSetMembers()
@@ -9,4 +9,4 @@
@attached(peer, names: overloaded)
macro OptionSetMembers()
-#myMacro()
+#myMacro()
diff --git a/test/markup/swift/numbers.expect.txt b/test/markup/swift/numbers.expect.txt
index d022e50f9c..d32834db43 100644
--- a/test/markup/swift/numbers.expect.txt
+++ b/test/markup/swift/numbers.expect.txt
@@ -26,11 +26,11 @@
+-1
2-3
-10.magnitude
-fn(-5)
+fn(-5)
0x2.p2
-fn(x0.d);
+fn(x0.d);
.0
diff --git a/test/markup/swift/ownership.expect.txt b/test/markup/swift/ownership.expect.txt
index d04b63c1fe..b8e345c027 100644
--- a/test/markup/swift/ownership.expect.txt
+++ b/test/markup/swift/ownership.expect.txt
@@ -1,9 +1,9 @@
consume x
_ = consume y
-doStuffUniquely(with: consume x)
+doStuffUniquely(with: consume x)
copy x
_ = copy x
-doStuff(with: copy x)
+doStuff(with: copy x)
struct MoveOnly: ~Copyable {}
@@ -13,9 +13,9 @@ doStuff(with: copy x)
func foo(_: borrowing Foo)
func foo(_: consuming Foo)
func foo(_: inout Foo)
-let f: (borrowing Foo) -> Void = { a in a.foo() }
-let f: (consuming Foo) -> Void = { a in a.foo() }
-let f: (inout Foo) -> Void = { a in a.foo() }
+let f: (borrowing Foo) -> Void = { a in a.foo() }
+let f: (consuming Foo) -> Void = { a in a.foo() }
+let f: (inout Foo) -> Void = { a in a.foo() }
struct Foo {
consuming func foo()
borrowing func foo()
diff --git a/test/markup/swift/regex.expect.txt b/test/markup/swift/regex.expect.txt
index 1c14db3699..0765ca8cff 100644
--- a/test/markup/swift/regex.expect.txt
+++ b/test/markup/swift/regex.expect.txt
@@ -9,12 +9,12 @@
let n = /hello/ + /world/ - /nice/
let q = /hello/ / 2
(/hello/)
-method(value: /hello/)
-method(/hello/, world)
-method(/hello/, /world/)
-foo(/a, b/)
-qux(/, !/)
-qux(/,/)
+method(value: /hello/)
+method(/hello/, world)
+method(/hello/, /world/)
+foo(/a, b/)
+qux(/, !/)
+qux(/,/)
let g = hasSubscript[/]/2
let h = /0; let f = 1/
let i = /^x/
@@ -37,9 +37,9 @@ qux(/,/)
/something
another line
-/
\ No newline at end of file
+/
diff --git a/test/markup/swift/swiftui.expect.txt b/test/markup/swift/swiftui.expect.txt
index ce514f88fc..a053b5f703 100644
--- a/test/markup/swift/swiftui.expect.txt
+++ b/test/markup/swift/swiftui.expect.txt
@@ -2,7 +2,7 @@
struct MyApp: App {
var body: some Scene {
WindowGroup {
- #if os(iOS)
+ #if os(iOS)
Text("Hello, world from iOS!")
#else
Text("Hello, world!")