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

View File

@ -10,6 +10,8 @@ import AppKit
class JavaScriptEditorView: NSTextView {
var highlighter: JavaScriptHighlighter?
override var string: String {
didSet {
rehighlight()
@ -17,12 +19,67 @@ class JavaScriptEditorView: NSTextView {
}
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() {
rehighlight()
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
let source = "a ? 'foo' : b ? 'bar' : 'baz'"
let source = "`$foo ${blah} bar`"
_ = JavaScriptHighlighter(text: source).highlight()