@actualwave/codemirror-lang-react-native 0.1.0

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/index.js ADDED
@@ -0,0 +1,614 @@
1
+ import { syntaxTree } from '@codemirror/language';
2
+
3
+ // Curated completion data. Only `react` and `react-native` are baked in here
4
+ // since every React Native project has them; anything else (expo-image,
5
+ // react-native-safe-area-context, @shopify/react-native-skia, ...) is
6
+ // supplied by the consumer via ReactNativeCompletionConfig.modules and merged
7
+ // on top of these built-ins (see completion.ts's mergeModules).
8
+ //
9
+ // This is a hand-curated subset, not derived from the packages' own .d.ts
10
+ // files — JSX prop types involve unions/generics/extends chains that aren't
11
+ // worth fully modeling for editor completion, and CodeMirror can't validate
12
+ // TypeScript anyway, so a maintained list of the props people actually reach
13
+ // for carries the value at a fraction of the fragility.
14
+ const COMMON_VIEW_PROPS = [
15
+ "style",
16
+ "children",
17
+ "testID",
18
+ "pointerEvents",
19
+ "onLayout",
20
+ "accessible",
21
+ "accessibilityLabel",
22
+ "accessibilityRole",
23
+ "hitSlop",
24
+ ];
25
+ const react = {
26
+ useState: { type: "function", detail: "useState(initialState)" },
27
+ useEffect: { type: "function", detail: "useEffect(setup, deps?)" },
28
+ useLayoutEffect: { type: "function", detail: "useLayoutEffect(setup, deps?)" },
29
+ useMemo: { type: "function", detail: "useMemo(factory, deps)" },
30
+ useCallback: { type: "function", detail: "useCallback(fn, deps)" },
31
+ useRef: { type: "function", detail: "useRef(initialValue)" },
32
+ useContext: { type: "function", detail: "useContext(Context)" },
33
+ useReducer: { type: "function", detail: "useReducer(reducer, initialState)" },
34
+ useId: { type: "function", detail: "useId()" },
35
+ useTransition: { type: "function", detail: "useTransition()" },
36
+ useDeferredValue: { type: "function", detail: "useDeferredValue(value)" },
37
+ useImperativeHandle: { type: "function", detail: "useImperativeHandle(ref, createHandle, deps?)" },
38
+ createContext: { type: "function", detail: "createContext(defaultValue)" },
39
+ memo: { type: "function", detail: "memo(Component)" },
40
+ forwardRef: { type: "function", detail: "forwardRef(render)" },
41
+ Fragment: { type: "type" },
42
+ StrictMode: { type: "type" },
43
+ };
44
+ const reactNative = {
45
+ View: { type: "type", props: [...COMMON_VIEW_PROPS] },
46
+ Text: { type: "type", props: ["style", "children", "numberOfLines", "ellipsizeMode", "onPress", "selectable", "testID"] },
47
+ ScrollView: {
48
+ type: "type",
49
+ props: [
50
+ ...COMMON_VIEW_PROPS,
51
+ "contentContainerStyle",
52
+ "horizontal",
53
+ "showsVerticalScrollIndicator",
54
+ "showsHorizontalScrollIndicator",
55
+ "onScroll",
56
+ "scrollEventThrottle",
57
+ "refreshControl",
58
+ "keyboardShouldPersistTaps",
59
+ ],
60
+ },
61
+ FlatList: {
62
+ type: "type",
63
+ props: [
64
+ "data",
65
+ "renderItem",
66
+ "keyExtractor",
67
+ "style",
68
+ "contentContainerStyle",
69
+ "horizontal",
70
+ "numColumns",
71
+ "onEndReached",
72
+ "onEndReachedThreshold",
73
+ "ListHeaderComponent",
74
+ "ListFooterComponent",
75
+ "ListEmptyComponent",
76
+ "ItemSeparatorComponent",
77
+ "refreshing",
78
+ "onRefresh",
79
+ "getItemLayout",
80
+ ],
81
+ },
82
+ SectionList: {
83
+ type: "type",
84
+ props: ["sections", "renderItem", "renderSectionHeader", "keyExtractor", "style", "stickySectionHeadersEnabled"],
85
+ },
86
+ Image: { type: "type", props: ["source", "style", "resizeMode", "onLoad", "onError", "defaultSource", "blurRadius"] },
87
+ ImageBackground: { type: "type", props: ["source", "style", "imageStyle", "resizeMode", "children"] },
88
+ Pressable: {
89
+ type: "type",
90
+ props: ["onPress", "onLongPress", "onPressIn", "onPressOut", "style", "disabled", "hitSlop", "android_ripple", "children"],
91
+ },
92
+ TouchableOpacity: { type: "type", props: ["onPress", "onLongPress", "style", "activeOpacity", "disabled", "hitSlop", "children"] },
93
+ TouchableHighlight: { type: "type", props: ["onPress", "onLongPress", "style", "underlayColor", "disabled", "children"] },
94
+ TouchableWithoutFeedback: { type: "type", props: ["onPress", "onLongPress", "children"] },
95
+ TextInput: {
96
+ type: "type",
97
+ props: [
98
+ "value",
99
+ "defaultValue",
100
+ "onChangeText",
101
+ "onSubmitEditing",
102
+ "onFocus",
103
+ "onBlur",
104
+ "placeholder",
105
+ "placeholderTextColor",
106
+ "style",
107
+ "secureTextEntry",
108
+ "keyboardType",
109
+ "autoCapitalize",
110
+ "autoCorrect",
111
+ "multiline",
112
+ "numberOfLines",
113
+ "editable",
114
+ "maxLength",
115
+ ],
116
+ },
117
+ Switch: { type: "type", props: ["value", "onValueChange", "disabled", "trackColor", "thumbColor", "style"] },
118
+ Button: { type: "type", props: ["title", "onPress", "color", "disabled", "accessibilityLabel"] },
119
+ ActivityIndicator: { type: "type", props: ["animating", "color", "size", "style", "hidesWhenStopped"] },
120
+ Modal: { type: "type", props: ["visible", "animationType", "transparent", "onRequestClose", "presentationStyle", "children"] },
121
+ SafeAreaView: { type: "type", props: [...COMMON_VIEW_PROPS] },
122
+ KeyboardAvoidingView: { type: "type", props: [...COMMON_VIEW_PROPS, "behavior", "keyboardVerticalOffset", "enabled"] },
123
+ StatusBar: { type: "type", props: ["barStyle", "backgroundColor", "hidden", "translucent", "animated"] },
124
+ Animated: { type: "variable" },
125
+ StyleSheet: { type: "variable", detail: "StyleSheet.create({...})" },
126
+ Dimensions: { type: "variable", detail: 'Dimensions.get("window")' },
127
+ Platform: { type: "variable", detail: "Platform.OS / Platform.select({...})" },
128
+ Alert: { type: "variable", detail: "Alert.alert(title, message)" },
129
+ Keyboard: { type: "variable" },
130
+ Linking: { type: "variable" },
131
+ Vibration: { type: "variable" },
132
+ PanResponder: { type: "variable", detail: "PanResponder.create({...})" },
133
+ LayoutAnimation: { type: "variable" },
134
+ useColorScheme: { type: "function", detail: "useColorScheme()" },
135
+ useWindowDimensions: { type: "function", detail: "useWindowDimensions()" },
136
+ };
137
+ const BUILTIN_MODULES = {
138
+ react,
139
+ "react-native": reactNative,
140
+ };
141
+ // RN StyleSheet property names → curated enum values (undefined means the
142
+ // value is free-form — a color, number, or dimension — and isn't worth a
143
+ // suggestion list). Deliberately not split by View/Text/Image since a JSX
144
+ // element's own component type isn't cross-referenced when completing inside
145
+ // its style object — a handful of Text-only properties showing up for a View
146
+ // style dict is an acceptable false positive for the simplicity it buys.
147
+ const STYLE_PROPERTIES = {
148
+ // Flexbox / layout
149
+ alignContent: ["flex-start", "flex-end", "center", "stretch", "space-between", "space-around"],
150
+ alignItems: ["flex-start", "flex-end", "center", "stretch", "baseline"],
151
+ alignSelf: ["auto", "flex-start", "flex-end", "center", "stretch", "baseline"],
152
+ aspectRatio: undefined,
153
+ borderBottomWidth: undefined,
154
+ borderEndWidth: undefined,
155
+ borderLeftWidth: undefined,
156
+ borderRightWidth: undefined,
157
+ borderStartWidth: undefined,
158
+ borderTopWidth: undefined,
159
+ borderWidth: undefined,
160
+ bottom: undefined,
161
+ columnGap: undefined,
162
+ direction: ["inherit", "ltr", "rtl"],
163
+ display: ["flex", "none"],
164
+ end: undefined,
165
+ flex: undefined,
166
+ flexBasis: undefined,
167
+ flexDirection: ["row", "row-reverse", "column", "column-reverse"],
168
+ flexGrow: undefined,
169
+ flexShrink: undefined,
170
+ flexWrap: ["wrap", "nowrap", "wrap-reverse"],
171
+ gap: undefined,
172
+ height: undefined,
173
+ justifyContent: ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"],
174
+ left: undefined,
175
+ margin: undefined,
176
+ marginBottom: undefined,
177
+ marginEnd: undefined,
178
+ marginHorizontal: undefined,
179
+ marginLeft: undefined,
180
+ marginRight: undefined,
181
+ marginStart: undefined,
182
+ marginTop: undefined,
183
+ marginVertical: undefined,
184
+ maxHeight: undefined,
185
+ maxWidth: undefined,
186
+ minHeight: undefined,
187
+ minWidth: undefined,
188
+ overflow: ["visible", "hidden", "scroll"],
189
+ padding: undefined,
190
+ paddingBottom: undefined,
191
+ paddingEnd: undefined,
192
+ paddingHorizontal: undefined,
193
+ paddingLeft: undefined,
194
+ paddingRight: undefined,
195
+ paddingStart: undefined,
196
+ paddingTop: undefined,
197
+ paddingVertical: undefined,
198
+ position: ["absolute", "relative", "static"],
199
+ right: undefined,
200
+ rowGap: undefined,
201
+ start: undefined,
202
+ top: undefined,
203
+ width: undefined,
204
+ zIndex: undefined,
205
+ // Border / background / shadow
206
+ backgroundColor: undefined,
207
+ borderColor: undefined,
208
+ borderTopColor: undefined,
209
+ borderBottomColor: undefined,
210
+ borderLeftColor: undefined,
211
+ borderRightColor: undefined,
212
+ borderRadius: undefined,
213
+ borderTopLeftRadius: undefined,
214
+ borderTopRightRadius: undefined,
215
+ borderBottomLeftRadius: undefined,
216
+ borderBottomRightRadius: undefined,
217
+ borderStyle: ["solid", "dotted", "dashed"],
218
+ opacity: undefined,
219
+ elevation: undefined,
220
+ shadowColor: undefined,
221
+ shadowOffset: undefined,
222
+ shadowOpacity: undefined,
223
+ shadowRadius: undefined,
224
+ transform: undefined,
225
+ // Text
226
+ color: undefined,
227
+ fontFamily: undefined,
228
+ fontSize: undefined,
229
+ fontStyle: ["normal", "italic"],
230
+ fontWeight: ["normal", "bold", "100", "200", "300", "400", "500", "600", "700", "800", "900"],
231
+ letterSpacing: undefined,
232
+ lineHeight: undefined,
233
+ textAlign: ["auto", "left", "right", "center", "justify"],
234
+ textAlignVertical: ["auto", "top", "bottom", "center"],
235
+ textDecorationLine: ["none", "underline", "line-through", "underline line-through"],
236
+ textDecorationColor: undefined,
237
+ textDecorationStyle: ["solid", "double", "dotted", "dashed"],
238
+ textShadowColor: undefined,
239
+ textShadowOffset: undefined,
240
+ textShadowRadius: undefined,
241
+ textTransform: ["none", "uppercase", "lowercase", "capitalize"],
242
+ writingDirection: ["auto", "ltr", "rtl"],
243
+ // Image
244
+ resizeMode: ["cover", "contain", "stretch", "repeat", "center"],
245
+ overlayColor: undefined,
246
+ tintColor: undefined,
247
+ };
248
+
249
+ // Walks every ImportDeclaration in the document and builds
250
+ // Map<localName, ImportInfo>. This is the scoping mechanism completion.ts
251
+ // relies on: nothing curated is ever suggested unless its local name is
252
+ // actually present here for the current document.
253
+ function collectImports(state) {
254
+ const imports = new Map();
255
+ const doc = state.doc;
256
+ const tree = syntaxTree(state);
257
+ tree.iterate({
258
+ enter(node) {
259
+ if (node.name !== "ImportDeclaration")
260
+ return;
261
+ const stringNode = node.node.getChild("String");
262
+ if (!stringNode)
263
+ return;
264
+ const raw = doc.sliceString(stringNode.from, stringNode.to);
265
+ const source = raw.slice(1, -1);
266
+ // Default import: `import Foo from '...'` — a VariableDefinition
267
+ // directly under ImportDeclaration (not nested in an ImportGroup).
268
+ for (const child of node.node.getChildren("VariableDefinition")) {
269
+ imports.set(doc.sliceString(child.from, child.to), { source, imported: "default" });
270
+ }
271
+ // Namespace import: `import * as Foo from '...'`.
272
+ if (node.node.getChild("Star")) {
273
+ const varDef = node.node.getChild("VariableDefinition");
274
+ if (varDef)
275
+ imports.set(doc.sliceString(varDef.from, varDef.to), { source, imported: "*" });
276
+ }
277
+ // Named imports: `import { a, b as c } from '...'`, nested inside an
278
+ // ImportGroup child.
279
+ const group = node.node.getChild("ImportGroup");
280
+ if (!group)
281
+ return;
282
+ for (const child of group.getChildren("VariableDefinition")) {
283
+ // A VariableDefinition directly inside ImportGroup with no sibling
284
+ // VariableName means no alias — local name === imported name.
285
+ const name = doc.sliceString(child.from, child.to);
286
+ imports.set(name, { source, imported: name });
287
+ }
288
+ for (const varName of group.getChildren("VariableName")) {
289
+ // `VariableName as VariableDefinition` — the alias pair.
290
+ const parent = varName.parent;
291
+ if (!parent)
292
+ continue;
293
+ const aliasDef = parent.getChildren("VariableDefinition").find((d) => d.from > varName.to);
294
+ if (!aliasDef)
295
+ continue;
296
+ imports.set(doc.sliceString(aliasDef.from, aliasDef.to), {
297
+ source,
298
+ imported: doc.sliceString(varName.from, varName.to),
299
+ });
300
+ }
301
+ },
302
+ });
303
+ return imports;
304
+ }
305
+ // Finds the nearest enclosing ImportDeclaration, if any — used by the import
306
+ // module-specifier and named-symbol completions, which both need the
307
+ // declaration's own source string regardless of where inside it the cursor
308
+ // currently sits.
309
+ function findImportDeclaration(node) {
310
+ for (let cur = node; cur; cur = cur.parent) {
311
+ if (cur.name === "ImportDeclaration")
312
+ return cur;
313
+ }
314
+ return null;
315
+ }
316
+ function getImportSource(state, importDecl) {
317
+ const stringNode = importDecl.getChild("String");
318
+ if (!stringNode)
319
+ return null;
320
+ const raw = state.doc.sliceString(stringNode.from, stringNode.to);
321
+ return raw.slice(1, -1);
322
+ }
323
+ // Local/imported names already listed in this ImportDeclaration's own
324
+ // ImportGroup, so the named-symbol completion doesn't re-suggest something
325
+ // the statement already imports.
326
+ function getImportGroupNames(state, importDecl) {
327
+ const names = new Set();
328
+ const group = importDecl.getChild("ImportGroup");
329
+ if (!group)
330
+ return names;
331
+ for (const child of group.getChildren("VariableDefinition")) {
332
+ names.add(state.doc.sliceString(child.from, child.to));
333
+ }
334
+ for (const varName of group.getChildren("VariableName")) {
335
+ names.add(state.doc.sliceString(varName.from, varName.to));
336
+ }
337
+ return names;
338
+ }
339
+
340
+ // Classifies the cursor position relative to the nearest enclosing JSX tag,
341
+ // if any: 'tag-name' when completing the element name itself (right after
342
+ // `<` or `</`), 'attribute-name' when inside an open tag's attribute list,
343
+ // otherwise null (plain expression/statement context).
344
+ function classifyJsxContext(state, pos) {
345
+ const tree = syntaxTree(state);
346
+ const node = tree.resolveInner(pos, -1);
347
+ for (let cur = node; cur; cur = cur.parent) {
348
+ if (cur.name === "JSXOpenTag" || cur.name === "JSXSelfClosingTag" || cur.name === "JSXStartTag") {
349
+ const openTag = cur.name === "JSXStartTag" ? cur.parent : cur;
350
+ if (!openTag)
351
+ break;
352
+ const tagIdentifier = openTag.getChild("JSXIdentifier");
353
+ // Inside the tag name itself (cursor within/immediately after it, and
354
+ // no attribute has started yet).
355
+ if (tagIdentifier && pos <= tagIdentifier.to && pos >= tagIdentifier.from) {
356
+ return { kind: "tag-name", openTag };
357
+ }
358
+ if (tagIdentifier && pos < tagIdentifier.from) {
359
+ return { kind: "tag-name", openTag };
360
+ }
361
+ return { kind: "attribute-name", openTag };
362
+ }
363
+ if (cur.name === "JSXAttribute") {
364
+ const openTag = cur.parent;
365
+ if (openTag)
366
+ return { kind: "attribute-name", openTag };
367
+ }
368
+ // Anything further out than the element itself is a normal
369
+ // expression/statement context, not a tag we're inside.
370
+ if (cur.name === "JSXElement" || cur.name === "JSXFragment")
371
+ break;
372
+ }
373
+ return null;
374
+ }
375
+ function getTagName(state, openTag) {
376
+ const identifier = openTag.getChild("JSXIdentifier");
377
+ if (!identifier)
378
+ return null;
379
+ return state.doc.sliceString(identifier.from, identifier.to);
380
+ }
381
+ const DEFAULT_STYLE_FACTORIES = ["StyleSheet.create"];
382
+ function calleeText(state, call) {
383
+ const callee = call.firstChild;
384
+ if (!callee)
385
+ return null;
386
+ return state.doc.sliceString(callee.from, callee.to);
387
+ }
388
+ // Detects whether the cursor sits inside a React Native style dictionary:
389
+ // either a JSX `style={{ ... }}` attribute value, or one of the per-key
390
+ // objects passed to `StyleSheet.create({ ... })` (or an equivalent factory
391
+ // name supplied via `styleFactories`). Only the *nearest* enclosing
392
+ // ObjectExpression is considered — the outer object passed to
393
+ // StyleSheet.create has arbitrary developer-chosen keys (its values are the
394
+ // actual style dicts), not style property names itself.
395
+ function findStyleObjectContext(state, pos, styleFactories = DEFAULT_STYLE_FACTORIES) {
396
+ const tree = syntaxTree(state);
397
+ for (let cur = tree.resolveInner(pos, -1); cur; cur = cur.parent) {
398
+ if (cur.name !== "ObjectExpression")
399
+ continue;
400
+ const parent = cur.parent;
401
+ if (parent && parent.name === "JSXEscape") {
402
+ const attr = parent.parent;
403
+ if (attr && attr.name === "JSXAttribute") {
404
+ const idNode = attr.getChild("JSXIdentifier");
405
+ if (idNode && state.doc.sliceString(idNode.from, idNode.to) === "style") {
406
+ return cur;
407
+ }
408
+ }
409
+ }
410
+ // Nested inside `<factory>({ key: { ← here } })`.
411
+ if (parent && parent.name === "Property") {
412
+ const outerObject = parent.parent;
413
+ if (outerObject && outerObject.name === "ObjectExpression") {
414
+ const argList = outerObject.parent;
415
+ if (argList && argList.name === "ArgList") {
416
+ const call = argList.parent;
417
+ if (call && call.name === "CallExpression") {
418
+ const text = calleeText(state, call);
419
+ if (text && styleFactories.includes(text))
420
+ return cur;
421
+ }
422
+ }
423
+ }
424
+ }
425
+ // Only the nearest enclosing ObjectExpression is ever a candidate.
426
+ break;
427
+ }
428
+ return null;
429
+ }
430
+ function classifyStylePosition(state, pos, objectExpression) {
431
+ for (const property of objectExpression.getChildren("Property")) {
432
+ if (pos < property.from || pos > property.to)
433
+ continue;
434
+ const colon = property.getChild(":");
435
+ const propDef = property.getChild("PropertyDefinition");
436
+ const propertyName = propDef ? state.doc.sliceString(propDef.from, propDef.to) : undefined;
437
+ if (colon && pos > colon.to) {
438
+ return { kind: "value", objectExpression, property, propertyName };
439
+ }
440
+ return { kind: "name", objectExpression, property };
441
+ }
442
+ return { kind: "name", objectExpression };
443
+ }
444
+ function getExistingStyleKeys(state, objectExpression) {
445
+ const keys = new Set();
446
+ for (const property of objectExpression.getChildren("Property")) {
447
+ const propDef = property.getChild("PropertyDefinition");
448
+ if (propDef)
449
+ keys.add(state.doc.sliceString(propDef.from, propDef.to));
450
+ }
451
+ return keys;
452
+ }
453
+
454
+ function mergeModules(config) {
455
+ if (!(config === null || config === void 0 ? void 0 : config.modules))
456
+ return BUILTIN_MODULES;
457
+ return Object.assign(Object.assign({}, BUILTIN_MODULES), config.modules);
458
+ }
459
+ function resolveEntry(modules, source, localName, imported) {
460
+ const moduleExports = modules[source];
461
+ if (!moduleExports)
462
+ return null;
463
+ const exportedName = imported === "default" || imported === "*" ? localName : imported;
464
+ const entry = moduleExports[exportedName];
465
+ return entry ? { entry, exportedName } : null;
466
+ }
467
+ const IDENTIFIER_WORD = /[A-Za-z_$][\w$]*$/;
468
+ const MODULE_SPECIFIER_WORD = /[\w@/.-]*$/;
469
+ const STYLE_VALUE_WORD = /[\w-]*$/;
470
+ function completeImportModuleSpecifier(context, modules) {
471
+ const word = context.matchBefore(MODULE_SPECIFIER_WORD);
472
+ const from = word ? word.from : context.pos;
473
+ const options = Object.keys(modules).map((name) => ({ label: name, type: "namespace" }));
474
+ return { from, options, validFor: MODULE_SPECIFIER_WORD };
475
+ }
476
+ function completeImportSymbols(context, importDecl, modules) {
477
+ const source = getImportSource(context.state, importDecl);
478
+ if (!source)
479
+ return null;
480
+ const moduleExports = modules[source];
481
+ if (!moduleExports)
482
+ return null;
483
+ const alreadyImported = getImportGroupNames(context.state, importDecl);
484
+ const word = context.matchBefore(IDENTIFIER_WORD);
485
+ const from = word ? word.from : context.pos;
486
+ const options = Object.entries(moduleExports)
487
+ .filter(([name]) => !alreadyImported.has(name))
488
+ .map(([name, entry]) => ({
489
+ label: name,
490
+ type: entry.type === "function" ? "function" : entry.type === "type" ? "class" : "variable",
491
+ detail: entry.detail || source,
492
+ }));
493
+ if (!options.length)
494
+ return null;
495
+ return { from, options, validFor: /^[\w$]*$/ };
496
+ }
497
+ function completeStyleProperty(context, styleFactories) {
498
+ var _a;
499
+ const objectExpression = findStyleObjectContext(context.state, context.pos, styleFactories);
500
+ if (!objectExpression)
501
+ return null;
502
+ const position = classifyStylePosition(context.state, context.pos, objectExpression);
503
+ if (position.kind === "value") {
504
+ const values = position.propertyName ? STYLE_PROPERTIES[position.propertyName] : undefined;
505
+ if (!values)
506
+ return null;
507
+ const valueNode = (_a = position.property) === null || _a === void 0 ? void 0 : _a.getChild("String");
508
+ const insideString = !!valueNode && context.pos > valueNode.from && context.pos <= valueNode.to;
509
+ const word = context.matchBefore(STYLE_VALUE_WORD);
510
+ const from = word ? word.from : context.pos;
511
+ const options = insideString
512
+ ? values.map((v) => ({ label: v, type: "constant" }))
513
+ : values.map((v) => ({ label: v, type: "constant", apply: `'${v}'` }));
514
+ return { from, options, validFor: STYLE_VALUE_WORD };
515
+ }
516
+ const existing = getExistingStyleKeys(context.state, objectExpression);
517
+ const word = context.matchBefore(IDENTIFIER_WORD);
518
+ const from = word ? word.from : context.pos;
519
+ const options = Object.keys(STYLE_PROPERTIES)
520
+ .filter((name) => !existing.has(name))
521
+ .map((name) => ({ label: name, type: "property" }));
522
+ return { from, options, validFor: /^[\w$]*$/ };
523
+ }
524
+ /**
525
+ * Builds the React/React Native contextual CompletionSource: import-scoped
526
+ * hook/component/prop suggestions, JSX tag-name and attribute-name
527
+ * completion, RN style dictionary property/value completion (inside
528
+ * `style={{ }}` or `StyleSheet.create({ ... })`), and import statement
529
+ * completion (module specifiers, then that module's exports).
530
+ *
531
+ * Nothing from `modules` is ever suggested as a hook/component/prop unless
532
+ * its local name is actually present in the current document's own import
533
+ * statements — this is the scoping rule the completion source is built
534
+ * around, not an incidental detail.
535
+ */
536
+ function reactNativeCompletionSource(config) {
537
+ var _a;
538
+ const modules = mergeModules(config);
539
+ const styleFactories = ((_a = config === null || config === void 0 ? void 0 : config.styleFactories) === null || _a === void 0 ? void 0 : _a.length)
540
+ ? [...new Set([...config.styleFactories, "StyleSheet.create"])]
541
+ : ["StyleSheet.create"];
542
+ return (context) => {
543
+ const { state, pos } = context;
544
+ const tree = syntaxTree(state);
545
+ const node = tree.resolveInner(pos, -1);
546
+ const importDecl = findImportDeclaration(node);
547
+ if (importDecl) {
548
+ const stringNode = importDecl.getChild("String");
549
+ if (stringNode && pos >= stringNode.from && pos <= stringNode.to) {
550
+ return completeImportModuleSpecifier(context, modules);
551
+ }
552
+ if (importDecl.getChild("ImportGroup")) {
553
+ return completeImportSymbols(context, importDecl, modules);
554
+ }
555
+ }
556
+ const styleResult = completeStyleProperty(context, styleFactories);
557
+ if (styleResult)
558
+ return styleResult;
559
+ const imports = collectImports(state);
560
+ if (imports.size === 0)
561
+ return null;
562
+ const jsxContext = classifyJsxContext(state, pos);
563
+ if (jsxContext && jsxContext.kind === "tag-name") {
564
+ const word = context.matchBefore(IDENTIFIER_WORD);
565
+ const from = word ? word.from : pos;
566
+ const options = [];
567
+ for (const [localName, importInfo] of imports) {
568
+ const found = resolveEntry(modules, importInfo.source, localName, importInfo.imported);
569
+ if (found && found.entry.type === "type") {
570
+ options.push({ label: localName, type: "class", detail: found.entry.detail || importInfo.source });
571
+ }
572
+ }
573
+ if (!options.length)
574
+ return null;
575
+ return { from, options, validFor: /^[\w$]*$/ };
576
+ }
577
+ if (jsxContext && jsxContext.kind === "attribute-name") {
578
+ const tagName = getTagName(state, jsxContext.openTag);
579
+ if (!tagName)
580
+ return null;
581
+ const importInfo = imports.get(tagName);
582
+ if (!importInfo)
583
+ return null;
584
+ const found = resolveEntry(modules, importInfo.source, tagName, importInfo.imported);
585
+ if (!found || !found.entry.props)
586
+ return null;
587
+ const word = context.matchBefore(IDENTIFIER_WORD);
588
+ const from = word ? word.from : pos;
589
+ const options = found.entry.props.map((prop) => ({ label: prop, type: "property" }));
590
+ return { from, options, validFor: /^[\w$]*$/ };
591
+ }
592
+ // General scope: offer every imported hook/variable/component by name,
593
+ // e.g. `useSta|` outside any JSX.
594
+ const word = context.matchBefore(IDENTIFIER_WORD);
595
+ if (!word || (word.from === word.to && !context.explicit))
596
+ return null;
597
+ const options = [];
598
+ for (const [localName, importInfo] of imports) {
599
+ const found = resolveEntry(modules, importInfo.source, localName, importInfo.imported);
600
+ if (!found)
601
+ continue;
602
+ options.push({
603
+ label: localName,
604
+ type: found.entry.type === "function" ? "function" : found.entry.type === "type" ? "class" : "variable",
605
+ detail: found.entry.detail || importInfo.source,
606
+ });
607
+ }
608
+ if (!options.length)
609
+ return null;
610
+ return { from: word.from, options, validFor: /^[\w$]*$/ };
611
+ };
612
+ }
613
+
614
+ export { BUILTIN_MODULES, STYLE_PROPERTIES, classifyJsxContext, classifyStylePosition, collectImports, findImportDeclaration, findStyleObjectContext, getExistingStyleKeys, getImportGroupNames, getImportSource, getTagName, reactNativeCompletionSource };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@actualwave/codemirror-lang-react-native",
3
+ "version": "0.1.0",
4
+ "description": "React / React Native contextual completion (hooks, components, props, style properties, import clauses) for CodeMirror 6",
5
+ "keywords": [
6
+ "codemirror",
7
+ "codemirror6",
8
+ "react",
9
+ "react-native",
10
+ "autocomplete",
11
+ "completion"
12
+ ],
13
+ "author": "Oleg Galaburda <burdiuz@gmail.com>",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/burdiuz/codemirror-lang-react-native.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/burdiuz/codemirror-lang-react-native/issues"
20
+ },
21
+ "homepage": "https://github.com/burdiuz/codemirror-lang-react-native#readme",
22
+ "scripts": {
23
+ "build": "rollup -c"
24
+ },
25
+ "publishConfig": {
26
+ "directory": "dist"
27
+ },
28
+ "type": "module",
29
+ "main": "index.cjs",
30
+ "module": "index.js",
31
+ "exports": {
32
+ "types": "./types/index.d.ts",
33
+ "import": "./index.js",
34
+ "require": "./index.cjs"
35
+ },
36
+ "types": "types/index.d.ts",
37
+ "sideEffects": false,
38
+ "peerDependencies": {
39
+ "@codemirror/autocomplete": "^6.0.0",
40
+ "@codemirror/language": "^6.0.0",
41
+ "@codemirror/state": "^6.0.0",
42
+ "@lezer/common": "^1.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@codemirror/autocomplete": "^6.20.3",
46
+ "@codemirror/language": "^6.10.0",
47
+ "@codemirror/state": "^6.4.0",
48
+ "@lezer/common": "^1.2.0",
49
+ "@rollup/plugin-typescript": "^12.1.4",
50
+ "@types/node": "^24.1.0",
51
+ "rollup": "^4.45.1",
52
+ "rollup-plugin-copy": "^3.5.0",
53
+ "tslib": "^2.8.1",
54
+ "typescript": "^5.8.3"
55
+ },
56
+ "license": "MIT"
57
+ }