Browse Source

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

master
Shadowfacts 3 months ago
parent
commit
2088a91098
Signed by: Shadowfacts <me@shadowfacts.net> GPG Key ID: 94A5AB95422746E5

+ 25
- 13
MongoView/Synax Highlighting/JavaScriptHighlighter.swift View File

@@ -26,6 +26,7 @@ class JavaScriptHighlighter {
26 26
     private var attributed: NSMutableAttributedString
27 27
     private var currentIndex: String.Index!
28 28
     private var indent = ""
29
+    private(set) var tokens = [(token: TokenType, range: NSRange)]()
29 30
     
30 31
     init(text: String) {
31 32
         self.text = text
@@ -94,6 +95,7 @@ class JavaScriptHighlighter {
94 95
             return
95 96
         }
96 97
         attributed.addAttribute(.foregroundColor, value: color, range: range)
98
+        tokens.append((token, range))
97 99
     }
98 100
 
99 101
     private func consumeExpression() {
@@ -173,32 +175,42 @@ class JavaScriptHighlighter {
173 175
     
174 176
     private func consumeTemplateString() {
175 177
         var stringFragmentStart: String.Index? = currentIndex
178
+        
179
+        func emitTemplateStringFragment() {
180
+            guard stringFragmentStart != currentIndex else { return }
181
+            print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
182
+            emit(token: .string, range: range(from: stringFragmentStart!, to: currentIndex))
183
+            stringFragmentStart = currentIndex
184
+        }
185
+        
176 186
         consume() // opening `
187
+        
177 188
         while currentIndex < text.endIndex {
178
-            if peek(length: 2) == "${" {
189
+            if peek() == "$" {
190
+                emitTemplateStringFragment()
179 191
                 consume() // $
180
-                consume() // {
181
-                print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
182
-                emit(token: .string, range: range(from: stringFragmentStart!, to: currentIndex))
183
-                consumeTemplateStringExpression()
184
-                stringFragmentStart = currentIndex
185
-                if currentIndex < text.endIndex && peek() == "}" {
186
-                    consume()
192
+                if peek() == "{" {
193
+                    consume() // {
194
+                    emitTemplateStringFragment()
195
+                    consumeTemplateStringExpression()
196
+                    stringFragmentStart = currentIndex
197
+                    if currentIndex < text.endIndex && peek() == "}" {
198
+                        consume() // }
199
+                    }
200
+
187 201
                 }
188 202
             } else if peek() == "`" {
189 203
                 stringFragmentStart = stringFragmentStart ?? currentIndex
190 204
                 consume() // `
191
-                print("Template string fragment: '\(text[stringFragmentStart!..<currentIndex])'")
192
-                emit(token: .string, range: range(from: stringFragmentStart!, to: currentIndex))
205
+                emitTemplateStringFragment()
193 206
                 stringFragmentStart = nil
194 207
                 break
195 208
             } else {
196 209
                 consume()
197 210
             }
198 211
         }
199
-        if let start = stringFragmentStart {
200
-            print("Template string fragment: '\(text[start..<currentIndex])'")
201
-            emit(token: .string, range: range(from: start, to: currentIndex))
212
+        if stringFragmentStart != nil {
213
+            emitTemplateStringFragment()
202 214
         }
203 215
     }
204 216
     

+ 58
- 1
MongoView/Views/JavaScriptEditorView.swift View File

@@ -10,6 +10,8 @@ import AppKit
10 10
 
11 11
 class JavaScriptEditorView: NSTextView {
12 12
     
13
+    var highlighter: JavaScriptHighlighter?
14
+    
13 15
     override var string: String {
14 16
         didSet {
15 17
             rehighlight()
@@ -17,12 +19,67 @@ class JavaScriptEditorView: NSTextView {
17 19
     }
18 20
 
19 21
     func rehighlight() {
20
-        JavaScriptHighlighter(mutableAttributed: self.textStorage!).highlight()
22
+        highlighter = JavaScriptHighlighter(mutableAttributed: self.textStorage!)
23
+        highlighter!.highlight()
21 24
     }
22 25
     
26
+    override func shouldChangeText(in affectedCharRange: NSRange, replacementString: String?) -> Bool {
27
+        guard super.shouldChangeText(in: affectedCharRange, replacementString: replacementString) else {
28
+            return false
29
+        }
30
+        
31
+        if affectedCharRange.length == 0,
32
+            let string = replacementString,
33
+            string.count == 1,
34
+            tryAutocompleteCharacter(for: affectedCharRange, string: string) {
35
+            return false
36
+        }
37
+
38
+        return true
39
+    }
40
+
23 41
     override func didChangeText() {
24 42
         rehighlight()
25 43
         
26 44
         super.didChangeText()
27 45
     }
46
+    
47
+    func tryAutocompleteCharacter(for range: NSRange, string inserted: String) -> Bool {
48
+        if let end = autocompleteResultFor(string: inserted, in: range) {
49
+            textStorage!.insert(NSAttributedString(string: "\(inserted)\(end)"), at: range.location)
50
+            didChangeText()
51
+            setSelectedRange(NSRange(location: range.location + 1, length: 0))
52
+            return true
53
+        } else {
54
+            return false
55
+        }
56
+    }
57
+    
58
+    private func autocompleteResultFor(string: String, in range: NSRange) -> String? {
59
+        switch string {
60
+        case "'", "\"", "`":
61
+            return token(at: range.location) != .string ? string : nil
62
+        case "(":
63
+            return token(at: range.location) != .string ? ")" : nil
64
+        case "[":
65
+            return token(at: range.location) != .string ? "]" : nil
66
+        case "{":
67
+            var prevChar: Unicode.Scalar?
68
+            if range.location > 0 {
69
+                let index = self.string.index(self.string.startIndex, offsetBy: range.location - 1)
70
+                prevChar = self.string.unicodeScalars[index]
71
+            }
72
+            return token(at: range.location) != .string || (prevChar == "$") ? "}" : nil
73
+        default:
74
+            return nil
75
+        }
76
+    }
77
+
78
+    private func token(at index: Int) -> JavaScriptHighlighter.TokenType? {
79
+        guard let highlighter = highlighter else { return nil }
80
+        for (token, range) in highlighter.tokens where range.contains(index) {
81
+            return token
82
+        }
83
+        return nil
84
+    }
28 85
 }

+ 1
- 1
jstest/main.swift View File

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

Loading…
Cancel
Save