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() {
|
|
|
|
var delimiters = CharacterSet.alphanumerics.inverted
|
|
|
|
delimiters.remove("_")
|
|
|
|
delimiters.remove("\"")
|
|
|
|
delimiters.remove("#")
|
2019-03-24 11:35:33 +01: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()
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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",
|
|
|
|
"dynamic"
|
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
|
|
|
]
|
|
|
|
|
|
|
|
struct PreprocessingRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .preprocessing }
|
|
|
|
private let tokens = ["#if", "#endif", "#elseif", "#else"]
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
|
|
|
if segment.tokens.current.isAny(of: tokens) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return segment.tokens.onSameLine.contains(anyOf: tokens)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct CommentRule: SyntaxRule {
|
|
|
|
var tokenType: TokenType { return .comment }
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
|
|
|
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 {
|
2019-03-24 11:35:33 +01:00
|
|
|
return segment.tokens.current.hasPrefix("@")
|
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-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-03-12 00:21:25 +01:00
|
|
|
guard segment.isWithinStringLiteral(withStart: "\"", end: "\"") else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return !segment.isWithinStringInterpolation
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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>
|
2018-08-24 18:42:07 +02:00
|
|
|
private let controlFlowTokens = ["if", "&&", "||", "for"]
|
|
|
|
|
|
|
|
init() {
|
|
|
|
var keywordsToAvoid = keywords
|
|
|
|
keywordsToAvoid.remove("return")
|
|
|
|
keywordsToAvoid.remove("try")
|
|
|
|
keywordsToAvoid.remove("throw")
|
|
|
|
keywordsToAvoid.remove("if")
|
|
|
|
self.keywordsToAvoid = keywordsToAvoid
|
2019-03-08 21:50:07 +01:00
|
|
|
|
|
|
|
var callLikeKeywords = accessControlKeywords
|
|
|
|
callLikeKeywords.insert("subscript")
|
|
|
|
self.callLikeKeywords = callLikeKeywords
|
2018-08-24 18:42:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func matches(_ segment: Segment) -> Bool {
|
|
|
|
guard segment.tokens.current.startsWithLetter else {
|
|
|
|
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
|
|
|
|
guard !segment.prefixedByDotAccess else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle trailing closure syntax
|
|
|
|
guard segment.trailingWhitespace == nil else {
|
|
|
|
guard segment.tokens.next.isAny(of: "{", "{}") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
guard !keywords.contains(segment.tokens.current) else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return !segment.tokens.onSameLine.contains(anyOf: controlFlowTokens)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check so that this is an initializer call, not the declaration
|
|
|
|
if segment.tokens.current == "init" {
|
|
|
|
guard segment.tokens.previous == "." else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
if segment.tokens.next == ":" {
|
2019-03-09 13:02:33 +01:00
|
|
|
// Nil pattern matching inside of a switch statement case
|
|
|
|
if segment.tokens.current == "nil" {
|
|
|
|
guard let previousToken = segment.tokens.previous else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return previousToken.isAny(of: "case", ",")
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
guard segment.tokens.current == "default" else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-27 19:01:57 +02:00
|
|
|
if let previousToken = segment.tokens.previous {
|
2019-03-08 21:50:07 +01:00
|
|
|
// 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
|
|
|
// Don't highlight most keywords when used as a parameter label
|
2019-03-08 18:32:12 +01:00
|
|
|
if !segment.tokens.current.isAny(of: "_", "self", "let", "var", "true", "false") {
|
2019-03-31 22:52:09 +02:00
|
|
|
guard !previousToken.isAny(of: "(", ",", ">(") else {
|
2018-08-27 19:01:57 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2019-03-08 17:55:44 +01:00
|
|
|
|
|
|
|
guard !segment.tokens.previous.isAny(of: "func", "`") else {
|
|
|
|
return false
|
|
|
|
}
|
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 }
|
|
|
|
|
2018-08-27 00:10:08 +02:00
|
|
|
private let declarationKeywords: Set<String> = [
|
|
|
|
"class", "struct", "enum", "func",
|
2019-03-08 18:43:12 +01:00
|
|
|
"protocol", "typealias", "import",
|
|
|
|
"associatedtype"
|
2018-08-27 00:10:08 +02:00
|
|
|
]
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
guard segment.tokens.current.isCapitalized else {
|
|
|
|
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:
|
|
|
|
guard !segment.tokens.current.starts(with: "XCTAssert") else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-08-24 18:42:07 +02:00
|
|
|
// In a generic declaration, only highlight constraints
|
2018-08-27 00:10:08 +02: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-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
|
|
|
|
if foundOpeningBracket && token == ":" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
guard !keywords.contains(token) else {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if token.isAny(of: ">", "=", "==", "(") {
|
|
|
|
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 {
|
2018-08-27 18:14:17 +02:00
|
|
|
guard segment.tokens.previous.isAny(of: ".", "(.", "[.") else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
guard !segment.tokens.onSameLine.isEmpty else {
|
|
|
|
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 {
|
|
|
|
guard !segment.tokens.onSameLine.isEmpty else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-08-25 12:56:34 +02:00
|
|
|
guard segment.tokens.previous.isAny(of: ".", "?.", "().", ").") else {
|
2018-08-24 18:42:07 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-08-27 18:44:07 +02:00
|
|
|
guard segment.tokens.current != "self" 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
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
guard previousToken != "\\" else {
|
|
|
|
previousToken = token
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
var prefixedByDotAccess: Bool {
|
|
|
|
return tokens.previous == "(." || prefix.hasSuffix(" .")
|
|
|
|
}
|
|
|
|
}
|