JavaScript Highlighter: significantly improve performance
This commit is contained in:
parent
f30675169a
commit
6b39e90ea4
|
@ -7,6 +7,9 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
fileprivate let log = OSLog(subsystem: "space.vaccor.MongoView.JavaScriptHighlighter", category: .pointsOfInterest)
|
||||||
|
|
||||||
fileprivate let identifiers: CharacterSet = {
|
fileprivate let identifiers: CharacterSet = {
|
||||||
var set = CharacterSet.alphanumerics
|
var set = CharacterSet.alphanumerics
|
||||||
|
@ -22,27 +25,30 @@ fileprivate let operators = CharacterSet(charactersIn: "+-*/<>=")
|
||||||
fileprivate let expressionEnds = CharacterSet(charactersIn: ",]});")
|
fileprivate let expressionEnds = CharacterSet(charactersIn: ",]});")
|
||||||
|
|
||||||
class JavaScriptHighlighter {
|
class JavaScriptHighlighter {
|
||||||
private let text: String
|
private var text: String!
|
||||||
private var attributed: NSMutableAttributedString
|
private var attributed: NSMutableAttributedString!
|
||||||
private var currentIndex: String.Index!
|
private var currentIndex: String.Index!
|
||||||
private var indent = ""
|
private var _indent = ""
|
||||||
private(set) var tokens = [(token: TokenType, range: NSRange)]()
|
private(set) var tokens = [(token: TokenType, range: NSRange)]()
|
||||||
var debug = false
|
var debug = false
|
||||||
|
|
||||||
init(text: String) {
|
|
||||||
self.text = text
|
|
||||||
self.attributed = NSMutableAttributedString(attributedString: NSAttributedString(string: text))
|
|
||||||
}
|
|
||||||
|
|
||||||
init(mutableAttributed: NSMutableAttributedString) {
|
|
||||||
self.text = mutableAttributed.string
|
|
||||||
self.attributed = mutableAttributed
|
|
||||||
}
|
|
||||||
|
|
||||||
private func print(_ str: @autoclosure () -> String) {
|
private func print(_ str: @autoclosure () -> String) {
|
||||||
|
#if DEBUG
|
||||||
if debug {
|
if debug {
|
||||||
Swift.print("\(indent)\(str())")
|
Swift.print("\(_indent)\(str())")
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func indent() {
|
||||||
|
#if DEBUG
|
||||||
|
_indent += " "
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
private func outdent() {
|
||||||
|
#if DEBUG
|
||||||
|
_indent = String(_indent.dropLast(2))
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func range(from: String.Index, to: String.Index) -> NSRange {
|
private func range(from: String.Index, to: String.Index) -> NSRange {
|
||||||
|
@ -65,14 +71,28 @@ class JavaScriptHighlighter {
|
||||||
return text[currentIndex..<text.index(currentIndex, offsetBy: realLength)]
|
return text[currentIndex..<text.index(currentIndex, offsetBy: realLength)]
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
private func consume() {
|
||||||
private func consume() -> Unicode.Scalar? {
|
|
||||||
let c = peek()
|
|
||||||
currentIndex = text.index(after: currentIndex)
|
currentIndex = text.index(after: currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAndConsume() -> Unicode.Scalar? {
|
||||||
|
let c = peek()
|
||||||
|
consume()
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func highlight() {
|
func highlight(text: String) {
|
||||||
|
self.highlight(attributed: NSMutableAttributedString(attributedString: NSAttributedString(string: text)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func highlight(attributed: NSMutableAttributedString) {
|
||||||
|
self.attributed = attributed
|
||||||
|
self.text = attributed.string
|
||||||
|
self.tokens = []
|
||||||
|
|
||||||
|
os_signpost(.begin, log: log, name: "highlight")
|
||||||
|
defer { os_signpost(.end, log: log, name: "highlight") }
|
||||||
|
|
||||||
attributed.beginEditing()
|
attributed.beginEditing()
|
||||||
let fullRange = NSRange(location: 0, length: attributed.length)
|
let fullRange = NSRange(location: 0, length: attributed.length)
|
||||||
attributed.setAttributes([
|
attributed.setAttributes([
|
||||||
|
@ -88,19 +108,95 @@ class JavaScriptHighlighter {
|
||||||
attributed.endEditing()
|
attributed.endEditing()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func emit(token: TokenType, range: NSRange) {
|
func inserted(character: Unicode.Scalar, index: Int) -> Bool {
|
||||||
let color: NSColor
|
os_signpost(.begin, log: log, name: "inserted(character:index:)")
|
||||||
|
defer { os_signpost(.end, log: log, name: "inserted(character:index:)") }
|
||||||
|
|
||||||
|
if let (token, range) = tokenAndRange(fullyContaining: index) {
|
||||||
switch token {
|
switch token {
|
||||||
case .string:
|
|
||||||
color = .systemRed
|
|
||||||
case .number:
|
|
||||||
color = .systemBlue
|
|
||||||
case .punctuation:
|
|
||||||
color = .systemTeal
|
|
||||||
case .identifier:
|
case .identifier:
|
||||||
return
|
let set = range.location == index ? identifierStarts : identifiers
|
||||||
|
guard set.contains(character) else { return false }
|
||||||
|
case let .string(stringChar):
|
||||||
|
guard character != stringChar || (index > 0 && Unicode.Scalar((text as NSString).character(at: index - 1)) == "\\"),
|
||||||
|
character != "\\" else { return false }
|
||||||
|
case .number:
|
||||||
|
if character == "." && (attributed.string as NSString).substring(with: range).contains(Character(".")) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var attributes: [NSAttributedString.Key: Any] = [
|
||||||
|
.font: NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||||
|
]
|
||||||
|
if let color = token.color {
|
||||||
|
attributes[.foregroundColor] = color
|
||||||
|
}
|
||||||
|
attributed.addAttributes(attributes, range: NSRange(location: index, length: 1))
|
||||||
|
tokens = tokens.map { (token, range) in
|
||||||
|
if range.contains(index) {
|
||||||
|
return (token, NSRange(location: range.location, length: range.length + 1))
|
||||||
|
} else if range.location >= index {
|
||||||
|
return (token, NSRange(location: range.location + 1, length: range.length))
|
||||||
|
} else {
|
||||||
|
return (token, range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func removed(index: Int) -> Bool {
|
||||||
|
os_signpost(.begin, log: log, name: "removed(at:)")
|
||||||
|
defer { os_signpost(.end, log: log, name: "removed(at:)") }
|
||||||
|
|
||||||
|
if let (token, range) = tokenAndRange(fullyContaining: index) {
|
||||||
|
switch token {
|
||||||
|
case .string(_):
|
||||||
|
guard index > range.location && index < range.location + range.length - 1 else { return false }
|
||||||
|
if index > 0 && Unicode.Scalar((text as NSString).character(at: index - 1)) == "\\" {
|
||||||
|
// removed character
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .number:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tokens = tokens.map { (token, range) in
|
||||||
|
if range.contains(index) {
|
||||||
|
return (token, NSRange(location: range.location, length: range.length - 1))
|
||||||
|
} else if range.location >= index {
|
||||||
|
return (token, NSRange(location: range.location - 1, length: range.length))
|
||||||
|
} else {
|
||||||
|
return (token, range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func token(at index: Int) -> TokenType? {
|
||||||
|
for (token, range) in tokens where range.contains(index) {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tokenAndRange(fullyContaining index: Int) -> (TokenType, NSRange)? {
|
||||||
|
for (token, range) in tokens where index > range.location && index < range.location + range.length {
|
||||||
|
return (token, range)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func emit(token: TokenType, range: NSRange) {
|
||||||
|
if let color = token.color {
|
||||||
attributed.addAttribute(.foregroundColor, value: color, range: range)
|
attributed.addAttribute(.foregroundColor, value: color, range: range)
|
||||||
|
}
|
||||||
tokens.append((token, range))
|
tokens.append((token, range))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +225,8 @@ class JavaScriptHighlighter {
|
||||||
consumeDotLookup()
|
consumeDotLookup()
|
||||||
} else if char == "?" {
|
} else if char == "?" {
|
||||||
consumeTernaryExpression()
|
consumeTernaryExpression()
|
||||||
|
} else if expressionEnds.contains(char) {
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
consume()
|
consume()
|
||||||
}
|
}
|
||||||
|
@ -168,15 +266,20 @@ class JavaScriptHighlighter {
|
||||||
|
|
||||||
private func consumeString() {
|
private func consumeString() {
|
||||||
let stringStart = currentIndex!
|
let stringStart = currentIndex!
|
||||||
let startChar = consume()
|
let quote = peek()!
|
||||||
while currentIndex < text.endIndex && peek() != startChar && (currentIndex == text.startIndex || text[text.index(before: currentIndex)] != "\\") {
|
consume() // opening quote
|
||||||
|
var prevChar: Unicode.Scalar?
|
||||||
|
var char = peek()
|
||||||
|
while currentIndex < text.endIndex {
|
||||||
consume()
|
consume()
|
||||||
|
if char == quote && prevChar != "\\" {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if currentIndex < text.endIndex {
|
prevChar = char
|
||||||
consume() // string closing quote
|
char = peek()
|
||||||
}
|
}
|
||||||
print("String: \(text[stringStart..<currentIndex])")
|
print("String: \(text[stringStart..<currentIndex])")
|
||||||
emit(token: .string, range: range(from: stringStart, to: currentIndex))
|
emit(token: .string(quote), range: range(from: stringStart, to: currentIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func consumeTemplateString() {
|
private func consumeTemplateString() {
|
||||||
|
@ -185,14 +288,14 @@ class JavaScriptHighlighter {
|
||||||
func emitTemplateStringFragment() {
|
func emitTemplateStringFragment() {
|
||||||
guard stringFragmentStart != currentIndex else { return }
|
guard stringFragmentStart != currentIndex else { return }
|
||||||
print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
|
print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
|
||||||
emit(token: .string, range: range(from: stringFragmentStart!, to: currentIndex))
|
emit(token: .string("`"), range: range(from: stringFragmentStart!, to: currentIndex))
|
||||||
stringFragmentStart = currentIndex
|
stringFragmentStart = currentIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
consume() // opening `
|
consume() // opening `
|
||||||
|
|
||||||
while currentIndex < text.endIndex {
|
while let char = peek() {
|
||||||
if peek() == "$" {
|
if char == "$" {
|
||||||
emitTemplateStringFragment()
|
emitTemplateStringFragment()
|
||||||
consume() // $
|
consume() // $
|
||||||
if peek() == "{" {
|
if peek() == "{" {
|
||||||
|
@ -205,7 +308,7 @@ class JavaScriptHighlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if peek() == "`" {
|
} else if char == "`" {
|
||||||
stringFragmentStart = stringFragmentStart ?? currentIndex
|
stringFragmentStart = stringFragmentStart ?? currentIndex
|
||||||
consume() // `
|
consume() // `
|
||||||
emitTemplateStringFragment()
|
emitTemplateStringFragment()
|
||||||
|
@ -221,11 +324,11 @@ class JavaScriptHighlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func consumeTemplateStringExpression() {
|
private func consumeTemplateStringExpression() {
|
||||||
indent += " "
|
indent()
|
||||||
while currentIndex < text.endIndex && peek() != "}" {
|
while currentIndex < text.endIndex && peek() != "}" {
|
||||||
consumeExpression()
|
consumeExpression()
|
||||||
}
|
}
|
||||||
indent = String(indent.dropLast(2))
|
outdent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func consumeOperator() {
|
private func consumeOperator() {
|
||||||
|
@ -237,11 +340,11 @@ class JavaScriptHighlighter {
|
||||||
consume() // (
|
consume() // (
|
||||||
print("Opening (")
|
print("Opening (")
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
emit(token: .punctuation, range: prevCharRange())
|
||||||
indent += " "
|
indent()
|
||||||
while currentIndex < text.endIndex && peek() != ")" {
|
while currentIndex < text.endIndex && peek() != ")" {
|
||||||
consumeExpression()
|
consumeExpression()
|
||||||
}
|
}
|
||||||
indent = String(indent.dropLast(2))
|
outdent()
|
||||||
if currentIndex < text.endIndex {
|
if currentIndex < text.endIndex {
|
||||||
consume() // )
|
consume() // )
|
||||||
print("Closing )")
|
print("Closing )")
|
||||||
|
@ -253,35 +356,30 @@ class JavaScriptHighlighter {
|
||||||
consume() // {
|
consume() // {
|
||||||
print("Opening {")
|
print("Opening {")
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
emit(token: .punctuation, range: prevCharRange())
|
||||||
indent += " "
|
indent()
|
||||||
object:
|
while let char = peek() {
|
||||||
while currentIndex < text.endIndex && peek() != "}" {
|
if char == "}" {
|
||||||
consumeObjectKey()
|
|
||||||
if peek() == ":" {
|
|
||||||
consume() // :
|
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
|
||||||
consumeWhitespace()
|
|
||||||
while currentIndex < text.endIndex && peek() != "," && peek() != "}" {
|
|
||||||
consumeExpression()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
consumeWhitespace()
|
|
||||||
|
|
||||||
if peek() == "," {
|
|
||||||
consume() // ,
|
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
indent = String(indent.dropLast(2))
|
|
||||||
if currentIndex < text.endIndex {
|
|
||||||
consume() // }
|
consume() // }
|
||||||
print("Closing }")
|
print("Closing }")
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
emit(token: .punctuation, range: prevCharRange())
|
||||||
|
break
|
||||||
|
} else if char == "'" || char == "\"" {
|
||||||
|
let keyStart = currentIndex!
|
||||||
|
consumeString()
|
||||||
|
print("Object key: '\(text[keyStart..<currentIndex])'")
|
||||||
|
} else if identifierStarts.contains(char) {
|
||||||
|
let keyStart = currentIndex!
|
||||||
|
consumeIdentifier()
|
||||||
|
print("Object key: '\(text[keyStart..<currentIndex])'")
|
||||||
|
} else if char == ":" || char == "," {
|
||||||
|
consume() // :
|
||||||
|
emit(token: .punctuation, range: prevCharRange())
|
||||||
|
} else {
|
||||||
|
consumeExpression()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
outdent()
|
||||||
|
}
|
||||||
|
|
||||||
private func consumeObjectKey() {
|
private func consumeObjectKey() {
|
||||||
consumeWhitespace()
|
consumeWhitespace()
|
||||||
|
@ -317,35 +415,28 @@ class JavaScriptHighlighter {
|
||||||
consume() // [
|
consume() // [
|
||||||
print("Opening [")
|
print("Opening [")
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
emit(token: .punctuation, range: prevCharRange())
|
||||||
indent += " "
|
indent()
|
||||||
array:
|
while let char = peek() {
|
||||||
while currentIndex < text.endIndex {
|
if char == "]" {
|
||||||
consumeWhitespace()
|
|
||||||
if peek() == "," {
|
|
||||||
consume() // ,
|
|
||||||
print("Array separator")
|
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
|
||||||
} else if peek() == "]" {
|
|
||||||
break array
|
|
||||||
} else {
|
|
||||||
print("Array element")
|
|
||||||
while let char = peek(), char != ",", char != "]" {
|
|
||||||
consumeExpression()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
indent = String(indent.dropLast(2))
|
|
||||||
if currentIndex < text.endIndex {
|
|
||||||
consume() // ]
|
consume() // ]
|
||||||
print("Closing ]")
|
print("Closing ]")
|
||||||
emit(token: .punctuation, range: prevCharRange())
|
emit(token: .punctuation, range: prevCharRange())
|
||||||
|
break
|
||||||
|
} else if char == "," {
|
||||||
|
consume() // ,
|
||||||
|
print("Array separator")
|
||||||
|
emit(token: .punctuation, range: prevCharRange())
|
||||||
|
} else {
|
||||||
|
consumeExpression()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
outdent()
|
||||||
|
}
|
||||||
|
|
||||||
func consumeTernaryExpression() {
|
func consumeTernaryExpression() {
|
||||||
consume() // ?
|
consume() // ?
|
||||||
print("Ternary expression")
|
print("Ternary expression")
|
||||||
indent += " "
|
indent()
|
||||||
print("Ternary true result")
|
print("Ternary true result")
|
||||||
while let char = peek(), char != ":" {
|
while let char = peek(), char != ":" {
|
||||||
consumeExpression() // true result
|
consumeExpression() // true result
|
||||||
|
@ -355,7 +446,7 @@ class JavaScriptHighlighter {
|
||||||
while let char = peek(), !expressionEnds.contains(char) {
|
while let char = peek(), !expressionEnds.contains(char) {
|
||||||
consumeExpression()
|
consumeExpression()
|
||||||
}
|
}
|
||||||
indent = String(indent.dropLast(2))
|
outdent()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -365,6 +456,19 @@ extension JavaScriptHighlighter {
|
||||||
case identifier
|
case identifier
|
||||||
case punctuation
|
case punctuation
|
||||||
case number
|
case number
|
||||||
case string
|
case string(Unicode.Scalar)
|
||||||
|
|
||||||
|
var color: NSColor? {
|
||||||
|
switch self {
|
||||||
|
case .string(_):
|
||||||
|
return .systemRed
|
||||||
|
case .number:
|
||||||
|
return .systemBlue
|
||||||
|
case .punctuation:
|
||||||
|
return .systemTeal
|
||||||
|
case .identifier:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,17 +10,30 @@ import AppKit
|
||||||
|
|
||||||
class JavaScriptEditorView: NSTextView {
|
class JavaScriptEditorView: NSTextView {
|
||||||
|
|
||||||
var highlighter: JavaScriptHighlighter?
|
var highlighter = JavaScriptHighlighter()
|
||||||
|
private var isRehighlighting = false
|
||||||
|
|
||||||
override var string: String {
|
override var string: String {
|
||||||
didSet {
|
get {
|
||||||
|
super.string
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
isRehighlighting = true
|
||||||
|
super.string = newValue
|
||||||
rehighlight()
|
rehighlight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rehighlight() {
|
func rehighlight() {
|
||||||
highlighter = JavaScriptHighlighter(mutableAttributed: self.textStorage!)
|
isRehighlighting = true
|
||||||
highlighter!.highlight()
|
highlighter.highlight(attributed: self.textStorage!)
|
||||||
|
isRehighlighting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
textStorage!.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
override func shouldChangeText(in affectedCharRange: NSRange, replacementString: String?) -> Bool {
|
override func shouldChangeText(in affectedCharRange: NSRange, replacementString: String?) -> Bool {
|
||||||
|
@ -38,16 +51,11 @@ class JavaScriptEditorView: NSTextView {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didChangeText() {
|
|
||||||
rehighlight()
|
|
||||||
|
|
||||||
super.didChangeText()
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryAutocompleteCharacter(for range: NSRange, string inserted: String) -> Bool {
|
func tryAutocompleteCharacter(for range: NSRange, string inserted: String) -> Bool {
|
||||||
if let end = autocompleteResultFor(string: inserted, in: range) {
|
if let end = autocompleteResultFor(string: inserted, in: range) {
|
||||||
textStorage!.insert(NSAttributedString(string: "\(inserted)\(end)"), at: range.location)
|
textStorage!.insert(NSAttributedString(string: "\(inserted)\(end)"), at: range.location)
|
||||||
didChangeText()
|
rehighlight()
|
||||||
|
super.didChangeText()
|
||||||
setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
@ -58,28 +66,57 @@ class JavaScriptEditorView: NSTextView {
|
||||||
private func autocompleteResultFor(string: String, in range: NSRange) -> String? {
|
private func autocompleteResultFor(string: String, in range: NSRange) -> String? {
|
||||||
switch string {
|
switch string {
|
||||||
case "'", "\"", "`":
|
case "'", "\"", "`":
|
||||||
return token(at: range.location) != .string ? string : nil
|
if case .string(_) = highlighter.token(at: range.location) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return string
|
||||||
|
}
|
||||||
case "(":
|
case "(":
|
||||||
return token(at: range.location) != .string ? ")" : nil
|
if case .string(_) = highlighter.token(at: range.location) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return ")"
|
||||||
|
}
|
||||||
case "[":
|
case "[":
|
||||||
return token(at: range.location) != .string ? "]" : nil
|
if case .string(_) = highlighter.token(at: range.location) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return "]"
|
||||||
|
}
|
||||||
case "{":
|
case "{":
|
||||||
var prevChar: Unicode.Scalar?
|
|
||||||
if range.location > 0 {
|
if range.location > 0 {
|
||||||
let index = self.string.index(self.string.startIndex, offsetBy: range.location - 1)
|
let index = self.string.index(self.string.startIndex, offsetBy: range.location - 1)
|
||||||
prevChar = self.string.unicodeScalars[index]
|
let prevChar = self.string.unicodeScalars[index]
|
||||||
|
if prevChar == "$" {
|
||||||
|
return "}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if case .string(_) = highlighter.token(at: range.location) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return "}"
|
||||||
}
|
}
|
||||||
return token(at: range.location) != .string || (prevChar == "$") ? "}" : nil
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func token(at index: Int) -> JavaScriptHighlighter.TokenType? {
|
|
||||||
guard let highlighter = highlighter else { return nil }
|
|
||||||
for (token, range) in highlighter.tokens where range.contains(index) {
|
|
||||||
return token
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
extension JavaScriptEditorView: NSTextStorageDelegate {
|
||||||
|
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {
|
||||||
|
guard editedMask.contains(.editedCharacters), !isRehighlighting else { return }
|
||||||
|
if delta == 1 {
|
||||||
|
if let char = Unicode.Scalar((textStorage.string as NSString).character(at: editedRange.location)),
|
||||||
|
highlighter.inserted(character: char, index: editedRange.location) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if delta == -1 {
|
||||||
|
if highlighter.removed(index: editedRange.location) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rehighlight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue