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 } + +// 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/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 // expressions not containing numeric literals -fn(x0.d); +fn(x0.d); // pseudo-expressions containing numeric literals .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/) // Will become regex literal '/a, b/' -qux(/, !/) // Will become regex literal '/, !/' -qux(/,/) // Will become regex literal '/,/' +method(value: /hello/) +method(/hello/, world) +method(/hello/, /world/) +foo(/a, b/) // Will become regex literal '/a, b/' +qux(/, !/) // Will become regex literal '/, !/' +qux(/,/) // Will become regex literal '/,/' let g = hasSubscript[/]/2 // Will become regex literal '/]/' let h = /0; let f = 1/ // Will become the regex literal '/0; let y = 1/' let i = /^x/ // Will become the regex literal '/^x/' @@ -37,9 +37,9 @@ qux(/,/) // Will let n = #/hello/# + /world/ - #/nice/# let q = #/hello/# / 2 (#/hello/#) -method(value: #/hello/#) -method(#/hello/#, world) -method(#/hello/#, #/world/#) +method(value: #/hello/#) +method(#/hello/#, world) +method(#/hello/#, #/world/#) #/regex with #not a comment/# // multiline extended literals @@ -100,4 +100,4 @@ x+#/y/# // unterminated /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!")