conjure-js 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/conjure +0 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/editor.worker-CdQrwHl8.js +26 -0
- package/dist/assets/main-A7ZMId9A.css +1 -0
- package/dist/assets/main-CmI-7epE.js +3137 -0
- package/dist/index.html +195 -0
- package/dist/vite.svg +1 -0
- package/package.json +68 -0
- package/src/bin/__fixtures__/smoke/app/lib.clj +4 -0
- package/src/bin/__fixtures__/smoke/app/main.clj +4 -0
- package/src/bin/__fixtures__/smoke/repl-smoke.ts +12 -0
- package/src/bin/bencode.ts +205 -0
- package/src/bin/cli.ts +250 -0
- package/src/bin/nrepl-utils.ts +59 -0
- package/src/bin/nrepl.ts +393 -0
- package/src/bin/version.ts +4 -0
- package/src/clojure/core.clj +620 -0
- package/src/clojure/core.clj.d.ts +189 -0
- package/src/clojure/demo/math.clj +16 -0
- package/src/clojure/demo/math.clj.d.ts +4 -0
- package/src/clojure/demo.clj +42 -0
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/clojure/generated/builtin-namespace-registry.ts +14 -0
- package/src/clojure/generated/clojure-core-source.ts +623 -0
- package/src/clojure/generated/clojure-string-source.ts +196 -0
- package/src/clojure/string.clj +192 -0
- package/src/clojure/string.clj.d.ts +25 -0
- package/src/core/assertions.ts +134 -0
- package/src/core/conversions.ts +108 -0
- package/src/core/core-env.ts +58 -0
- package/src/core/env.ts +78 -0
- package/src/core/errors.ts +39 -0
- package/src/core/evaluator/apply.ts +114 -0
- package/src/core/evaluator/arity.ts +174 -0
- package/src/core/evaluator/collections.ts +25 -0
- package/src/core/evaluator/destructure.ts +247 -0
- package/src/core/evaluator/dispatch.ts +73 -0
- package/src/core/evaluator/evaluate.ts +100 -0
- package/src/core/evaluator/expand.ts +79 -0
- package/src/core/evaluator/index.ts +72 -0
- package/src/core/evaluator/quasiquote.ts +87 -0
- package/src/core/evaluator/recur-check.ts +109 -0
- package/src/core/evaluator/special-forms.ts +517 -0
- package/src/core/factories.ts +155 -0
- package/src/core/gensym.ts +9 -0
- package/src/core/index.ts +76 -0
- package/src/core/positions.ts +38 -0
- package/src/core/printer.ts +86 -0
- package/src/core/reader.ts +559 -0
- package/src/core/scanners.ts +93 -0
- package/src/core/session.ts +610 -0
- package/src/core/stdlib/arithmetic.ts +361 -0
- package/src/core/stdlib/atoms.ts +88 -0
- package/src/core/stdlib/collections.ts +784 -0
- package/src/core/stdlib/errors.ts +81 -0
- package/src/core/stdlib/hof.ts +307 -0
- package/src/core/stdlib/meta.ts +48 -0
- package/src/core/stdlib/predicates.ts +240 -0
- package/src/core/stdlib/regex.ts +238 -0
- package/src/core/stdlib/strings.ts +311 -0
- package/src/core/stdlib/transducers.ts +256 -0
- package/src/core/stdlib/utils.ts +287 -0
- package/src/core/tokenizer.ts +437 -0
- package/src/core/transformations.ts +75 -0
- package/src/core/types.ts +258 -0
- package/src/main.ts +1 -0
- package/src/monaco-esm.d.ts +7 -0
- package/src/playground/clojure-tokens.ts +67 -0
- package/src/playground/editor.worker.ts +5 -0
- package/src/playground/find-form.ts +138 -0
- package/src/playground/playground.ts +342 -0
- package/src/playground/samples/00-welcome.clj +385 -0
- package/src/playground/samples/01-collections.clj +191 -0
- package/src/playground/samples/02-higher-order-functions.clj +215 -0
- package/src/playground/samples/03-destructuring.clj +194 -0
- package/src/playground/samples/04-strings-and-regex.clj +202 -0
- package/src/playground/samples/05-error-handling.clj +212 -0
- package/src/repl/repl.ts +116 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
export const valueKeywords = {
|
|
2
|
+
number: 'number',
|
|
3
|
+
string: 'string',
|
|
4
|
+
boolean: 'boolean',
|
|
5
|
+
keyword: 'keyword',
|
|
6
|
+
nil: 'nil',
|
|
7
|
+
symbol: 'symbol',
|
|
8
|
+
list: 'list',
|
|
9
|
+
vector: 'vector',
|
|
10
|
+
map: 'map',
|
|
11
|
+
function: 'function',
|
|
12
|
+
nativeFunction: 'native-function',
|
|
13
|
+
macro: 'macro',
|
|
14
|
+
multiMethod: 'multi-method',
|
|
15
|
+
atom: 'atom',
|
|
16
|
+
reduced: 'reduced',
|
|
17
|
+
volatile: 'volatile',
|
|
18
|
+
regex: 'regex',
|
|
19
|
+
} as const
|
|
20
|
+
export type ValueKeywords = (typeof valueKeywords)[keyof typeof valueKeywords]
|
|
21
|
+
|
|
22
|
+
export type CljNumber = { kind: 'number'; value: number }
|
|
23
|
+
export type CljString = { kind: 'string'; value: string }
|
|
24
|
+
export type CljBoolean = { kind: 'boolean'; value: boolean }
|
|
25
|
+
export type CljKeyword = { kind: 'keyword'; name: string }
|
|
26
|
+
export type CljNil = { kind: 'nil'; value: null }
|
|
27
|
+
export type CljSymbol = { kind: 'symbol'; name: string }
|
|
28
|
+
export type CljList = { kind: 'list'; value: CljValue[] }
|
|
29
|
+
export type CljVector = { kind: 'vector'; value: CljValue[] }
|
|
30
|
+
export type CljMap = { kind: 'map'; entries: [CljValue, CljValue][] }
|
|
31
|
+
export type Env = {
|
|
32
|
+
bindings: Map<string, CljValue>
|
|
33
|
+
outer: Env | null
|
|
34
|
+
namespace?: string // only present on namespace-root envs
|
|
35
|
+
aliases?: Map<string, Env> // only present on namespace-root envs; set by :as
|
|
36
|
+
readerAliases?: Map<string, string> // only present on namespace-root envs; set by :as-alias
|
|
37
|
+
resolveNs?: (name: string) => Env | null // only present on the root coreEnv
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DestructurePattern = CljSymbol | CljVector | CljMap
|
|
41
|
+
|
|
42
|
+
export type Arity = {
|
|
43
|
+
params: DestructurePattern[]
|
|
44
|
+
restParam: DestructurePattern | null
|
|
45
|
+
body: CljValue[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type CljFunction = {
|
|
49
|
+
kind: 'function'
|
|
50
|
+
arities: Arity[]
|
|
51
|
+
env: Env
|
|
52
|
+
meta?: CljMap
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type CljMacro = {
|
|
56
|
+
kind: 'macro'
|
|
57
|
+
arities: Arity[]
|
|
58
|
+
env: Env
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type CljAtom = { kind: 'atom'; value: CljValue }
|
|
62
|
+
export type CljReduced = { kind: 'reduced'; value: CljValue }
|
|
63
|
+
export type CljVolatile = { kind: 'volatile'; value: CljValue }
|
|
64
|
+
export type CljRegex = { kind: 'regex'; pattern: string; flags: string }
|
|
65
|
+
|
|
66
|
+
export type CljMultiMethod = {
|
|
67
|
+
kind: 'multi-method'
|
|
68
|
+
name: string
|
|
69
|
+
dispatchFn: CljFunction | CljNativeFunction
|
|
70
|
+
methods: Array<{ dispatchVal: CljValue; fn: CljFunction | CljNativeFunction }>
|
|
71
|
+
defaultMethod?: CljFunction | CljNativeFunction
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type EvaluationContext = {
|
|
75
|
+
evaluate: (expr: CljValue, env: Env) => CljValue
|
|
76
|
+
evaluateForms: (forms: CljValue[], env: Env) => CljValue
|
|
77
|
+
applyFunction: (
|
|
78
|
+
fn: CljFunction | CljNativeFunction,
|
|
79
|
+
args: CljValue[],
|
|
80
|
+
callEnv: Env
|
|
81
|
+
) => CljValue
|
|
82
|
+
/** Invokes any IFn value: functions, native functions, keywords, and maps. */
|
|
83
|
+
applyCallable: (fn: CljValue, args: CljValue[], callEnv: Env) => CljValue
|
|
84
|
+
applyMacro: (macro: CljMacro, rawArgs: CljValue[]) => CljValue
|
|
85
|
+
expandAll: (form: CljValue, env: Env) => CljValue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type CljNativeFunction = {
|
|
89
|
+
kind: 'native-function'
|
|
90
|
+
name: string
|
|
91
|
+
fn: (...args: CljValue[]) => CljValue
|
|
92
|
+
// Only used in case the function needs to access the evaluation context
|
|
93
|
+
fnWithContext?: (
|
|
94
|
+
ctx: EvaluationContext,
|
|
95
|
+
callEnv: Env,
|
|
96
|
+
...args: CljValue[]
|
|
97
|
+
) => CljValue
|
|
98
|
+
meta?: CljMap
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type CljValue =
|
|
102
|
+
| CljNumber
|
|
103
|
+
| CljString
|
|
104
|
+
| CljBoolean
|
|
105
|
+
| CljKeyword
|
|
106
|
+
| CljNil
|
|
107
|
+
| CljSymbol
|
|
108
|
+
| CljList
|
|
109
|
+
| CljVector
|
|
110
|
+
| CljMap
|
|
111
|
+
| CljFunction
|
|
112
|
+
| CljNativeFunction
|
|
113
|
+
| CljMacro
|
|
114
|
+
| CljMultiMethod
|
|
115
|
+
| CljAtom
|
|
116
|
+
| CljReduced
|
|
117
|
+
| CljVolatile
|
|
118
|
+
| CljRegex
|
|
119
|
+
|
|
120
|
+
/** Tokens */
|
|
121
|
+
export const tokenKeywords = {
|
|
122
|
+
LParen: 'LParen',
|
|
123
|
+
RParen: 'RParen',
|
|
124
|
+
LBracket: 'LBracket',
|
|
125
|
+
RBracket: 'RBracket',
|
|
126
|
+
LBrace: 'LBrace',
|
|
127
|
+
RBrace: 'RBrace',
|
|
128
|
+
String: 'String',
|
|
129
|
+
Number: 'Number',
|
|
130
|
+
Keyword: 'Keyword',
|
|
131
|
+
Quote: 'Quote',
|
|
132
|
+
Quasiquote: 'Quasiquote',
|
|
133
|
+
Unquote: 'Unquote',
|
|
134
|
+
UnquoteSplicing: 'UnquoteSplicing',
|
|
135
|
+
Comment: 'Comment',
|
|
136
|
+
Whitespace: 'Whitespace',
|
|
137
|
+
Symbol: 'Symbol',
|
|
138
|
+
AnonFnStart: 'AnonFnStart',
|
|
139
|
+
Deref: 'Deref',
|
|
140
|
+
Regex: 'Regex',
|
|
141
|
+
} as const
|
|
142
|
+
export const tokenSymbols = {
|
|
143
|
+
Quote: 'quote',
|
|
144
|
+
Quasiquote: 'quasiquote',
|
|
145
|
+
Unquote: 'unquote',
|
|
146
|
+
UnquoteSplicing: 'unquote-splicing',
|
|
147
|
+
LParen: '(',
|
|
148
|
+
RParen: ')',
|
|
149
|
+
LBracket: '[',
|
|
150
|
+
RBracket: ']',
|
|
151
|
+
LBrace: '{',
|
|
152
|
+
RBrace: '}',
|
|
153
|
+
} as const
|
|
154
|
+
export type TokenSymbols = (typeof tokenSymbols)[keyof typeof tokenSymbols]
|
|
155
|
+
export type TokenKinds = keyof typeof tokenKeywords
|
|
156
|
+
|
|
157
|
+
export type Cursor = {
|
|
158
|
+
line: number
|
|
159
|
+
col: number
|
|
160
|
+
offset: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type Pos = { start: number; end: number } // absolute char offsets into the source string
|
|
164
|
+
|
|
165
|
+
export type TokenLParen = {
|
|
166
|
+
kind: 'LParen'
|
|
167
|
+
value: '('
|
|
168
|
+
}
|
|
169
|
+
export type TokenRParen = {
|
|
170
|
+
kind: 'RParen'
|
|
171
|
+
value: ')'
|
|
172
|
+
}
|
|
173
|
+
export type TokenLBracket = {
|
|
174
|
+
kind: 'LBracket'
|
|
175
|
+
value: '['
|
|
176
|
+
}
|
|
177
|
+
export type TokenRBracket = {
|
|
178
|
+
kind: 'RBracket'
|
|
179
|
+
value: ']'
|
|
180
|
+
}
|
|
181
|
+
export type TokenLBrace = {
|
|
182
|
+
kind: 'LBrace'
|
|
183
|
+
value: '{'
|
|
184
|
+
}
|
|
185
|
+
export type TokenRBrace = {
|
|
186
|
+
kind: 'RBrace'
|
|
187
|
+
value: '}'
|
|
188
|
+
}
|
|
189
|
+
export type TokenString = {
|
|
190
|
+
kind: 'String'
|
|
191
|
+
value: string
|
|
192
|
+
}
|
|
193
|
+
export type TokenNumber = {
|
|
194
|
+
kind: 'Number'
|
|
195
|
+
value: number
|
|
196
|
+
}
|
|
197
|
+
export type TokenKeyword = {
|
|
198
|
+
kind: 'Keyword'
|
|
199
|
+
value: string
|
|
200
|
+
}
|
|
201
|
+
export type TokenQuote = {
|
|
202
|
+
kind: 'Quote'
|
|
203
|
+
value: 'quote'
|
|
204
|
+
}
|
|
205
|
+
export type TokenComment = {
|
|
206
|
+
kind: 'Comment'
|
|
207
|
+
value: string
|
|
208
|
+
}
|
|
209
|
+
export type TokenWhitespace = {
|
|
210
|
+
kind: 'Whitespace'
|
|
211
|
+
}
|
|
212
|
+
export type TokenSymbol = {
|
|
213
|
+
kind: 'Symbol'
|
|
214
|
+
value: string
|
|
215
|
+
}
|
|
216
|
+
export type TokenQuasiquote = {
|
|
217
|
+
kind: 'Quasiquote'
|
|
218
|
+
value: 'quasiquote'
|
|
219
|
+
}
|
|
220
|
+
export type TokenUnquote = {
|
|
221
|
+
kind: 'Unquote'
|
|
222
|
+
value: 'unquote'
|
|
223
|
+
}
|
|
224
|
+
export type TokenUnquoteSplicing = {
|
|
225
|
+
kind: 'UnquoteSplicing'
|
|
226
|
+
value: 'unquote-splicing'
|
|
227
|
+
}
|
|
228
|
+
export type TokenAnonFnStart = {
|
|
229
|
+
kind: 'AnonFnStart'
|
|
230
|
+
}
|
|
231
|
+
export type TokenDeref = {
|
|
232
|
+
kind: 'Deref'
|
|
233
|
+
}
|
|
234
|
+
export type TokenRegex = {
|
|
235
|
+
kind: 'Regex'
|
|
236
|
+
value: string
|
|
237
|
+
}
|
|
238
|
+
export type Token = (
|
|
239
|
+
| TokenLParen
|
|
240
|
+
| TokenRParen
|
|
241
|
+
| TokenLBracket
|
|
242
|
+
| TokenRBracket
|
|
243
|
+
| TokenLBrace
|
|
244
|
+
| TokenRBrace
|
|
245
|
+
| TokenString
|
|
246
|
+
| TokenNumber
|
|
247
|
+
| TokenKeyword
|
|
248
|
+
| TokenQuote
|
|
249
|
+
| TokenComment
|
|
250
|
+
| TokenWhitespace
|
|
251
|
+
| TokenSymbol
|
|
252
|
+
| TokenQuasiquote
|
|
253
|
+
| TokenUnquote
|
|
254
|
+
| TokenUnquoteSplicing
|
|
255
|
+
| TokenAnonFnStart
|
|
256
|
+
| TokenDeref
|
|
257
|
+
| TokenRegex
|
|
258
|
+
) & { start: Cursor; end: Cursor }
|
package/src/main.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './playground/playground'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { language as clojureLang } from 'monaco-editor/esm/vs/basic-languages/clojure/clojure'
|
|
2
|
+
import type * as Monaco from 'monaco-editor'
|
|
3
|
+
|
|
4
|
+
export const THEME_ID = 'vscode-clj-dark'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keep Monaco's native Clojure highlighting, but stop treating `(comment ...)`
|
|
8
|
+
* as a real comment block. In Clojure that form is a macro and its contents
|
|
9
|
+
* should still be tokenized as normal code.
|
|
10
|
+
*/
|
|
11
|
+
export function registerClojureLanguage(monaco: typeof Monaco): void {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const base = clojureLang as any
|
|
14
|
+
|
|
15
|
+
const filteredWhitespace = base.tokenizer.whitespace.filter(
|
|
16
|
+
// Monaco's built-in Clojure grammar marks `(comment ... )` as comment.
|
|
17
|
+
// We keep `;` comments, but drop this macro-specific rule.
|
|
18
|
+
(rule: unknown) =>
|
|
19
|
+
!(
|
|
20
|
+
Array.isArray(rule) &&
|
|
21
|
+
rule[0] instanceof RegExp &&
|
|
22
|
+
rule[1] === 'comment' &&
|
|
23
|
+
rule[0].source === '\\(comment\\b'
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
monaco.languages.setMonarchTokensProvider('clojure', {
|
|
28
|
+
...base,
|
|
29
|
+
tokenizer: {
|
|
30
|
+
...base.tokenizer,
|
|
31
|
+
whitespace: filteredWhitespace,
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Small VSCode-like theme override:
|
|
38
|
+
* - keep Monaco's default tokenization behavior
|
|
39
|
+
* - use neutral dark background/colors similar to VS Code Dark+
|
|
40
|
+
*/
|
|
41
|
+
export function defineMonacoTheme(monaco: typeof Monaco): void {
|
|
42
|
+
monaco.editor.defineTheme(THEME_ID, {
|
|
43
|
+
base: 'vs-dark',
|
|
44
|
+
inherit: true,
|
|
45
|
+
rules: [
|
|
46
|
+
{ token: 'comment.clj', foreground: '6a9955' },
|
|
47
|
+
{ token: 'string.clj', foreground: 'ce9178' },
|
|
48
|
+
{ token: 'number.clj', foreground: 'b5cea8' },
|
|
49
|
+
{ token: 'keyword.clj', foreground: 'c586c0' },
|
|
50
|
+
{ token: 'constant.clj', foreground: '4fc1ff' },
|
|
51
|
+
{ token: 'meta.clj', foreground: 'd7ba7d' },
|
|
52
|
+
{ token: 'identifier.clj', foreground: 'd4d4d4' },
|
|
53
|
+
],
|
|
54
|
+
colors: {
|
|
55
|
+
'editor.background': '#1e1e1e',
|
|
56
|
+
'editor.foreground': '#d4d4d4',
|
|
57
|
+
'editor.lineHighlightBackground': '#2a2d2e',
|
|
58
|
+
'editorCursor.foreground': '#aeafad',
|
|
59
|
+
'editor.selectionBackground': '#264f78',
|
|
60
|
+
'editor.inactiveSelectionBackground': '#3a3d41',
|
|
61
|
+
'editorLineNumber.foreground': '#858585',
|
|
62
|
+
'editorLineNumber.activeForeground': '#c6c6c6',
|
|
63
|
+
'editorBracketMatch.background': '#0064001a',
|
|
64
|
+
'editorBracketMatch.border': '#888888',
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Thin wrapper so Vite can bundle Monaco's editor worker via the
|
|
2
|
+
// `new URL('./editor.worker.ts', import.meta.url)` pattern.
|
|
3
|
+
// Vite only recognises the worker-bundling pattern for relative paths;
|
|
4
|
+
// bare package specifiers inside new URL() are not resolved the same way.
|
|
5
|
+
import 'monaco-editor/esm/vs/editor/editor.worker'
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { tokenize } from '../core/tokenizer'
|
|
2
|
+
|
|
3
|
+
export type FormRange = {
|
|
4
|
+
/** Inclusive start offset in the source string */
|
|
5
|
+
start: number
|
|
6
|
+
/** Exclusive end offset in the source string */
|
|
7
|
+
end: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// AnonFnStart (#() consumes both # and (, so it opens a RParen-closed scope
|
|
11
|
+
const OPEN = new Set(['LParen', 'LBracket', 'LBrace', 'AnonFnStart'])
|
|
12
|
+
const CLOSE = new Set(['RParen', 'RBracket', 'RBrace'])
|
|
13
|
+
const PREFIX = new Set(['Quote', 'Quasiquote', 'Unquote', 'UnquoteSplicing'])
|
|
14
|
+
const SKIP = new Set(['Whitespace', 'Comment'])
|
|
15
|
+
|
|
16
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Calva-style "form before cursor" heuristic.
|
|
20
|
+
*
|
|
21
|
+
* Scans backwards from the cursor offset to find the last **complete** form:
|
|
22
|
+
*
|
|
23
|
+
* - If the last token before the cursor is a close bracket `)` `]` `}` →
|
|
24
|
+
* walk backwards to find its matching open bracket. The whole balanced
|
|
25
|
+
* expression is the form.
|
|
26
|
+
* - If the last token is an atom (symbol, number, keyword, string, …) →
|
|
27
|
+
* that atom is the form.
|
|
28
|
+
* - Reader-macro prefixes (`'` `` ` `` `~` `~@`) immediately before the form
|
|
29
|
+
* are included in the returned range.
|
|
30
|
+
*
|
|
31
|
+
* Returns null when the cursor is after an unmatched open bracket, in
|
|
32
|
+
* whitespace/comments with nothing before it, or if tokenization fails.
|
|
33
|
+
*/
|
|
34
|
+
export function findFormBeforeCursor(
|
|
35
|
+
source: string,
|
|
36
|
+
cursorOffset: number,
|
|
37
|
+
): FormRange | null {
|
|
38
|
+
let tokens
|
|
39
|
+
try {
|
|
40
|
+
tokens = tokenize(source)
|
|
41
|
+
} catch {
|
|
42
|
+
// Full-source tokenization failed (e.g. unsupported syntax elsewhere in
|
|
43
|
+
// the file). Retry with just the text up to the cursor — enough to find
|
|
44
|
+
// the form before it, and avoids any bad syntax that lives after it.
|
|
45
|
+
try {
|
|
46
|
+
tokens = tokenize(source.slice(0, cursorOffset))
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Relevant tokens: non-whitespace, non-comment, ending at or before cursor
|
|
53
|
+
const relevant = tokens.filter(
|
|
54
|
+
(t) => !SKIP.has(t.kind) && t.end.offset <= cursorOffset,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (relevant.length === 0) return null
|
|
58
|
+
|
|
59
|
+
const lastIdx = relevant.length - 1
|
|
60
|
+
const last = relevant[lastIdx]
|
|
61
|
+
|
|
62
|
+
let range: FormRange
|
|
63
|
+
|
|
64
|
+
if (CLOSE.has(last.kind)) {
|
|
65
|
+
// Closing bracket — find its matching open
|
|
66
|
+
const openIdx = findMatchingOpen(relevant, lastIdx)
|
|
67
|
+
if (openIdx === -1) return null
|
|
68
|
+
range = {
|
|
69
|
+
start: relevant[openIdx].start.offset,
|
|
70
|
+
end: last.end.offset,
|
|
71
|
+
}
|
|
72
|
+
return extendWithPrefix(relevant, openIdx, range)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (OPEN.has(last.kind)) {
|
|
76
|
+
// Cursor is right after an open bracket with no matching close before it.
|
|
77
|
+
// Return just the bracket itself so the evaluator reports an "unmatched
|
|
78
|
+
// bracket" error — correct and honest, no fallback to whole-buffer eval.
|
|
79
|
+
return { start: last.start.offset, end: last.end.offset }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!PREFIX.has(last.kind)) {
|
|
83
|
+
// Atom (symbol, number, keyword, string, nil, …)
|
|
84
|
+
range = { start: last.start.offset, end: last.end.offset }
|
|
85
|
+
return extendWithPrefix(relevant, lastIdx, range)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Bare reader-macro prefix with nothing after it — nothing to eval.
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
type Token = ReturnType<typeof tokenize>[number]
|
|
95
|
+
|
|
96
|
+
/** Scan backwards from closeIdx to find the index of the matching open bracket. */
|
|
97
|
+
function findMatchingOpen(relevant: Token[], closeIdx: number): number {
|
|
98
|
+
const closeKind = relevant[closeIdx].kind
|
|
99
|
+
const openKind =
|
|
100
|
+
closeKind === 'RParen'
|
|
101
|
+
? 'LParen'
|
|
102
|
+
: closeKind === 'RBracket'
|
|
103
|
+
? 'LBracket'
|
|
104
|
+
: 'LBrace'
|
|
105
|
+
|
|
106
|
+
let depth = 1
|
|
107
|
+
for (let i = closeIdx - 1; i >= 0; i--) {
|
|
108
|
+
const k = relevant[i].kind
|
|
109
|
+
if (k === closeKind) depth++
|
|
110
|
+
else if (
|
|
111
|
+
k === openKind ||
|
|
112
|
+
// AnonFnStart opens a RParen-closed scope just like LParen
|
|
113
|
+
(openKind === 'LParen' && k === 'AnonFnStart')
|
|
114
|
+
) {
|
|
115
|
+
depth--
|
|
116
|
+
if (depth === 0) return i
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return -1
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* If the token immediately before formStartIdx is a reader-macro prefix
|
|
124
|
+
* (`'`, `` ` ``, `~`, `~@`), extend the range to include it.
|
|
125
|
+
*/
|
|
126
|
+
function extendWithPrefix(
|
|
127
|
+
relevant: Token[],
|
|
128
|
+
formStartIdx: number,
|
|
129
|
+
range: FormRange,
|
|
130
|
+
): FormRange {
|
|
131
|
+
if (formStartIdx > 0) {
|
|
132
|
+
const prev = relevant[formStartIdx - 1]
|
|
133
|
+
if (PREFIX.has(prev.kind)) {
|
|
134
|
+
return { start: prev.start.offset, end: range.end }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return range
|
|
138
|
+
}
|