// // JavaScriptEditorView.swift // MongoView // // Created by Shadowfacts on 4/4/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import AppKit class JavaScriptEditorView: NSTextView { var highlighter = JavaScriptHighlighter() private var isRehighlighting = false override var string: String { get { super.string } set { isRehighlighting = true super.string = newValue rehighlight() } } func rehighlight() { isRehighlighting = true highlighter.highlight(attributed: self.textStorage!) isRehighlighting = false } override func awakeFromNib() { super.awakeFromNib() textStorage!.delegate = self } override func shouldChangeText(in affectedCharRange: NSRange, replacementString: String?) -> Bool { guard super.shouldChangeText(in: affectedCharRange, replacementString: replacementString) else { return false } if affectedCharRange.length == 0, let string = replacementString, string.count == 1, tryAutocompleteCharacter(for: affectedCharRange, string: string) { return false } return true } func tryAutocompleteCharacter(for range: NSRange, string inserted: String) -> Bool { if let end = autocompleteResultFor(string: inserted, in: range) { textStorage!.insert(NSAttributedString(string: "\(inserted)\(end)"), at: range.location) rehighlight() super.didChangeText() setSelectedRange(NSRange(location: range.location + 1, length: 0)) return true } else { return false } } private func autocompleteResultFor(string: String, in range: NSRange) -> String? { switch string { case "'", "\"", "`": if case .string(_) = highlighter.token(at: range.location) { return nil } else { return string } case "(": if case .string(_) = highlighter.token(at: range.location) { return nil } else { return ")" } case "[": if case .string(_) = highlighter.token(at: range.location) { return nil } else { return "]" } case "{": if range.location > 0 { let index = self.string.index(self.string.startIndex, offsetBy: range.location - 1) let prevChar = self.string.unicodeScalars[index] if prevChar == "$" { return "}" } } if case .string(_) = highlighter.token(at: range.location) { return nil } else { return "}" } default: 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() } }