turbo-editors/turbo-jspublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-js.git
git clone ssh://git@rickub.com/turbo-editors/turbo-js.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

words.go · 239 lines · 8.9 KBGo Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 12h ago1package jslang
2
3// Words: what a run of letters turns out to be, and the tables that decide it.
4
5import (
6 "slices"
7 "strings"
8 "unicode"
9
10 "rickub.com/turbo-editors/turbo-core/syntax"
11)
12
13// isWordStart reports whether a rune can begin an identifier. JavaScript
14// identifiers are Unicode: café and 名前 are names, and so is $, which jQuery
15// made ordinary, and _, which lodash did.
16func isWordStart(r rune) bool { return unicode.IsLetter(r) || r == '_' || r == '$' }
17
18// isWordRune reports whether a rune can continue one.
19func isWordRune(r rune) bool { return isWordStart(r) || unicode.IsDigit(r) }
20
21// takeWord colours an identifier, deciding what kind of thing it is from the
22// word itself, from what came before it and from the rune after it.
23func takeWord(s *syntax.LineScanner, state *lineState) {
24 start := s.Pos()
25 for !s.AtEnd() && isWordRune(s.Peek(0)) {
26 s.Advance(1)
27 }
28 word := wordAt(s, start)
29
30 class := classOfWord(word, *state, isCallSite(s))
31 s.Emit(start, s.Pos(), class)
32
33 if class == syntax.ClassKeyword {
34 state.keyword(word)
35 return
36 }
37 state.operand()
38}
39
40// wordAt returns the text running from start to the scanner's position.
41func wordAt(s *syntax.LineScanner, start int) string {
42 var b strings.Builder
43 for at := start; at < s.Pos(); at++ {
44 b.WriteRune(s.Peek(at - s.Pos()))
45 }
46 return b.String()
47}
48
49// isCallSite reports whether the next thing on the line, past any spaces, is
50// an opening parenthesis.
51func isCallSite(s *syntax.LineScanner) bool {
52 for at := 0; s.Pos()+at < s.Len(); at++ {
53 switch s.Peek(at) {
54 case ' ', '\t':
55 case '(':
56 return true
57 default:
58 return false
59 }
60 }
61 return false
62}
63
64// classOfWord decides what a word is.
65//
66// The order is the design. A word after a dot is a property, whatever it is
67// spelt like — map.get(k) is a call and obj.default a field, and neither is
68// the keyword it would be on its own. A word after function or class is named
69// by its position. Then a word the language names is what the language says
70// it is, except a contextual keyword — get, set, static, of, from, as — that
71// is being called, which is a function of that name. Then a leading capital
72// says class, leaning on the convention every JavaScript style guide shares;
73// then a following parenthesis says function; and what is left is a name.
74func classOfWord(word string, state lineState, call bool) syntax.Class {
75 if class, decided := classByPosition(state, call); decided {
76 return class
77 }
78 if class, known := knownWords[word]; known {
79 return knownWordClass(word, class, call)
80 }
81 return classBySpelling(word, call)
82}
83
84// classByPosition decides a word by what came before it, and reports whether
85// that settled it: a property after a dot, a name after function or class.
86func classByPosition(state lineState, call bool) (syntax.Class, bool) {
87 switch {
88 case state.afterDot:
89 return propertyClass(call), true
90 case state.afterKeyword == "function":
91 return syntax.ClassFunction, true
92 case state.afterKeyword == "class":
93 return syntax.ClassType, true
94 }
95 return syntax.ClassIdentifier, false
96}
97
98// knownWordClass is what a word the language names turns out to be: what the
99// table says, unless it is a contextual keyword being called — get(k) on a Map
100// — which is a function of that name.
101func knownWordClass(word string, class syntax.Class, call bool) syntax.Class {
102 if class == syntax.ClassKeyword && contextual[word] && call {
103 return syntax.ClassFunction
104 }
105 return class
106}
107
108// classBySpelling decides a word nothing else claimed, from its spelling and
109// the rune after it.
110//
111// A leading capital says class — a constructor, a namespace-like object, a
112// React component. It is visibly a heuristic in one place: a
113// SCREAMING_SNAKE_CASE constant is coloured as a class too, and the reference
114// says so rather than leaving somebody to find out.
115func classBySpelling(word string, call bool) syntax.Class {
116 switch {
117 case startsUpperCase(word):
118 return syntax.ClassType
119 case call:
120 return syntax.ClassFunction
121 }
122 return syntax.ClassIdentifier
123}
124
125// propertyClass is what a word after a dot is: a method when it is called, a
126// field otherwise.
127func propertyClass(call bool) syntax.Class {
128 if call {
129 return syntax.ClassFunction
130 }
131 return syntax.ClassIdentifier
132}
133
134// startsUpperCase reports whether a word begins with an ASCII capital.
135func startsUpperCase(word string) bool {
136 return word != "" && word[0] >= 'A' && word[0] <= 'Z'
137}
138
139// knownWords is every word the language itself names, and what each one is.
140//
141// It is one table rather than three because it answers one question. The
142// groups below are kept apart only so that each can carry the reasoning that
143// belongs to it.
144var knownWords = merge(
145 classify(syntax.ClassKeyword, keywords),
146 classify(syntax.ClassConstant, constants),
147 classify(syntax.ClassBuiltin, builtins),
148)
149
150// contextual are the keywords that are only keywords in some positions, and
151// ordinary names everywhere else: get and set before a method's name, static
152// before a member, of in a for-of, from and as in an import. Called — get(k),
153// set(k, v) — they are functions of that name, on a Map or anywhere else.
154var contextual = map[string]bool{
155 "get": true, "set": true, "static": true, "of": true, "from": true, "as": true,
156}
157
158// keywords are JavaScript's reserved words and the contextual ones a reader
159// meets as keywords — async and await, of, get and set, static, from and as.
160var keywords = words(
161 "as", "async", "await", "break", "case", "catch", "class", "const",
162 "continue", "debugger", "default", "delete", "do", "else", "export",
163 "extends", "finally", "for", "from", "function", "get", "if", "import",
164 "in", "instanceof", "let", "new", "of", "return", "set", "static", "super",
165 "switch", "throw", "try", "typeof", "var", "void", "while", "with", "yield",
166 // Reserved for future use, and in strict mode. A file using one will not
167 // run, and colouring it as an identifier would be the friendlier of two
168 // wrong answers.
169 "enum", "implements", "interface", "package", "private", "protected", "public",
170)
171
172// constants are the literals the language itself provides. this is one of
173// them rather than a keyword: a reader reads it as a value, the one it stands
174// for, and a slash after it divides.
175var constants = words("true", "false", "null", "undefined", "NaN", "Infinity", "this")
176
177// builtins are the globals worth telling apart from a variable of your own:
178// the standard library's constructors and namespaces, and — because this is a
179// Node editor — Node's own globals, from process and Buffer to the CommonJS
180// five. Recognised by name, as Go's predeclared identifiers are, because a
181// file may shadow them and colouring the shadowed one anyway is what every
182// editor does.
183var builtins = words(
184 // The standard library.
185 "Array", "ArrayBuffer", "BigInt", "Boolean", "DataView", "Date", "Error",
186 "EvalError", "Function", "Intl", "JSON", "Map", "Math", "Number", "Object",
187 "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp",
188 "Set", "String", "Symbol", "SyntaxError", "TypeError", "URIError", "WeakMap",
189 "WeakRef", "WeakSet", "globalThis",
190 "parseInt", "parseFloat", "isNaN", "isFinite", "encodeURIComponent",
191 "decodeURIComponent", "structuredClone", "queueMicrotask",
192 "setTimeout", "clearTimeout", "setInterval", "clearInterval",
193 // Node.js, and the web platform pieces Node ships.
194 "process", "Buffer", "console", "require", "module", "exports",
195 "__dirname", "__filename", "setImmediate", "clearImmediate",
196 "fetch", "URL", "URLSearchParams", "TextEncoder", "TextDecoder",
197 "AbortController", "AbortSignal", "Blob", "Event", "EventTarget",
198 "performance", "crypto", "navigator",
199 // The browser's two, for the front-end half of a Node project.
200 "document", "window",
201)
202
203// words gathers a group of them, which reads better at the call sites above
204// than a slice literal does.
205func words(list ...string) []string { return list }
206
207// classify pairs every word in a group with the class it belongs to.
208func classify(class syntax.Class, list []string) map[string]syntax.Class {
209 out := make(map[string]syntax.Class, len(list))
210 for _, word := range list {
211 out[word] = class
212 }
213 return out
214}
215
216// merge folds the groups into one table. An earlier group wins a word a later
217// one repeats, which is what keeps a keyword a keyword.
218func merge(groups ...map[string]syntax.Class) map[string]syntax.Class {
219 out := map[string]syntax.Class{}
220 for _, group := range groups {
221 for word, class := range group {
222 if _, taken := out[word]; !taken {
223 out[word] = class
224 }
225 }
226 }
227 return out
228}
229
230// Keywords returns the words the scanner colours as keywords, sorted. It is
231// what a test compares against a real language server's completion.
232func Keywords() []string { return sorted(keywords) }
233
234// Builtins returns the globals the scanner colours as built in, sorted.
235func Builtins() []string { return sorted(builtins) }
236
237// sorted returns a sorted copy of a word list, so a caller cannot reorder the
238// package's own table.
239func sorted(list []string) []string { return slices.Sorted(slices.Values(list)) }