2018-08-24 18:42:07 +02:00
|
|
|
/**
|
|
|
|
* Splash
|
|
|
|
* Copyright (c) John Sundell 2018
|
|
|
|
* MIT license - see LICENSE.md
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
|
|
|
/// Grammar for the Swift language. Use this implementation when
|
|
|
|
/// highlighting Swift code. This is the default grammar.
|
|
|
|
public struct SwiftGrammar: Grammar {
|
2018-09-28 13:52:51 +02:00
|
|
|
public var delimiters: CharacterSet
|
|
|
|
public var syntaxRules: [SyntaxRule]
|
2018-08-24 18:42:07 +02:00
|
|
|
|
|
|
|
public init() {
|
2024-11-30 23:25:16 -05:00
|
|
|
var delimiters = CharacterSet.alphanumerics
|
|
|
|
// Using the .inverted property crashes on Linux
|
|
|
|
delimiters.invert()
|
2018-08-24 18:42:07 +02:00
|
|
|
delimiters.remove("_")
|
|
|
|
delimiters.remove("\"")
|
|
|
|
delimiters.remove("#")
|
2019-03-24 11:35:33 +01:00
|
|
|
delimiters.remove("@")
|
2019-07-17 10:57:30 +02:00
|
|
|
delimiters.remove("$")
|
2018-08-24 18:42:07 +02:00
|
|
|
self.delimiters = delimiters
|
|
|
|
|
|
|
|
syntaxRules = [
|
|
|
|
PreprocessingRule(),
|
|
|
|
CommentRule(),
|
2019-03-12 00:21:25 +01:00
|
|
|
RawStringRule(),
|
2018-08-24 18:42:07 +02:00
|
|
|
MultiLineStringRule(),
|
|
|
|
SingleLineStringRule(),
|
|
|
|
AttributeRule(),
|
|
|
|
NumberRule(),
|
|
|
|
TypeRule(),
|
|
|
|
CallRule(),
|
2019-03-16 12:44:02 +01:00
|
|
|
KeyPathRule(),
|
2018-08-24 18:42:07 +02:00
|
|
|
PropertyRule(),
|
|
|
|
DotAccessRule(),
|
|
|
|
KeywordRule()
|
|
|
|
]
|
|
|
|
}
|
2019-08-07 15:45:15 +02:00
|
|
|
|
|
|
|
public func isDelimiter(_ delimiterA: Character,
|
|
|
|
mergableWith delimiterB: Character) -> Bool {
|
|
|
|
switch (delimiterA, delimiterB) {
|
|
|
|
case ("\\", "("):
|
|
|
|
return true
|
|
|
|
case ("\\", _), (_, "\\"):
|
|
|
|
return false
|
|
|
|
case (")", _):
|
|
|
|
return false
|
2020-01-29 10:19:38 +01:00
|
|
|
case ("/", "/"), ("/", "*"), ("*", "/"):
|
2019-08-07 16:07:22 +02:00
|
|
|
return true
|
|
|
|
case ("/", _):
|
|
|
|
return false
|
2020-01-29 10:19:38 +01:00
|
|
|
case ("(", _) where delimiterB != ".":
|
|
|
|
return false
|
2020-06-28 13:33:09 +02:00
|
|
|
case (".", "/"), (",", "/"):
|
2020-01-29 10:19:38 +01:00
|
|
|
return false
|
2020-05-22 01:24:29 +02:00
|
|
|
case ("{", "/"), ("}", "/"):
|
|
|
|
return false
|
2020-11-14 19:20:35 +01:00
|
|
|
case ("[", "/"), ("]", "/"):
|
|
|
|
return false
|
2020-11-12 23:48:02 +01:00
|
|
|
case (">", "/"), ("?", "/"):
|
|
|
|
return false
|
2019-08-07 15:45:15 +02:00
|
|
|
default:
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private extension SwiftGrammar {
|
2019-03-08 21:50:07 +01:00
|
|
|
static let keywords = ([
|
2018-08-24 18:42:07 +02:00
|
|
|
"final", "class", "struct", "enum", "protocol",
|
|
|
|
"extension", "let", "var", "func", "typealias",
|
|
|
|
"init", "guard", "if", "else", "return", "get",
|
2019-03-12 23:12:54 +01:00
|
|
|
"throw", "throws", "rethrows", "for", "in", "open", "weak",
|
2019-03-11 21:31:32 +01:00
|
|
|
"import", "mutating", "nonmutating", "associatedtype",
|
|
|
|
"case", "switch", "static", "do", "try", "catch", "as",
|
2018-08-24 18:42:07 +02:00
|
|
|
"super", "self", "set", "true", "false", "nil",
|
|
|
|
"override", "where", "_", "default", "break",
|
|
|
|
"#selector", "required", "willSet", "didSet",
|
2018-09-05 23:36:12 +02:00
|
|
|
"lazy", "subscript", "defer", "inout", "while",
|
2019-03-08 21:57:56 +01:00
|
|
|
"continue", "fallthrough", "repeat", "indirect",
|
2019-03-11 13:57:33 +01:00
|
|
|
"deinit", "is", "#file", "#line", "#function",
|
2021-06-14 13:03:57 +02:00
|
|
|
"dynamic", "some", "#available", "convenience", "unowned",
|
2022-06-08 21:50:21 +02:00
|
|
|
"async", "await", "actor", "any"
|
2019-03-08 21:50:07 +01:00
|
|
|
] as Set<String>).union(accessControlKeywords)
|
|
|
|
|
|
|
|
static let accessControlKeywords: Set<String> = [
|
|
|
|
"public", "internal", "fileprivate", "private"
|
2018-08-24 18:42:07 +02:00
|
|
|
]
|
|
|
|
|
2020-01-29 00:37:08 +01:00
|
|
|
static let declarationKeywords: Set<String> = [
|
|
|
|
"class", "struct", "enum", "func",
|
|
|
|
"protocol", "typealias", "import",
|
2021-06-14 13:03:57 +02:00
|
|
|
"associatedtype", "subscript", "init",
|
|
|
|
"actor"
|
2020-01-29 00:37:08 +01:00
|
|
|
]
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
struct PreprocessingRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .preprocessing }
|
2019-11-04 11:31:08 +01:00
|
|
|
private let controlFlowTokens: Set<String> = ["#if", "#endif", "#elseif", "#else"]
|
|
|
|
private let directiveTokens: Set<String> = ["#warning", "#error"]
|
2018-08-24 18:42:07 +02:00
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2019-11-04 11:31:08 +01:00
|
|
|
if segment.tokens.current.isAny(of: controlFlowTokens) {
|
2018-08-24 18:42:07 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-11-04 11:31:08 +01:00
|
|
|
if segment.tokens.current.isAny(of: directiveTokens) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return segment.tokens.onSameLine.contains(anyOf: controlFlowTokens)
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct CommentRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .comment }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2020-01-29 10:19:38 +01:00
|
|
|
if segment.tokens.current.hasPrefix("/*") {
|
|
|
|
if segment.tokens.current.hasSuffix("*/") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
if segment.tokens.current.hasPrefix("//") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if segment.tokens.onSameLine.contains(anyOf: "//", "///") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-03-11 00:40:57 +01:00
|
|
|
if segment.tokens.current.isAny(of: "/*", "/**", "*/") {
|
2018-08-24 18:42:07 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-03-11 00:40:57 +01:00
|
|
|
let multiLineStartCount = segment.tokens.count(of: "/*") + segment.tokens.count(of: "/**")
|
|
|
|
return multiLineStartCount != segment.tokens.count(of: "*/")
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct AttributeRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .keyword }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2020-06-11 21:23:06 +02:00
|
|
|
if segment.tokens.current.hasPrefix("@") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if segment.tokens.previous == "." {
|
|
|
|
let suffix = segment.tokens.onSameLine.suffix(2)
|
|
|
|
|
|
|
|
guard suffix.count == 2 else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return suffix.first?.hasPrefix("@") ?? false
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-12 00:21:25 +01:00
|
|
|
struct RawStringRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .string }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2019-09-05 11:59:28 +02:00
|
|
|
guard !segment.isWithinRawStringInterpolation else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-03-12 00:34:56 +01:00
|
|
|
if segment.isWithinStringLiteral(withStart: "#\"", end: "\"#") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
let multiLineStartCount = segment.tokens.count(of: "#\"\"\"")
|
|
|
|
let multiLineEndCount = segment.tokens.count(of: "\"\"\"#")
|
|
|
|
return multiLineStartCount != multiLineEndCount
|
2019-03-12 00:21:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
struct MultiLineStringRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .string }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
|
|
|
guard !segment.tokens.count(of: "\"\"\"").isEven else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return !segment.isWithinStringInterpolation
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct SingleLineStringRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .string }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2019-09-05 14:11:58 +02:00
|
|
|
if segment.tokens.current.hasPrefix("\"") &&
|
|
|
|
segment.tokens.current.hasSuffix("\"") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-03-12 00:21:25 +01:00
|
|
|
guard segment.isWithinStringLiteral(withStart: "\"", end: "\"") else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-09-05 11:59:28 +02:00
|
|
|
return !segment.isWithinStringInterpolation &&
|
2019-09-05 14:11:58 +02:00
|
|
|
!segment.isWithinRawStringInterpolation
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct NumberRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .number }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
|
|
|
// Don't match against index-based closure arguments
|
2019-04-14 22:17:47 +02:00
|
|
|
if let previous = segment.tokens.previous {
|
|
|
|
guard !previous.hasSuffix("$") else {
|
|
|
|
return false
|
|
|
|
}
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Integers can be separated using "_", so handle that
|
|
|
|
if segment.tokens.current.removing("_").isNumber {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Double and floating point values that contain a "."
|
|
|
|
guard segment.tokens.current == "." else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let previous = segment.tokens.previous,
|
|
|
|
let next = segment.tokens.next else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return previous.isNumber && next.isNumber
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct CallRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .call }
|
|
|
|
private let keywordsToAvoid: Set<String>
|
2019-03-08 21:50:07 +01:00
|
|
|
private let callLikeKeywords: Set<String>
|
2020-04-16 19:52:26 +02:00
|
|
|
private let controlFlowTokens = ["if", "&&", "||", "for", "switch"]
|
2018-08-24 18:42:07 +02:00
|
|
|
|
|
|
|
init() {
|
|
|
|
var keywordsToAvoid = keywords
|
|
|
|
keywordsToAvoid.remove("return")
|
|
|
|
keywordsToAvoid.remove("try")
|
|
|
|
keywordsToAvoid.remove("throw")
|
|
|
|
keywordsToAvoid.remove("if")
|
2020-04-16 19:29:01 +02:00
|
|
|
keywordsToAvoid.remove("in")
|
2021-06-14 13:03:57 +02:00
|
|
|
keywordsToAvoid.remove("await")
|
2018-08-24 18:42:07 +02:00
|
|
|
self.keywordsToAvoid = keywordsToAvoid
|
2019-03-08 21:50:07 +01:00
|
|
|
|
|
|
|
var callLikeKeywords = accessControlKeywords
|
|
|
|
callLikeKeywords.insert("subscript")
|
2019-08-06 15:50:23 +02:00
|
|
|
callLikeKeywords.insert("init")
|
2019-03-08 21:50:07 +01:00
|
|
|
self.callLikeKeywords = callLikeKeywords
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2019-07-17 10:57:44 +02:00
|
|
|
let token = segment.tokens.current.trimmingCharacters(
|
|
|
|
in: CharacterSet(charactersIn: "_")
|
|
|
|
)
|
|
|
|
|
|
|
|
guard token.startsWithLetter else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-10-12 23:44:13 +02:00
|
|
|
// Never highlight initializers as regular function calls
|
|
|
|
if token == "init" {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-03-08 21:50:07 +01:00
|
|
|
// There's a few keywords that might look like function calls
|
|
|
|
if callLikeKeywords.contains(segment.tokens.current) {
|
|
|
|
if let nextToken = segment.tokens.next {
|
|
|
|
guard !nextToken.starts(with: "(") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2018-08-27 18:32:22 +02:00
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
if let previousToken = segment.tokens.previous {
|
|
|
|
guard !keywordsToAvoid.contains(previousToken) else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't treat enums with associated values as function calls
|
2019-08-06 15:50:23 +02:00
|
|
|
// when they appear within a switch statement
|
|
|
|
if previousToken == "." {
|
|
|
|
let previousTokens = segment.tokens.onSameLine
|
|
|
|
|
|
|
|
if previousTokens.count > 1 {
|
|
|
|
let lastToken = previousTokens[previousTokens.count - 2]
|
|
|
|
|
|
|
|
guard lastToken != "case" else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Multiple expressions can be matched within a single case
|
|
|
|
guard !lastToken.hasSuffix(",") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle trailing closure syntax
|
|
|
|
guard segment.trailingWhitespace == nil else {
|
|
|
|
guard segment.tokens.next.isAny(of: "{", "{}") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-10-12 23:44:13 +02:00
|
|
|
if segment.tokens.previous != "." || segment.tokens.onSameLine.isEmpty {
|
2020-10-12 23:08:25 +02:00
|
|
|
guard !keywords.contains(segment.tokens.current) else {
|
|
|
|
return false
|
|
|
|
}
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return !segment.tokens.onSameLine.contains(anyOf: controlFlowTokens)
|
|
|
|
}
|
|
|
|
|
2019-04-14 22:17:47 +02:00
|
|
|
return segment.tokens.next?.starts(with: "(") ?? false
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct KeywordRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .keyword }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2020-11-14 19:20:35 +01:00
|
|
|
if segment.tokens.current == "_" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-12-30 23:39:54 +00:00
|
|
|
if segment.tokens.current == "prefix" && segment.tokens.next == "func" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-06-08 22:13:57 +02:00
|
|
|
if segment.tokens.current.isAny(of: "some", "any") {
|
2020-02-11 16:14:53 +01:00
|
|
|
guard segment.tokens.previous != "case" else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-14 19:20:35 +01:00
|
|
|
if segment.tokens.next == ":", segment.tokens.current != "nil" {
|
2018-08-24 18:42:07 +02:00
|
|
|
guard segment.tokens.current == "default" else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 13:03:57 +02:00
|
|
|
if segment.trailingWhitespace == nil {
|
|
|
|
if !segment.tokens.current.isAny(of: "self", "super") {
|
|
|
|
guard segment.tokens.next != "." else {
|
2020-01-29 11:30:16 +01:00
|
|
|
return false
|
|
|
|
}
|
2019-03-08 21:50:07 +01:00
|
|
|
}
|
2021-06-14 13:03:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if let previousToken = segment.tokens.previous {
|
|
|
|
if !segment.tokens.onSameLine.isEmpty {
|
|
|
|
// Don't highlight variables with the same name as a keyword
|
|
|
|
// when used in optional binding, such as if let, guard let:
|
|
|
|
if segment.tokens.current != "self" {
|
|
|
|
guard !previousToken.isAny(of: "let", "var") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if segment.tokens.current == "actor" {
|
|
|
|
if accessControlKeywords.contains(previousToken) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return previousToken.first == "@"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-03-08 21:50:07 +01:00
|
|
|
|
2020-01-29 11:04:14 +01:00
|
|
|
if !declarationKeywords.contains(segment.tokens.current) {
|
|
|
|
// Highlight the '(set)' part of setter access modifiers
|
|
|
|
switch segment.tokens.current {
|
|
|
|
case "(":
|
|
|
|
return accessControlKeywords.contains(previousToken)
|
|
|
|
case "set":
|
|
|
|
if previousToken == "(" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
case ")":
|
|
|
|
return previousToken == "set"
|
|
|
|
default:
|
|
|
|
break
|
2018-08-27 19:01:57 +02:00
|
|
|
}
|
2019-03-08 17:55:44 +01:00
|
|
|
|
2020-01-29 11:04:14 +01:00
|
|
|
// Don't highlight most keywords when used as a parameter label
|
2021-06-14 13:03:57 +02:00
|
|
|
if !segment.tokens.current.isAny(of: "self", "let", "var", "true", "false", "inout", "nil", "try", "actor") {
|
2020-01-29 11:04:14 +01:00
|
|
|
guard !previousToken.isAny(of: "(", ",", ">(") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
guard !segment.tokens.previous.isAny(of: "func", "`") else {
|
|
|
|
return false
|
|
|
|
}
|
2019-03-08 17:55:44 +01:00
|
|
|
}
|
2018-08-27 19:01:57 +02:00
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
return keywords.contains(segment.tokens.current)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct TypeRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .type }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
|
|
|
// Types should not be highlighted when declared
|
|
|
|
if let previousToken = segment.tokens.previous {
|
|
|
|
guard !previousToken.isAny(of: declarationKeywords) else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-17 10:57:44 +02:00
|
|
|
let token = segment.tokens.current.trimmingCharacters(
|
|
|
|
in: CharacterSet(charactersIn: "_")
|
|
|
|
)
|
|
|
|
|
|
|
|
guard token.isCapitalized else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
guard !segment.prefixedByDotAccess else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-03-08 19:14:18 +01:00
|
|
|
// The XCTAssert family of functions is a bit of an edge case,
|
|
|
|
// since they start with capital letters. Since they are so
|
|
|
|
// commonly used, we'll add a special case for them here:
|
2019-07-17 10:57:44 +02:00
|
|
|
guard !token.starts(with: "XCTAssert") else {
|
2019-03-08 19:14:18 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
// In a generic declaration, only highlight constraints
|
2020-11-12 23:48:02 +01:00
|
|
|
if segment.tokens.previous.isAny(of: "<", ",", "*/") {
|
2019-03-08 18:20:36 +01:00
|
|
|
var foundOpeningBracket = false
|
|
|
|
|
2018-08-27 00:10:08 +02:00
|
|
|
// Since the declaration might be on another line, we have to walk
|
|
|
|
// backwards through all tokens until we've found enough information.
|
|
|
|
for token in segment.tokens.all.reversed() {
|
2019-05-13 11:25:16 +02:00
|
|
|
// Highlight return type generics as normal
|
2020-11-12 23:48:02 +01:00
|
|
|
if token.isAny(of: "->", ">", ">:") {
|
2019-05-13 11:25:16 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-03-08 18:20:36 +01:00
|
|
|
if !foundOpeningBracket && token == "<" {
|
|
|
|
foundOpeningBracket = true
|
|
|
|
}
|
|
|
|
|
2019-03-16 12:41:53 +01:00
|
|
|
// Handling generic lists for parameters, rather than declarations
|
2020-08-03 12:31:53 +02:00
|
|
|
if foundOpeningBracket {
|
2020-11-12 23:48:02 +01:00
|
|
|
if token == ":" || token.first == "@" {
|
2020-08-03 12:31:53 +02:00
|
|
|
return true
|
|
|
|
}
|
2019-03-16 12:41:53 +01:00
|
|
|
}
|
|
|
|
|
2018-08-27 00:10:08 +02:00
|
|
|
guard !declarationKeywords.contains(token) else {
|
2019-03-08 18:20:36 +01:00
|
|
|
// If it turns out that we weren't in fact inside of a generic
|
|
|
|
// declaration, (lacking "<"), then highlight the type as normal.
|
|
|
|
return !foundOpeningBracket
|
2018-08-27 00:10:08 +02:00
|
|
|
}
|
|
|
|
|
2021-06-14 13:03:57 +02:00
|
|
|
if token.isAny(of: "=", "==", "(", "_", "@escaping") {
|
2018-08-27 00:10:08 +02:00
|
|
|
return true
|
|
|
|
}
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct DotAccessRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .dotAccess }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2019-08-07 15:45:15 +02:00
|
|
|
guard !segment.tokens.onSameLine.isEmpty else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-08-07 15:45:15 +02:00
|
|
|
guard segment.isValidSymbol else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
guard segment.tokens.previous.isAny(of: ".", "(.", "[.") else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-03-08 17:26:10 +01:00
|
|
|
guard !segment.tokens.current.isAny(of: "self", "init") else {
|
2018-08-27 18:44:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
return segment.tokens.onSameLine.first != "import"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-16 12:44:02 +01:00
|
|
|
struct KeyPathRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .property }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2019-03-31 22:40:37 +02:00
|
|
|
return segment.tokens.previous.isAny(of: #"\."#, #"(\."#)
|
2019-03-16 12:44:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
struct PropertyRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .property }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
2020-11-14 19:20:35 +01:00
|
|
|
let currentToken = segment.tokens.current
|
|
|
|
|
|
|
|
if currentToken.first == "$" {
|
|
|
|
let secondIndex = currentToken.index(after: currentToken.startIndex)
|
|
|
|
|
|
|
|
guard secondIndex != currentToken.endIndex else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return currentToken[secondIndex].isLetter
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
guard !segment.tokens.onSameLine.isEmpty else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-08-07 15:45:15 +02:00
|
|
|
guard segment.isValidSymbol else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-10-12 22:27:00 +02:00
|
|
|
guard segment.tokens.previous.isAny(of: ".", "?.", "().", ").", ">.") else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-11-14 19:20:35 +01:00
|
|
|
guard !currentToken.isAny(of: "self", "init") else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-08-27 18:44:07 +02:00
|
|
|
guard !segment.prefixedByDotAccess else {
|
|
|
|
return false
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
|
2019-08-06 15:50:23 +02:00
|
|
|
if let next = segment.tokens.next {
|
|
|
|
guard !next.hasPrefix("(") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
return segment.tokens.onSameLine.first != "import"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension Segment {
|
2019-03-12 00:21:25 +01:00
|
|
|
func isWithinStringLiteral(withStart start: String, end: String) -> Bool {
|
|
|
|
if tokens.current.hasPrefix(start) {
|
2018-08-24 18:42:07 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-03-12 00:21:25 +01:00
|
|
|
if tokens.current.hasSuffix(end) {
|
2018-08-24 18:42:07 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
var markerCounts = (start: 0, end: 0)
|
|
|
|
var previousToken: String?
|
|
|
|
|
|
|
|
for token in tokens.onSameLine {
|
2020-01-29 11:09:14 +01:00
|
|
|
if token.hasPrefix("(") || token.hasPrefix("#(") || token.hasPrefix("\"") {
|
|
|
|
guard previousToken != "\\" else {
|
|
|
|
previousToken = token
|
|
|
|
continue
|
|
|
|
}
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
|
2019-03-12 00:21:25 +01:00
|
|
|
if token == start {
|
|
|
|
if start != end || markerCounts.start == markerCounts.end {
|
2018-08-24 18:42:07 +02:00
|
|
|
markerCounts.start += 1
|
|
|
|
} else {
|
|
|
|
markerCounts.end += 1
|
|
|
|
}
|
2019-03-12 00:21:25 +01:00
|
|
|
} else if token == end && start != end {
|
|
|
|
markerCounts.end += 1
|
2018-08-24 18:42:07 +02:00
|
|
|
} else {
|
2019-03-12 00:21:25 +01:00
|
|
|
if token.hasPrefix(start) {
|
2018-08-24 18:42:07 +02:00
|
|
|
markerCounts.start += 1
|
|
|
|
}
|
|
|
|
|
2019-03-12 00:21:25 +01:00
|
|
|
if token.hasSuffix(end) {
|
2018-08-24 18:42:07 +02:00
|
|
|
markerCounts.end += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
previousToken = token
|
|
|
|
}
|
|
|
|
|
|
|
|
return markerCounts.start != markerCounts.end
|
|
|
|
}
|
|
|
|
|
|
|
|
var isWithinStringInterpolation: Bool {
|
|
|
|
let delimiter = "\\("
|
|
|
|
|
|
|
|
if tokens.current == delimiter || tokens.previous == delimiter {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
let components = tokens.onSameLine.split(separator: delimiter)
|
|
|
|
|
|
|
|
guard components.count > 1 else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
let suffix = components.last!
|
|
|
|
var paranthesisCount = 1
|
|
|
|
|
|
|
|
for component in suffix {
|
|
|
|
paranthesisCount += component.numberOfOccurrences(of: "(")
|
|
|
|
paranthesisCount -= component.numberOfOccurrences(of: ")")
|
|
|
|
|
|
|
|
guard paranthesisCount > 0 else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-09-05 11:59:28 +02:00
|
|
|
var isWithinRawStringInterpolation: Bool {
|
|
|
|
// Quick fix for supporting single expressions within raw string
|
|
|
|
// interpolation, a proper fix should be developed ASAP.
|
|
|
|
switch tokens.current {
|
|
|
|
case "\\":
|
|
|
|
return tokens.previous != "\\" && tokens.next == "#"
|
|
|
|
case "#":
|
|
|
|
return tokens.previous == "\\" && tokens.next == "("
|
|
|
|
case "(":
|
|
|
|
return tokens.onSameLine.suffix(2) == ["\\", "#"]
|
|
|
|
case ")":
|
|
|
|
let suffix = tokens.onSameLine.suffix(4)
|
|
|
|
return suffix.prefix(3) == ["\\", "#", "("]
|
|
|
|
default:
|
|
|
|
let suffix = tokens.onSameLine.suffix(3)
|
|
|
|
return suffix == ["\\", "#", "("] && tokens.next == ")"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
var prefixedByDotAccess: Bool {
|
|
|
|
return tokens.previous == "(." || prefix.hasSuffix(" .")
|
|
|
|
}
|
2019-08-07 15:45:15 +02:00
|
|
|
|
|
|
|
var isValidSymbol: Bool {
|
|
|
|
guard let firstCharacter = tokens.current.first else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-06-28 13:41:09 +02:00
|
|
|
return firstCharacter == "_" || firstCharacter == "$" || firstCharacter.isLetter
|
2019-08-07 15:45:15 +02:00
|
|
|
}
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|