@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/LICENSE +21 -0
- package/README.md +71 -0
- package/index.cjs +627 -0
- package/index.js +614 -0
- package/package.json +57 -0
- package/types/completion.d.ts +20 -0
- package/types/completion.d.ts.map +1 -0
- package/types/data.d.ts +11 -0
- package/types/data.d.ts.map +1 -0
- package/types/imports.d.ts +11 -0
- package/types/imports.d.ts.map +1 -0
- package/types/index.d.ts +9 -0
- package/types/index.d.ts.map +1 -0
- package/types/jsx.d.ts +21 -0
- package/types/jsx.d.ts.map +1 -0
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
|
+
}
|