JavaScript editor: add autocompletion for ', ", `, (, [, and {
This commit is contained in:
parent
0b4a09433b
commit
2088a91098
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import AppKit
|
|||
|
||||
class JavaScriptEditorView: NSTextView {
|
||||
|
||||
var highlighter: JavaScriptHighlighter?
|
||||
|
||||
override var string: String {
|
||||
didSet {
|
||||
rehighlight()
|
||||
|
@ -17,7 +19,23 @@ 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() {
|
||||
|
@ -25,4 +43,43 @@ class JavaScriptEditorView: NSTextView {
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
let source = "a ? 'foo' : b ? 'bar' : 'baz'"
|
||||
let source = "`$foo ${blah} bar`"
|
||||
_ = JavaScriptHighlighter(text: source).highlight()
|
||||
|
||||
|
|
Loading…
Reference in New Issue