JavaScript editor: add autocompletion for ', ", `, (, [, and {

This commit is contained in:
Shadowfacts 2020-04-04 13:07:54 -04:00
parent 0b4a09433b
commit 2088a91098
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
3 changed files with 84 additions and 15 deletions

View File

@ -26,6 +26,7 @@ class JavaScriptHighlighter {
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)]()
init(text: String) { init(text: String) {
self.text = text self.text = text
@ -94,6 +95,7 @@ class JavaScriptHighlighter {
return return
} }
attributed.addAttribute(.foregroundColor, value: color, range: range) attributed.addAttribute(.foregroundColor, value: color, range: range)
tokens.append((token, range))
} }
private func consumeExpression() { private func consumeExpression() {
@ -173,32 +175,42 @@ class JavaScriptHighlighter {
private func consumeTemplateString() { private func consumeTemplateString() {
var stringFragmentStart: String.Index? = currentIndex var stringFragmentStart: String.Index? = currentIndex
func emitTemplateStringFragment() {
guard stringFragmentStart != currentIndex else { return }
print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
emit(token: .string, range: range(from: stringFragmentStart!, to: currentIndex))
stringFragmentStart = currentIndex
}
consume() // opening ` consume() // opening `
while currentIndex < text.endIndex { while currentIndex < text.endIndex {
if peek(length: 2) == "${" { if peek() == "$" {
emitTemplateStringFragment()
consume() // $ consume() // $
consume() // { if peek() == "{" {
print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'") consume() // {
emit(token: .string, range: range(from: stringFragmentStart!, to: currentIndex)) emitTemplateStringFragment()
consumeTemplateStringExpression() consumeTemplateStringExpression()
stringFragmentStart = currentIndex stringFragmentStart = currentIndex
if currentIndex < text.endIndex && peek() == "}" { if currentIndex < text.endIndex && peek() == "}" {
consume() consume() // }
}
} }
} else if peek() == "`" { } else if peek() == "`" {
stringFragmentStart = stringFragmentStart ?? currentIndex stringFragmentStart = stringFragmentStart ?? currentIndex
consume() // ` consume() // `
print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'") emitTemplateStringFragment()
emit(token: .string, range: range(from: stringFragmentStart!, to: currentIndex))
stringFragmentStart = nil stringFragmentStart = nil
break break
} else { } else {
consume() consume()
} }
} }
if let start = stringFragmentStart { if stringFragmentStart != nil {
print("Template string fragment: '\(text[start..<currentIndex])'") emitTemplateStringFragment()
emit(token: .string, range: range(from: start, to: currentIndex))
} }
} }

View File

@ -10,6 +10,8 @@ import AppKit
class JavaScriptEditorView: NSTextView { class JavaScriptEditorView: NSTextView {
var highlighter: JavaScriptHighlighter?
override var string: String { override var string: String {
didSet { didSet {
rehighlight() rehighlight()
@ -17,12 +19,67 @@ class JavaScriptEditorView: NSTextView {
} }
func rehighlight() { func rehighlight() {
JavaScriptHighlighter(mutableAttributed: self.textStorage!).highlight() highlighter = JavaScriptHighlighter(mutableAttributed: self.textStorage!)
highlighter!.highlight()
} }
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
}
override func didChangeText() { override func didChangeText() {
rehighlight() rehighlight()
super.didChangeText() super.didChangeText()
} }
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)
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 "'", "\"", "`":
return token(at: range.location) != .string ? string : nil
case "(":
return token(at: range.location) != .string ? ")" : nil
case "[":
return token(at: range.location) != .string ? "]" : nil
case "{":
var prevChar: Unicode.Scalar?
if range.location > 0 {
let index = self.string.index(self.string.startIndex, offsetBy: range.location - 1)
prevChar = self.string.unicodeScalars[index]
}
return token(at: range.location) != .string || (prevChar == "$") ? "}" : nil
default:
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
}
} }

View File

@ -8,6 +8,6 @@
import Foundation import Foundation
let source = "a ? 'foo' : b ? 'bar' : 'baz'" let source = "`$foo ${blah} bar`"
_ = JavaScriptHighlighter(text: source).highlight() _ = JavaScriptHighlighter(text: source).highlight()