diff --git a/CHANGES.md b/CHANGES.md index 7e58c3474f..39e6cd9b8e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,7 @@ Language grammar improvements: - Added support for quoted identifiers, implicit parameters, and property wrapper projections - Support for more complex expressions in string interpolation - enh(swift) Improved highlighting for types and generic arguments (#2920) [Steven Van Impe][] +- enh(swift) Improved highlighting for functions, initializers, and subscripts (#2930) [Steven Van Impe][] - fix(http) avoid recursive sublanguage and tighten rules (#2893) [Josh Goebel][] - fix(asciidoc): Handle section titles level 5 (#2868) [Vaibhav Chanana][] - fix(asciidoc): Support unconstrained emphasis syntax (#2869) [Guillaume Grossetie][] diff --git a/src/languages/lib/kws_swift.js b/src/languages/lib/kws_swift.js index 77b1021017..a52ca459bc 100644 --- a/src/languages/lib/kws_swift.js +++ b/src/languages/lib/kws_swift.js @@ -52,7 +52,7 @@ export const keywords = [ 'enum', 'extension', 'fallthrough', - 'fileprivate(set)', + /fileprivate\(set\)/, 'fileprivate', 'final', // contextual 'for', @@ -66,7 +66,7 @@ export const keywords = [ /init\?/, /init!/, 'inout', - 'internal(set)', + /internal\(set\)/, 'internal', 'in', 'is', // operator @@ -74,7 +74,7 @@ export const keywords = [ 'let', 'mutating', // contextual 'nonmutating', // contextual - 'open(set)', // contextual + /open\(set\)/, // contextual 'open', // contextual 'operator', 'optional', // contextual @@ -82,10 +82,10 @@ export const keywords = [ 'postfix', // contextual 'precedencegroup', 'prefix', // contextual - 'private(set)', + /private\(set\)/, 'private', 'protocol', - 'public(set)', + /public\(set\)/, 'public', 'repeat', 'required', // contextual @@ -104,8 +104,8 @@ export const keywords = [ /try!/, // operator 'try', // operator 'typealias', - 'unowned(safe)', // contextual - 'unowned(unsafe)', // contextual + /unowned\(safe\)/, // contextual + /unowned\(unsafe\)/, // contextual 'unowned', // contextual 'var', 'weak', // contextual @@ -240,7 +240,7 @@ export const identifierHead = either( /[\u2C00-\u2DFF\u2E80-\u2FFF]/, /[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/, /[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/, - /[\uFE47-\uFFFD]/ + /[\uFE47-\uFEFE\uFF00-\uFFFD]/ // Should be /[\uFE47-\uFFFD]/, but we have to exclude FEFF. // The following characters are also allowed, but the regexes aren't supported yet. // /[\u{10000}-\u{1FFFD}\u{20000-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}]/u, // /[\u{50000}-\u{5FFFD}\u{60000-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}]/u, diff --git a/src/languages/swift.js b/src/languages/swift.js index 3b95e40676..5f8e1a7389 100644 --- a/src/languages/swift.js +++ b/src/languages/swift.js @@ -16,6 +16,10 @@ import { /** @type LanguageFn */ export default function(hljs) { + const WHITESPACE = { + match: /\s+/, + relevance: 0 + }; // https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID411 const BLOCK_COMMENT = hljs.COMMENT( '/\\*', @@ -24,6 +28,10 @@ export default function(hljs) { contains: [ 'self' ] } ); + const COMMENTS = [ + hljs.C_LINE_COMMENT_MODE, + BLOCK_COMMENT + ]; // https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID413 // https://docs.swift.org/swift-book/ReferenceManual/zzSummaryOfTheGrammar.html @@ -35,7 +43,7 @@ export default function(hljs) { }; const KEYWORD_GUARD = { // Consume .keyword to prevent highlighting properties and methods as keywords. - begin: concat(/\./, either(...Swift.keywords)), + match: concat(/\./, either(...Swift.keywords)), relevance: 0 }; const PLAIN_KEYWORDS = Swift.keywords @@ -49,14 +57,14 @@ export default function(hljs) { variants: [ { className: 'keyword', - begin: either(...REGEX_KEYWORDS, ...Swift.optionalDotKeywords) + match: either(...REGEX_KEYWORDS, ...Swift.optionalDotKeywords) } ] }; // find all the regular keywords const KEYWORDS = { $pattern: either( - /\b\w+(\(\w+\))?/, // kw or kw(arg) + /\b\w+/, // regular keywords /#\w+/ // number keywords ), keyword: PLAIN_KEYWORDS @@ -73,12 +81,12 @@ export default function(hljs) { // https://github.com/apple/swift/tree/main/stdlib/public/core const BUILT_IN_GUARD = { // Consume .built_in to prevent highlighting properties and methods. - begin: concat(/\./, either(...Swift.builtIns)), + match: concat(/\./, either(...Swift.builtIns)), relevance: 0 }; const BUILT_IN = { className: 'built_in', - begin: concat(/\b/, either(...Swift.builtIns), /(?=\()/) + match: concat(/\b/, either(...Swift.builtIns), /(?=\()/) }; const BUILT_INS = [ BUILT_IN_GUARD, @@ -88,7 +96,7 @@ export default function(hljs) { // https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID418 const OPERATOR_GUARD = { // Prevent -> from being highlighting as an operator. - begin: /->/, + match: /->/, relevance: 0 }; const OPERATOR = { @@ -96,13 +104,13 @@ export default function(hljs) { relevance: 0, variants: [ { - begin: Swift.operator + match: Swift.operator }, { // dot-operator: only operators that start with a dot are allowed to use dots as // characters (..., ...<, .*, etc). So there rule here is: a dot followed by one or more // characters that may also include dots. - begin: `\\.(\\.|${Swift.operatorCharacter})+` + match: `\\.(\\.|${Swift.operatorCharacter})+` } ] }; @@ -121,19 +129,19 @@ export default function(hljs) { variants: [ // decimal floating-point-literal (subsumes decimal-literal) { - begin: `\\b(${decimalDigits})(\\.(${decimalDigits}))?` + `([eE][+-]?(${decimalDigits}))?\\b` + match: `\\b(${decimalDigits})(\\.(${decimalDigits}))?` + `([eE][+-]?(${decimalDigits}))?\\b` }, // hexadecimal floating-point-literal (subsumes hexadecimal-literal) { - begin: `\\b0x(${hexDigits})(\\.(${hexDigits}))?` + `([pP][+-]?(${decimalDigits}))?\\b` + match: `\\b0x(${hexDigits})(\\.(${hexDigits}))?` + `([pP][+-]?(${decimalDigits}))?\\b` }, // octal-literal { - begin: /\b0o([0-7]_*)+\b/ + match: /\b0o([0-7]_*)+\b/ }, // binary-literal { - begin: /\b0b([01]_*)+\b/ + match: /\b0b([01]_*)+\b/ } ] }; @@ -143,16 +151,16 @@ export default function(hljs) { className: 'subst', variants: [ { - begin: concat(/\\/, rawDelimiter, /[0\\tnr"']/) + match: concat(/\\/, rawDelimiter, /[0\\tnr"']/) }, { - begin: concat(/\\/, rawDelimiter, /u\{[0-9a-fA-F]{1,8}\}/) + match: concat(/\\/, rawDelimiter, /u\{[0-9a-fA-F]{1,8}\}/) } ] }); const ESCAPED_NEWLINE = (rawDelimiter = "") => ({ className: 'subst', - begin: concat(/\\/, rawDelimiter, /[\t ]*(?:[\r\n]|\r\n)/) + match: concat(/\\/, rawDelimiter, /[\t ]*(?:[\r\n]|\r\n)/) }); const INTERPOLATION = (rawDelimiter = "") => ({ className: 'subst', @@ -193,15 +201,15 @@ export default function(hljs) { // https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID412 const QUOTED_IDENTIFIER = { - begin: concat(/`/, Swift.identifier, /`/) + match: concat(/`/, Swift.identifier, /`/) }; const IMPLICIT_PARAMETER = { className: 'variable', - begin: /\$\d+/ + match: /\$\d+/ }; const PROPERTY_WRAPPER_PROJECTION = { className: 'variable', - begin: `\\$${Swift.identifierCharacter}+` + match: `\\$${Swift.identifierCharacter}+` }; const IDENTIFIERS = [ QUOTED_IDENTIFIER, @@ -211,30 +219,30 @@ export default function(hljs) { // https://docs.swift.org/swift-book/ReferenceManual/Attributes.html const AVAILABLE_ATTRIBUTE = { - begin: /(@|#)available\(/, - end: /\)/, - keywords: { - $pattern: /[@#]?\w+/, - keyword: Swift.availabilityKeywords - .concat([ - "@available", - "#available" - ]) - .join(' ') - }, - contains: [ - ...OPERATORS, - NUMBER, - STRING - ] + match: /(@|#)available/, + className: "keyword", + starts: { + contains: [ + { + begin: /\(/, + end: /\)/, + keywords: Swift.availabilityKeywords.join(' '), + contains: [ + ...OPERATORS, + NUMBER, + STRING + ] + } + ] + } }; const KEYWORD_ATTRIBUTE = { className: 'keyword', - begin: concat(/@/, either(...Swift.keywordAttributes)) + match: concat(/@/, either(...Swift.keywordAttributes)) }; const USER_DEFINED_ATTRIBUTE = { className: 'meta', - begin: concat(/@/, Swift.identifier) + match: concat(/@/, Swift.identifier) }; const ATTRIBUTES = [ AVAILABLE_ATTRIBUTE, @@ -244,28 +252,28 @@ export default function(hljs) { // https://docs.swift.org/swift-book/ReferenceManual/Types.html const TYPE = { - begin: lookahead(/\b[A-Z]/), + match: lookahead(/\b[A-Z]/), relevance: 0, contains: [ { // Common Apple frameworks, for relevance boost className: 'type', - begin: concat(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/, Swift.identifierCharacter, '+') + match: concat(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/, Swift.identifierCharacter, '+') }, { // Type identifier className: 'type', - begin: Swift.typeIdentifier, + match: Swift.typeIdentifier, relevance: 0 }, { // Optional type - begin: /[?!]+/, + match: /[?!]+/, relevance: 0 }, { // Variadic parameter - begin: /\.\.\./, + match: /\.\.\./, relevance: 0 }, { // Protocol composition - begin: concat(/\s+&\s+/, lookahead(Swift.typeIdentifier)), + match: concat(/\s+&\s+/, lookahead(Swift.typeIdentifier)), relevance: 0 } ] @@ -275,6 +283,7 @@ export default function(hljs) { end: />/, keywords: KEYWORDS, contains: [ + ...COMMENTS, ...KEYWORD_MODES, ...ATTRIBUTES, OPERATOR_GUARD, @@ -283,6 +292,128 @@ export default function(hljs) { }; TYPE.contains.push(GENERIC_ARGUMENTS); + // https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#ID552 + // Prevents element names from being highlighted as keywords. + const TUPLE_ELEMENT_NAME = { + match: concat(Swift.identifier, /\s*:/), + keywords: "_|0", + relevance: 0 + }; + // Matches tuples as well as the parameter list of a function type. + const TUPLE = { + begin: /\(/, + end: /\)/, + relevance: 0, + keywords: KEYWORDS, + contains: [ + 'self', + TUPLE_ELEMENT_NAME, + ...COMMENTS, + ...KEYWORD_MODES, + ...BUILT_INS, + ...OPERATORS, + NUMBER, + STRING, + ...IDENTIFIERS, + ...ATTRIBUTES, + TYPE + ] + }; + + // https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID362 + // Matches both the keyword func and the function title. + // Grouping these lets us differentiate between the operator function < + // and the start of the generic parameter clause (also <). + const FUNC_PLUS_TITLE = { + beginKeywords: 'func', + contains: [ + { + className: 'title', + match: either(QUOTED_IDENTIFIER.match, Swift.identifier, Swift.operator), + // Required to make sure the opening < of the generic parameter clause + // isn't parsed as a second title. + endsParent: true, + relevance: 0 + }, + WHITESPACE + ] + }; + const GENERIC_PARAMETERS = { + begin: //, + contains: [ + ...COMMENTS, + TYPE + ] + }; + const FUNCTION_PARAMETER_NAME = { + begin: either( + lookahead(concat(Swift.identifier, /\s*:/)), + lookahead(concat(Swift.identifier, /\s+/, Swift.identifier, /\s*:/)) + ), + end: /:/, + relevance: 0, + contains: [ + { + className: 'keyword', + match: /\b_\b/ + }, + { + className: 'params', + match: Swift.identifier + } + ] + }; + const FUNCTION_PARAMETERS = { + begin: /\(/, + end: /\)/, + keywords: KEYWORDS, + contains: [ + FUNCTION_PARAMETER_NAME, + ...COMMENTS, + ...KEYWORD_MODES, + ...OPERATORS, + NUMBER, + STRING, + ...ATTRIBUTES, + TYPE, + TUPLE + ], + endsParent: true, + illegal: /["']/ + }; + const FUNCTION = { + className: 'function', + match: lookahead(/\bfunc\b/), + contains: [ + FUNC_PLUS_TITLE, + GENERIC_PARAMETERS, + FUNCTION_PARAMETERS, + WHITESPACE + ], + illegal: [ + /\[/, + /%/ + ] + }; + + // https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID375 + // https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID379 + const INIT_SUBSCRIPT = { + className: 'function', + match: /\b(subscript|init[?!]?)\s*(?=[<(])/, + keywords: { + keyword: "subscript init init? init!", + $pattern: /\w+[?!]?/ + }, + contains: [ + GENERIC_PARAMETERS, + FUNCTION_PARAMETERS, + WHITESPACE + ], + illegal: /\[|%/ + }; + // Add supported submodes to string interpolation. for (const variant of STRING.variants) { const interpolation = variant.contains.find(mode => mode.label === "interpol"); @@ -313,42 +444,9 @@ export default function(hljs) { name: 'Swift', keywords: KEYWORDS, contains: [ - hljs.C_LINE_COMMENT_MODE, - BLOCK_COMMENT, - { - className: 'function', - beginKeywords: 'func', - end: /\{/, - excludeEnd: true, - contains: [ - hljs.inherit(hljs.TITLE_MODE, { - begin: /[A-Za-z$_][0-9A-Za-z$_]*/ - }), - { - begin: // - }, - { - className: 'params', - begin: /\(/, - end: /\)/, - endsParent: true, - keywords: KEYWORDS, - contains: [ - 'self', - ...KEYWORD_MODES, - NUMBER, - STRING, - hljs.C_BLOCK_COMMENT_MODE, - { // relevance booster - begin: ':' - } - ], - illegal: /["']/ - } - ], - illegal: /\[|%/ - }, + ...COMMENTS, + FUNCTION, + INIT_SUBSCRIPT, { className: 'class', beginKeywords: 'struct protocol class extension enum', @@ -365,10 +463,7 @@ export default function(hljs) { { beginKeywords: 'import', end: /$/, - contains: [ - hljs.C_LINE_COMMENT_MODE, - BLOCK_COMMENT - ], + contains: [ ...COMMENTS ], relevance: 0 }, ...KEYWORD_MODES, @@ -378,7 +473,8 @@ export default function(hljs) { STRING, ...IDENTIFIERS, ...ATTRIBUTES, - TYPE + TYPE, + TUPLE ] }; } diff --git a/test/markup/swift/functions.expect.txt b/test/markup/swift/functions.expect.txt index a83afdfb6b..0a10c71ff0 100644 --- a/test/markup/swift/functions.expect.txt +++ b/test/markup/swift/functions.expect.txt @@ -1,10 +1,32 @@ -protocol Protocol { - func f1() - func f2() -} +func f1< + X, + Y: A, + // documentation + Z: B & C<D> +>() where X == Y, Y: A, Z: B & C<D> { } + +func < <T>() { } + +func f2(_ p: @escaping () throws -> Void) rethrows -> some Collection { } + +func f3( + p1e p1i: inout Int = 5, + _ p2: (x: Int, y: Int), + p3: (var: Int, let: Int) throws -> Int, + p4: Int... + p5: @attribute String? = "text" +) { } + +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)) { } + +subscript<X: A>(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { } + +protocol Comparable: Equatable { -class MyClass { - func f() { - return true - } + static func < (lhs: Self, rhs: Self) -> Bool + static func <= (lhs: Self, rhs: Self) -> Bool + static func > (lhs: Self, rhs: Self) -> Bool + static func >= (lhs: Self, rhs: Self) -> Bool } diff --git a/test/markup/swift/functions.txt b/test/markup/swift/functions.txt index cfd64aed92..1f8807d3c2 100644 --- a/test/markup/swift/functions.txt +++ b/test/markup/swift/functions.txt @@ -1,10 +1,32 @@ -protocol Protocol { - func f1() - func f2() -} +func f1< + X, + Y: A, + // documentation + Z: B & C +>() where X == Y, Y: A, Z: B & C { } + +func < () { } + +func f2(_ p: @escaping () throws -> Void) rethrows -> some Collection { } + +func f3( + p1e p1i: inout Int = 5, + _ p2: (x: Int, y: Int), + p3: (var: Int, let: Int) throws -> Int, + p4: Int... + p5: @attribute String? = "text" +) { } + +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)) { } + +subscript(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { } + +protocol Comparable: Equatable { -class MyClass { - func f() { - return true - } + static func < (lhs: Self, rhs: Self) -> Bool + static func <= (lhs: Self, rhs: Self) -> Bool + static func > (lhs: Self, rhs: Self) -> Bool + static func >= (lhs: Self, rhs: Self) -> Bool } diff --git a/test/markup/swift/keywords.expect.txt b/test/markup/swift/keywords.expect.txt index a7f03bca5e..73adf593a4 100644 --- a/test/markup/swift/keywords.expect.txt +++ b/test/markup/swift/keywords.expect.txt @@ -10,7 +10,7 @@ x as Int x as? Double x as! String x is String -init?() init!() init +init? init! init try? try! try true false nil fileprivate(set) internal(set) open(set) private(set) public(set) diff --git a/test/markup/swift/keywords.txt b/test/markup/swift/keywords.txt index 6b354c0db1..53358810d9 100644 --- a/test/markup/swift/keywords.txt +++ b/test/markup/swift/keywords.txt @@ -10,7 +10,7 @@ x as Int x as? Double x as! String x is String -init?() init!() init +init? init! init try? try! try true false nil fileprivate(set) internal(set) open(set) private(set) public(set) diff --git a/test/markup/swift/tuples.expect.txt b/test/markup/swift/tuples.expect.txt new file mode 100644 index 0000000000..6b259a3343 --- /dev/null +++ b/test/markup/swift/tuples.expect.txt @@ -0,0 +1,16 @@ +(3, "string") +(c: (x: 1, y: 1), z: 1) +(var: Array<Int>, let: Array<Double>) +(_ x: inout Int) throws -> Int +(abs(-2), abs(2)) +(x < y, a > b) +($0, $1) +(@escaping (String) -> Void, @autoclosure () -> String) -> String +( + // x + x, + /* y */ + y +) +(let x, var y) +([key: value, key: value]) diff --git a/test/markup/swift/tuples.txt b/test/markup/swift/tuples.txt new file mode 100644 index 0000000000..c66e4bd33a --- /dev/null +++ b/test/markup/swift/tuples.txt @@ -0,0 +1,16 @@ +(3, "string") +(c: (x: 1, y: 1), z: 1) +(var: Array, let: Array) +(_ x: inout Int) throws -> Int +(abs(-2), abs(2)) +(x < y, a > b) +($0, $1) +(@escaping (String) -> Void, @autoclosure () -> String) -> String +( + // x + x, + /* y */ + y +) +(let x, var y) +([key: value, key: value]) diff --git a/test/markup/swift/types.expect.txt b/test/markup/swift/types.expect.txt index 731db7c394..4bd3695140 100644 --- a/test/markup/swift/types.expect.txt +++ b/test/markup/swift/types.expect.txt @@ -41,3 +41,7 @@ Dictionary<String, Any> Dictionary<String, Array<Int>> Array<(@autoclosure () -> String) throws -> String?> +Array< + // documentation + Int +> diff --git a/test/markup/swift/types.txt b/test/markup/swift/types.txt index 0348b00e9a..89a27ea6a9 100644 --- a/test/markup/swift/types.txt +++ b/test/markup/swift/types.txt @@ -41,3 +41,7 @@ Array Dictionary Dictionary> Array<(@autoclosure () -> String) throws -> String?> +Array< + // documentation + Int +>