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 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,7 +19,23 @@ 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() {
|
||||||
|
@ -25,4 +43,43 @@ class JavaScriptEditorView: NSTextView {
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue