brighterscript-xml-plugin 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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # brighterscript-xml-plugin
2
+ ## BrighterScript plugin for SceneGraph XML completions
3
+
4
+ ### It Does:
5
+ - suggest components
6
+ - suggest component fields
7
+
8
+ ### It Does Not:
9
+ - validate field values
10
+ - suggest interface elements like `field` and `function`
11
+
12
+ ### Installation
13
+ ```
14
+ npm i bsc-xml
15
+ ```
16
+ `bsconfig.json`
17
+ ```
18
+ {
19
+ ...
20
+ plugins: [
21
+ 'bsc-xml'
22
+ ]
23
+ }
24
+ ```
25
+
26
+ ### Usage
27
+ SceneGraph Component completions
28
+ ![usage gif](./bsc-xml.gif)
29
+
30
+
31
+ - provides both built-in Roku SceneGraph components and custom project components
32
+ - provides built-in fields and custom fields
33
+ - auto suggestions for fields when typing inside an element tag
34
+ - use completions shortcut to trigger manually
35
+ - component completions on `<` do not fill automatically, pretty sure this is something that needs to be configured at the vscode extension level.
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * unused, just an export of all types for eventual validation
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SGFieldTypes = void 0;
7
+ class SGFieldTypes {
8
+ constructor() {
9
+ this.typeMap = {
10
+ Time: true,
11
+ string: true,
12
+ float: true,
13
+ boolean: true,
14
+ 'option string': true,
15
+ time: true,
16
+ Boolean: true,
17
+ integer: true,
18
+ 'array of floats': true,
19
+ ContentNode: true,
20
+ color: true,
21
+ uri: true,
22
+ rect2d: true,
23
+ Float: true,
24
+ vector2d: true,
25
+ font: true,
26
+ 'N/A': true,
27
+ 'array of strings': true,
28
+ 'Poster node': true,
29
+ Font: true,
30
+ 'array of Boolean': true,
31
+ 'array of colors': true,
32
+ ButtonGroup: true,
33
+ Event: true,
34
+ 'array of integers': true,
35
+ 'RSGPalette node': true,
36
+ "array of float's": true,
37
+ Node: true,
38
+ 'option as string': true,
39
+ 'array of float': true,
40
+ 'URI string': true,
41
+ BusySpinner: true,
42
+ 'color (string containing hex value e.g. RGBA)': true,
43
+ 'array of integer': true,
44
+ 'array of vector2d': true,
45
+ 'associative array': true,
46
+ assocarray: true,
47
+ bool: true,
48
+ 'associative array of associative arrays': true,
49
+ URL: true,
50
+ TargetSet: true,
51
+ int: true,
52
+ 'array of TargetSet nodes': true,
53
+ Color: true,
54
+ 'array of rectangles': true,
55
+ 'roArray of roAssociativeArrays': true,
56
+ };
57
+ }
58
+ }
59
+ exports.SGFieldTypes = SGFieldTypes;
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SGXmlCompletionProvider = void 0;
4
+ const brighterscript_1 = require("brighterscript");
5
+ const vscode_languageserver_types_1 = require("vscode-languageserver-types");
6
+ const roku_types_1 = require("brighterscript/dist/roku-types");
7
+ const SystemCompletion_1 = require("./SystemCompletion");
8
+ const OPEN_CLOSE_TAGS = ['<', '</', '>', '/>'];
9
+ const systemNodes = roku_types_1.nodes;
10
+ class SGXmlCompletionProvider {
11
+ constructor(program) {
12
+ this.program = program;
13
+ this.systemCompletions = this.processSystemNodes();
14
+ }
15
+ /**
16
+ *
17
+ * @returns system nodes indexed by name {[name]: SystemCompletion}
18
+ */
19
+ processSystemNodes() {
20
+ const result = {};
21
+ for (const key in systemNodes) {
22
+ const node = systemNodes[key];
23
+ result[key] = new SystemCompletion_1.SystemCompletion(node);
24
+ }
25
+ return result;
26
+ }
27
+ getFieldCompletions(componentName) {
28
+ var _a;
29
+ componentName = componentName.toLocaleLowerCase();
30
+ const projectComponent = this.program.getComponent(componentName);
31
+ let result = [];
32
+ if (projectComponent) {
33
+ result.push(...this.getCompletionsFromXmlFile(projectComponent.file));
34
+ const extendsName = (_a = projectComponent.file.ast.component) === null || _a === void 0 ? void 0 : _a.extends;
35
+ if (extendsName) {
36
+ result.push(...this.getAllAvailableFields(extendsName));
37
+ }
38
+ }
39
+ else if (systemNodes[componentName]) {
40
+ const node = this.systemCompletions[componentName];
41
+ result.push(...node.fields);
42
+ if (node.extends) {
43
+ result.push(...this.getAllAvailableFields(node.extends));
44
+ }
45
+ }
46
+ else {
47
+ this.program.logger.error(`Unknown component extension: ${componentName}`);
48
+ }
49
+ return result;
50
+ }
51
+ getCompletionsFromXmlFile(xmlFile) {
52
+ var _a, _b;
53
+ return ((_b = (_a = xmlFile.ast.component) === null || _a === void 0 ? void 0 : _a.api.fields.map((f) => {
54
+ return {
55
+ label: f.id,
56
+ detail: `${f.type}${f.value ? `: ${f.value}` : ''}`,
57
+ };
58
+ })) !== null && _b !== void 0 ? _b : []);
59
+ }
60
+ getComponentCompletions(file, beforeToken) {
61
+ const completions = [];
62
+ // suggest components
63
+ completions.unshift(...Object.entries(this.systemCompletions).map(([key, obj]) => {
64
+ return {
65
+ label: obj.name,
66
+ sortText: '<' + obj.name,
67
+ kind: vscode_languageserver_types_1.CompletionItemKind.Class,
68
+ insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
69
+ insertText: `${beforeToken === '<' ? '' : '<'}${obj.name} $0></${obj.name}>`,
70
+ };
71
+ }), ...this.program
72
+ .getScopes()
73
+ .filter(brighterscript_1.isXmlScope)
74
+ .filter((s) => s.xmlFile.componentName !== file.componentName)
75
+ .map((s) => s.xmlFile.componentName)
76
+ .map((name) => {
77
+ return {
78
+ label: name.text,
79
+ sortText: '<' + name.text,
80
+ kind: vscode_languageserver_types_1.CompletionItemKind.Class,
81
+ insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
82
+ insertText: `${beforeToken === '<' ? '' : '<'}${name.text} $0></${name.text}>`,
83
+ };
84
+ }));
85
+ return completions;
86
+ }
87
+ getAllAvailableFields(componentName) {
88
+ var _a, _b, _c, _d, _e, _f;
89
+ const component = this.program.getComponent(componentName);
90
+ let result = [];
91
+ if (component) {
92
+ result = [
93
+ ...((_c = (_b = (_a = component.file.ast.component) === null || _a === void 0 ? void 0 : _a.api) === null || _b === void 0 ? void 0 : _b.fields.map((f) => {
94
+ return {
95
+ name: f.id,
96
+ type: f.type,
97
+ sortText: '0_' + f.id,
98
+ };
99
+ })) !== null && _c !== void 0 ? _c : []),
100
+ ...this.getAllAvailableFields((_e = (_d = component.file.ast.component) === null || _d === void 0 ? void 0 : _d.extends) !== null && _e !== void 0 ? _e : '').map((f) => {
101
+ return Object.assign(Object.assign({}, f), { sortText: '1_' + f.sortText });
102
+ }),
103
+ ];
104
+ }
105
+ else {
106
+ const rokuComponent = roku_types_1.nodes[componentName.toLowerCase()];
107
+ if (rokuComponent) {
108
+ const rokuComponentFields = (_f = rokuComponent.fields) === null || _f === void 0 ? void 0 : _f.filter((f) => { var _a; return (_a = f.accessPermission) === null || _a === void 0 ? void 0 : _a.includes('WRITE'); }).map((f) => {
109
+ return {
110
+ name: f.name,
111
+ type: f.type,
112
+ sortText: '0_' + f.name,
113
+ };
114
+ });
115
+ if (rokuComponent.extends) {
116
+ result = [
117
+ ...rokuComponentFields,
118
+ ...this.getAllAvailableFields(rokuComponent.extends.name).map((f) => {
119
+ return Object.assign(Object.assign({}, f), { sortText: '1_' + f.sortText });
120
+ }),
121
+ ];
122
+ }
123
+ else {
124
+ result = rokuComponentFields;
125
+ }
126
+ }
127
+ }
128
+ const resultMap = {};
129
+ result.forEach((e) => {
130
+ resultMap[e.name] = e;
131
+ });
132
+ result = Object.values(result);
133
+ return result;
134
+ }
135
+ process(event) {
136
+ var _a, _b, _c, _d, _e, _f;
137
+ const { file, position } = event;
138
+ const tokens = event.file.parser.tokens.map((t, i) => {
139
+ return Object.assign({ index: i }, t);
140
+ });
141
+ const cursorToken = tokens.find((t) => {
142
+ (t) => {
143
+ var _a;
144
+ return ((_a = (t.startLine - 1 <= position.line &&
145
+ t.startColumn - 1 < position.character &&
146
+ t.endColumn)) !== null && _a !== void 0 ? _a : 0 > position.character - 1);
147
+ };
148
+ });
149
+ const beforeToken = cursorToken
150
+ ? cursorToken
151
+ : tokens.find((t, i, o) => {
152
+ var _a, _b, _c, _d;
153
+ if (t.endLine <= position.line + 1) {
154
+ const nextStartLine = (_b = (_a = o[i + 1]) === null || _a === void 0 ? void 0 : _a.startLine) !== null && _b !== void 0 ? _b : -1;
155
+ let nextStartColumn = -1;
156
+ if (nextStartLine === position.line + 1) {
157
+ nextStartColumn = (_c = o[i + 1]) === null || _c === void 0 ? void 0 : _c.startColumn;
158
+ }
159
+ else if (nextStartLine > position.line + 1) {
160
+ nextStartColumn = Number.MAX_SAFE_INTEGER;
161
+ }
162
+ return ((t.endLine < position.line + 1 ||
163
+ ((_d = t.endColumn) !== null && _d !== void 0 ? _d : 0) < position.character + 1) &&
164
+ nextStartColumn > position.character);
165
+ }
166
+ return false;
167
+ });
168
+ if (beforeToken) {
169
+ if (beforeToken.image === '<') {
170
+ event.completions.push(...this.getComponentCompletions(file, beforeToken === null || beforeToken === void 0 ? void 0 : beforeToken.image));
171
+ return;
172
+ }
173
+ else {
174
+ let openTag = beforeToken;
175
+ // iterate backwards until we find some kind of open or close tag
176
+ while (openTag) {
177
+ if (OPEN_CLOSE_TAGS.includes(openTag.image)) {
178
+ break;
179
+ }
180
+ openTag = tokens[openTag.index - 1];
181
+ }
182
+ switch (openTag === null || openTag === void 0 ? void 0 : openTag.image) {
183
+ case '<': // we are inside an element definition
184
+ const fields = [];
185
+ let nextField = tokens[openTag.index + 1];
186
+ while (nextField) {
187
+ if (OPEN_CLOSE_TAGS.includes(nextField.image)) {
188
+ break;
189
+ }
190
+ else if (nextField.image === '=') {
191
+ fields.push({
192
+ name: (_b = (_a = tokens[nextField.index - 1]) === null || _a === void 0 ? void 0 : _a.image) !== null && _b !== void 0 ? _b : '',
193
+ value: (_d = (_c = tokens[nextField.index + 1]) === null || _c === void 0 ? void 0 : _c.image.replace(/"/g, '')) !== null && _d !== void 0 ? _d : '',
194
+ });
195
+ }
196
+ nextField = tokens[nextField.index + 1];
197
+ }
198
+ const componentName = (_e = tokens[openTag.index + 1]) === null || _e === void 0 ? void 0 : _e.image;
199
+ event.completions.push(...((_f = this.getAllAvailableFields(componentName !== null && componentName !== void 0 ? componentName : '')
200
+ .filter((f) => !fields.map((f) => f.name).includes(f.name))
201
+ .map((field) => {
202
+ var _a;
203
+ return {
204
+ label: field.name,
205
+ insertText: `${field.name}="\${1:${(_a = field.value) !== null && _a !== void 0 ? _a : ''}}"`,
206
+ kind: vscode_languageserver_types_1.CompletionItemKind.Field,
207
+ insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
208
+ detail: field.type,
209
+ sortText: field.sortText,
210
+ };
211
+ })) !== null && _f !== void 0 ? _f : []));
212
+ return;
213
+ case '>':
214
+ case '/>':
215
+ event.completions.push(...this.getComponentCompletions(file, beforeToken === null || beforeToken === void 0 ? void 0 : beforeToken.image));
216
+ return;
217
+ default:
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ else {
223
+ const childrenOpenToken = tokens.find((token) => {
224
+ return token.image.toLowerCase() === 'children';
225
+ });
226
+ if (childrenOpenToken &&
227
+ (childrenOpenToken.endLine <= position.line ||
228
+ (childrenOpenToken.endLine === position.line + 1 &&
229
+ childrenOpenToken.endColumn < position.character))) {
230
+ event.completions.push(...this.getComponentCompletions(file, undefined));
231
+ }
232
+ else {
233
+ event.completions.push({
234
+ label: 'new component',
235
+ insertText: `<component name="" extends="Group">
236
+
237
+ </component>`,
238
+ });
239
+ }
240
+ }
241
+ }
242
+ }
243
+ exports.SGXmlCompletionProvider = SGXmlCompletionProvider;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SystemCompletion = void 0;
4
+ class SystemCompletion {
5
+ constructor(systemNode) {
6
+ var _a;
7
+ this.name = systemNode.name;
8
+ this.component = {
9
+ label: systemNode.name,
10
+ };
11
+ this.extends = (_a = systemNode.extends) === null || _a === void 0 ? void 0 : _a.name;
12
+ this.fields = systemNode.fields
13
+ .filter((f) => f.accessPermission.includes('WRITE'))
14
+ .map((f) => {
15
+ return {
16
+ label: f.name,
17
+ type: f.type,
18
+ detail: f.default,
19
+ description: f.description,
20
+ };
21
+ });
22
+ }
23
+ }
24
+ exports.SystemCompletion = SystemCompletion;
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BscXmlPlugin = void 0;
4
+ const brighterscript_1 = require("brighterscript");
5
+ const SGXmlCompletionProvider_1 = require("./SGXmlCompletionProvider");
6
+ const crypto_1 = require("crypto");
7
+ class BscXmlPlugin {
8
+ constructor() {
9
+ this.name = 'bsc-xml-plugin';
10
+ this.id = (0, crypto_1.randomUUID)();
11
+ // console.log('creating bsc xml plugin');
12
+ }
13
+ provideCompletions(completionEvent) {
14
+ if ((0, brighterscript_1.isXmlFile)(completionEvent.file)) {
15
+ return new SGXmlCompletionProvider_1.SGXmlCompletionProvider(completionEvent.program).process(completionEvent);
16
+ }
17
+ }
18
+ }
19
+ exports.BscXmlPlugin = BscXmlPlugin;
20
+ exports.default = () => {
21
+ return new BscXmlPlugin();
22
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "brighterscript-xml-plugin",
3
+ "version": "0.0.1",
4
+ "description": "BrighterScript plugin for SceneGraph XML completions",
5
+ "main": "dist/index.js",
6
+ "keywords": [
7
+ "roku",
8
+ "brightscript",
9
+ "scenegraph",
10
+ "brighterscript",
11
+ "bsc"
12
+ ],
13
+ "scripts": {
14
+ "build": "npm run clean && tsc",
15
+ "test": "mocha",
16
+ "clean": "rimraf dist",
17
+ "prepack": "npm run build"
18
+ },
19
+ "mocha": {
20
+ "spec": "src/**/*.spec.ts",
21
+ "require": [
22
+ "source-map-support/register",
23
+ "ts-node/register"
24
+ ],
25
+ "fullTrace": true,
26
+ "timeout": 60000,
27
+ "watchExtensions": [
28
+ "ts"
29
+ ]
30
+ },
31
+ "author": "Sam Heavner",
32
+ "license": "ISC",
33
+ "devDependencies": {
34
+ "@types/chai": "^4.3.4",
35
+ "@types/mocha": "^10.0.1",
36
+ "@types/node": "^18.15.3",
37
+ "brighterscript": "0.65.x",
38
+ "chai": "^4.3.7",
39
+ "mocha": "^10.2.0",
40
+ "rimraf": "^5.0.5",
41
+ "source-map-support": "^0.5.21",
42
+ "ts-node": "^10.9.1",
43
+ "typescript": "^5.0.2",
44
+ "vscode-languageserver-types": "^3.17.3"
45
+ },
46
+ "peerDependencies": {
47
+ "brighterscript": "0.65.x"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "brighterscript": {
51
+ "optional": false
52
+ }
53
+ },
54
+ "files": [
55
+ "dist/*.js",
56
+ "!dist/*.spec.js"
57
+ ]
58
+ }