@player-tools/typescript-expression-plugin 0.2.2--canary.20.454
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/dist/index.cjs.js +210 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.esm.js +188 -0
- package/package.json +33 -0
- package/src/index.ts +23 -0
- package/src/logger.ts +14 -0
- package/src/service.ts +188 -0
- package/src/utils.ts +45 -0
- package/src/virtual-service-host.ts +72 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var typescriptTemplateLanguageServiceDecorator = require('typescript-template-language-service-decorator');
|
|
4
|
+
var ts = require('typescript/lib/tsserverlibrary');
|
|
5
|
+
var player = require('@player-ui/player');
|
|
6
|
+
|
|
7
|
+
function _interopNamespace(e) {
|
|
8
|
+
if (e && e.__esModule) return e;
|
|
9
|
+
var n = Object.create(null);
|
|
10
|
+
if (e) {
|
|
11
|
+
Object.keys(e).forEach(function (k) {
|
|
12
|
+
if (k !== 'default') {
|
|
13
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return e[k]; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
n["default"] = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var ts__namespace = /*#__PURE__*/_interopNamespace(ts);
|
|
26
|
+
|
|
27
|
+
function isInRange(position, location) {
|
|
28
|
+
return position.character >= location.start.character && position.character <= location.end.character;
|
|
29
|
+
}
|
|
30
|
+
function getTokenAtPosition(node, position) {
|
|
31
|
+
var _a;
|
|
32
|
+
if (node.type === "CallExpression") {
|
|
33
|
+
const anyArgs = node.args.find((arg) => {
|
|
34
|
+
return getTokenAtPosition(arg, position);
|
|
35
|
+
});
|
|
36
|
+
if (anyArgs) {
|
|
37
|
+
return anyArgs;
|
|
38
|
+
}
|
|
39
|
+
const asTarget = getTokenAtPosition(node.callTarget, position);
|
|
40
|
+
if (asTarget) {
|
|
41
|
+
return asTarget;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (node.type === "Assignment") {
|
|
45
|
+
const asTarget = (_a = getTokenAtPosition(node.left, position)) != null ? _a : getTokenAtPosition(node.right, position);
|
|
46
|
+
if (asTarget) {
|
|
47
|
+
return asTarget;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (node.location && isInRange(position, node.location)) {
|
|
51
|
+
return node;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class ExpressionLanguageService {
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this._expressions = new Map();
|
|
58
|
+
this.logger = options == null ? void 0 : options.logger;
|
|
59
|
+
this.setExpressions(new Map([
|
|
60
|
+
[
|
|
61
|
+
"test",
|
|
62
|
+
{
|
|
63
|
+
name: "test",
|
|
64
|
+
description: "test expression",
|
|
65
|
+
args: []
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"foo",
|
|
70
|
+
{
|
|
71
|
+
name: "foo",
|
|
72
|
+
description: "Test foo expression",
|
|
73
|
+
args: [
|
|
74
|
+
{
|
|
75
|
+
name: "path",
|
|
76
|
+
type: "string"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
]));
|
|
82
|
+
}
|
|
83
|
+
setExpressions(expressionData) {
|
|
84
|
+
this._expressions = expressionData;
|
|
85
|
+
}
|
|
86
|
+
getCompletionsAtPosition(context, position) {
|
|
87
|
+
var _a, _b, _c;
|
|
88
|
+
const line = context.text.split(/\n/g)[position.line];
|
|
89
|
+
(_a = this.logger) == null ? void 0 : _a.log(`[expression-LSP] getCompletionsAtPosition: ${line} -- ${context.rawText} -- ${context.text}`);
|
|
90
|
+
const parsed = player.parseExpression(line, { strict: false });
|
|
91
|
+
const token = getTokenAtPosition(parsed, position);
|
|
92
|
+
const completionInfo = {
|
|
93
|
+
isGlobalCompletion: false,
|
|
94
|
+
isMemberCompletion: false,
|
|
95
|
+
isNewIdentifierLocation: false,
|
|
96
|
+
entries: []
|
|
97
|
+
};
|
|
98
|
+
if ((token == null ? void 0 : token.type) === "Identifier") {
|
|
99
|
+
const start = (_c = (_b = token.location) == null ? void 0 : _b.start) != null ? _c : { character: 0 };
|
|
100
|
+
const wordFromStart = line.slice(start.character, position.character);
|
|
101
|
+
const allCompletions = Array.from(this._expressions.keys()).filter((key) => key.startsWith(wordFromStart));
|
|
102
|
+
allCompletions.forEach((c) => {
|
|
103
|
+
completionInfo.entries.push({
|
|
104
|
+
name: c,
|
|
105
|
+
kind: ts__namespace.ScriptElementKind.functionElement,
|
|
106
|
+
sortText: c,
|
|
107
|
+
isRecommended: true
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return completionInfo;
|
|
112
|
+
}
|
|
113
|
+
getQuickInfoAtPosition(context, position) {
|
|
114
|
+
var _a, _b, _c, _d, _e;
|
|
115
|
+
(_a = this.logger) == null ? void 0 : _a.log(`getCompletionsAtPosition: ${context.text}`);
|
|
116
|
+
const parsed = player.parseExpression(context.text, { strict: false });
|
|
117
|
+
const token = getTokenAtPosition(parsed, position);
|
|
118
|
+
if ((token == null ? void 0 : token.type) === "Identifier") {
|
|
119
|
+
const expression = this._expressions.get(token.name);
|
|
120
|
+
if (expression) {
|
|
121
|
+
return {
|
|
122
|
+
textSpan: {
|
|
123
|
+
start: (_c = (_b = token.location) == null ? void 0 : _b.start.character) != null ? _c : 0,
|
|
124
|
+
length: (_e = (_d = token.location) == null ? void 0 : _d.end.character) != null ? _e : 0
|
|
125
|
+
},
|
|
126
|
+
kindModifiers: ts__namespace.ScriptElementKindModifier.none,
|
|
127
|
+
kind: ts__namespace.ScriptElementKind.functionElement,
|
|
128
|
+
documentation: [
|
|
129
|
+
{
|
|
130
|
+
kind: "text",
|
|
131
|
+
text: expression.description
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
getCompletionEntryDetails(context, position, name) {
|
|
140
|
+
var _a, _b;
|
|
141
|
+
const expression = this._expressions.get(name);
|
|
142
|
+
const prefix = [
|
|
143
|
+
{
|
|
144
|
+
text: ", ",
|
|
145
|
+
kind: ts__namespace.ScriptElementKind.unknown
|
|
146
|
+
}
|
|
147
|
+
];
|
|
148
|
+
const completionDetails = {
|
|
149
|
+
name,
|
|
150
|
+
kind: ts__namespace.ScriptElementKind.functionElement,
|
|
151
|
+
kindModifiers: ts__namespace.ScriptElementKindModifier.none,
|
|
152
|
+
documentation: [
|
|
153
|
+
{
|
|
154
|
+
kind: "text",
|
|
155
|
+
text: (_a = expression == null ? void 0 : expression.description) != null ? _a : "Some description"
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
displayParts: [
|
|
159
|
+
{
|
|
160
|
+
text: name,
|
|
161
|
+
kind: ts__namespace.ScriptElementKind.functionElement
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
text: "(",
|
|
165
|
+
kind: ts__namespace.ScriptElementKind.unknown
|
|
166
|
+
},
|
|
167
|
+
...(_b = expression == null ? void 0 : expression.args.flatMap((arg, index) => [
|
|
168
|
+
...index === 0 ? [] : prefix,
|
|
169
|
+
{
|
|
170
|
+
text: arg.name,
|
|
171
|
+
kind: ts__namespace.ScriptElementKind.parameterElement
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
text: ": ",
|
|
175
|
+
kind: ts__namespace.ScriptElementKind.unknown
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
text: arg.type,
|
|
179
|
+
kind: ts__namespace.ScriptElementKind.typeParameterElement
|
|
180
|
+
}
|
|
181
|
+
])) != null ? _b : [],
|
|
182
|
+
{
|
|
183
|
+
text: ")",
|
|
184
|
+
kind: ts__namespace.ScriptElementKind.unknown
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
return completionDetails;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class LSPLogger {
|
|
193
|
+
constructor(info) {
|
|
194
|
+
this.info = info;
|
|
195
|
+
}
|
|
196
|
+
log(msg) {
|
|
197
|
+
this.info.project.projectService.logger.info(`[player-expr-lsp] ${msg}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = (mod) => {
|
|
202
|
+
return {
|
|
203
|
+
create(info) {
|
|
204
|
+
const logger = new LSPLogger(info);
|
|
205
|
+
const templateService = new ExpressionLanguageService({ logger });
|
|
206
|
+
return typescriptTemplateLanguageServiceDecorator.decorateWithTemplateLanguageService(mod.typescript, info.languageService, info.project, templateService, { tags: ["e", "expr"], enableForStringWithSubstitutions: true }, { logger });
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { decorateWithTemplateLanguageService } from 'typescript-template-language-service-decorator';
|
|
2
|
+
import * as ts from 'typescript/lib/tsserverlibrary';
|
|
3
|
+
import { parseExpression } from '@player-ui/player';
|
|
4
|
+
|
|
5
|
+
function isInRange(position, location) {
|
|
6
|
+
return position.character >= location.start.character && position.character <= location.end.character;
|
|
7
|
+
}
|
|
8
|
+
function getTokenAtPosition(node, position) {
|
|
9
|
+
var _a;
|
|
10
|
+
if (node.type === "CallExpression") {
|
|
11
|
+
const anyArgs = node.args.find((arg) => {
|
|
12
|
+
return getTokenAtPosition(arg, position);
|
|
13
|
+
});
|
|
14
|
+
if (anyArgs) {
|
|
15
|
+
return anyArgs;
|
|
16
|
+
}
|
|
17
|
+
const asTarget = getTokenAtPosition(node.callTarget, position);
|
|
18
|
+
if (asTarget) {
|
|
19
|
+
return asTarget;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (node.type === "Assignment") {
|
|
23
|
+
const asTarget = (_a = getTokenAtPosition(node.left, position)) != null ? _a : getTokenAtPosition(node.right, position);
|
|
24
|
+
if (asTarget) {
|
|
25
|
+
return asTarget;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (node.location && isInRange(position, node.location)) {
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class ExpressionLanguageService {
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this._expressions = new Map();
|
|
36
|
+
this.logger = options == null ? void 0 : options.logger;
|
|
37
|
+
this.setExpressions(new Map([
|
|
38
|
+
[
|
|
39
|
+
"test",
|
|
40
|
+
{
|
|
41
|
+
name: "test",
|
|
42
|
+
description: "test expression",
|
|
43
|
+
args: []
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"foo",
|
|
48
|
+
{
|
|
49
|
+
name: "foo",
|
|
50
|
+
description: "Test foo expression",
|
|
51
|
+
args: [
|
|
52
|
+
{
|
|
53
|
+
name: "path",
|
|
54
|
+
type: "string"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
]));
|
|
60
|
+
}
|
|
61
|
+
setExpressions(expressionData) {
|
|
62
|
+
this._expressions = expressionData;
|
|
63
|
+
}
|
|
64
|
+
getCompletionsAtPosition(context, position) {
|
|
65
|
+
var _a, _b, _c;
|
|
66
|
+
const line = context.text.split(/\n/g)[position.line];
|
|
67
|
+
(_a = this.logger) == null ? void 0 : _a.log(`[expression-LSP] getCompletionsAtPosition: ${line} -- ${context.rawText} -- ${context.text}`);
|
|
68
|
+
const parsed = parseExpression(line, { strict: false });
|
|
69
|
+
const token = getTokenAtPosition(parsed, position);
|
|
70
|
+
const completionInfo = {
|
|
71
|
+
isGlobalCompletion: false,
|
|
72
|
+
isMemberCompletion: false,
|
|
73
|
+
isNewIdentifierLocation: false,
|
|
74
|
+
entries: []
|
|
75
|
+
};
|
|
76
|
+
if ((token == null ? void 0 : token.type) === "Identifier") {
|
|
77
|
+
const start = (_c = (_b = token.location) == null ? void 0 : _b.start) != null ? _c : { character: 0 };
|
|
78
|
+
const wordFromStart = line.slice(start.character, position.character);
|
|
79
|
+
const allCompletions = Array.from(this._expressions.keys()).filter((key) => key.startsWith(wordFromStart));
|
|
80
|
+
allCompletions.forEach((c) => {
|
|
81
|
+
completionInfo.entries.push({
|
|
82
|
+
name: c,
|
|
83
|
+
kind: ts.ScriptElementKind.functionElement,
|
|
84
|
+
sortText: c,
|
|
85
|
+
isRecommended: true
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return completionInfo;
|
|
90
|
+
}
|
|
91
|
+
getQuickInfoAtPosition(context, position) {
|
|
92
|
+
var _a, _b, _c, _d, _e;
|
|
93
|
+
(_a = this.logger) == null ? void 0 : _a.log(`getCompletionsAtPosition: ${context.text}`);
|
|
94
|
+
const parsed = parseExpression(context.text, { strict: false });
|
|
95
|
+
const token = getTokenAtPosition(parsed, position);
|
|
96
|
+
if ((token == null ? void 0 : token.type) === "Identifier") {
|
|
97
|
+
const expression = this._expressions.get(token.name);
|
|
98
|
+
if (expression) {
|
|
99
|
+
return {
|
|
100
|
+
textSpan: {
|
|
101
|
+
start: (_c = (_b = token.location) == null ? void 0 : _b.start.character) != null ? _c : 0,
|
|
102
|
+
length: (_e = (_d = token.location) == null ? void 0 : _d.end.character) != null ? _e : 0
|
|
103
|
+
},
|
|
104
|
+
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
105
|
+
kind: ts.ScriptElementKind.functionElement,
|
|
106
|
+
documentation: [
|
|
107
|
+
{
|
|
108
|
+
kind: "text",
|
|
109
|
+
text: expression.description
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
getCompletionEntryDetails(context, position, name) {
|
|
118
|
+
var _a, _b;
|
|
119
|
+
const expression = this._expressions.get(name);
|
|
120
|
+
const prefix = [
|
|
121
|
+
{
|
|
122
|
+
text: ", ",
|
|
123
|
+
kind: ts.ScriptElementKind.unknown
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
const completionDetails = {
|
|
127
|
+
name,
|
|
128
|
+
kind: ts.ScriptElementKind.functionElement,
|
|
129
|
+
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
130
|
+
documentation: [
|
|
131
|
+
{
|
|
132
|
+
kind: "text",
|
|
133
|
+
text: (_a = expression == null ? void 0 : expression.description) != null ? _a : "Some description"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
displayParts: [
|
|
137
|
+
{
|
|
138
|
+
text: name,
|
|
139
|
+
kind: ts.ScriptElementKind.functionElement
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
text: "(",
|
|
143
|
+
kind: ts.ScriptElementKind.unknown
|
|
144
|
+
},
|
|
145
|
+
...(_b = expression == null ? void 0 : expression.args.flatMap((arg, index) => [
|
|
146
|
+
...index === 0 ? [] : prefix,
|
|
147
|
+
{
|
|
148
|
+
text: arg.name,
|
|
149
|
+
kind: ts.ScriptElementKind.parameterElement
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
text: ": ",
|
|
153
|
+
kind: ts.ScriptElementKind.unknown
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
text: arg.type,
|
|
157
|
+
kind: ts.ScriptElementKind.typeParameterElement
|
|
158
|
+
}
|
|
159
|
+
])) != null ? _b : [],
|
|
160
|
+
{
|
|
161
|
+
text: ")",
|
|
162
|
+
kind: ts.ScriptElementKind.unknown
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
|
166
|
+
return completionDetails;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
class LSPLogger {
|
|
171
|
+
constructor(info) {
|
|
172
|
+
this.info = info;
|
|
173
|
+
}
|
|
174
|
+
log(msg) {
|
|
175
|
+
this.info.project.projectService.logger.info(`[player-expr-lsp] ${msg}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = (mod) => {
|
|
180
|
+
return {
|
|
181
|
+
create(info) {
|
|
182
|
+
const logger = new LSPLogger(info);
|
|
183
|
+
const templateService = new ExpressionLanguageService({ logger });
|
|
184
|
+
return decorateWithTemplateLanguageService(mod.typescript, info.languageService, info.project, templateService, { tags: ["e", "expr"], enableForStringWithSubstitutions: true }, { logger });
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
//# sourceMappingURL=index.esm.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@player-tools/typescript-expression-plugin",
|
|
3
|
+
"version": "0.2.2--canary.20.454",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org"
|
|
7
|
+
},
|
|
8
|
+
"peerDependencies": {},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@player-ui/player": "0.3.1--canary.117.4130",
|
|
11
|
+
"typescript-template-language-service-decorator": "^2.3.1",
|
|
12
|
+
"@babel/runtime": "7.15.4"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/index.cjs.js",
|
|
15
|
+
"module": "dist/index.esm.js",
|
|
16
|
+
"typings": "dist/index.d.ts",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/player-ui/tools"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/player-ui/tools/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://player-ui.github.io",
|
|
27
|
+
"contributors": [
|
|
28
|
+
{
|
|
29
|
+
"name": "Ketan Reddy",
|
|
30
|
+
"url": "https://github.com/KetanReddy"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as ts from 'typescript/lib/tsserverlibrary';
|
|
2
|
+
import { decorateWithTemplateLanguageService } from 'typescript-template-language-service-decorator';
|
|
3
|
+
import { ExpressionLanguageService } from './service';
|
|
4
|
+
import { LSPLogger } from './logger';
|
|
5
|
+
|
|
6
|
+
export = (mod: { typescript: typeof ts }) => {
|
|
7
|
+
return {
|
|
8
|
+
create(info: ts.server.PluginCreateInfo): ts.LanguageService {
|
|
9
|
+
const logger = new LSPLogger(info);
|
|
10
|
+
|
|
11
|
+
const templateService = new ExpressionLanguageService({ logger });
|
|
12
|
+
|
|
13
|
+
return decorateWithTemplateLanguageService(
|
|
14
|
+
mod.typescript,
|
|
15
|
+
info.languageService,
|
|
16
|
+
info.project,
|
|
17
|
+
templateService,
|
|
18
|
+
{ tags: ['e', 'expr'], enableForStringWithSubstitutions: true },
|
|
19
|
+
{ logger }
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Logger } from 'typescript-template-language-service-decorator';
|
|
2
|
+
import type * as ts from 'typescript/lib/tsserverlibrary';
|
|
3
|
+
|
|
4
|
+
export class LSPLogger implements Logger {
|
|
5
|
+
private readonly info: ts.server.PluginCreateInfo;
|
|
6
|
+
|
|
7
|
+
constructor(info: ts.server.PluginCreateInfo) {
|
|
8
|
+
this.info = info;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
log(msg: string) {
|
|
12
|
+
this.info.project.projectService.logger.info(`[player-expr-lsp] ${msg}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as ts from 'typescript/lib/tsserverlibrary';
|
|
2
|
+
import type {
|
|
3
|
+
TemplateLanguageService,
|
|
4
|
+
TemplateContext,
|
|
5
|
+
Logger,
|
|
6
|
+
} from 'typescript-template-language-service-decorator';
|
|
7
|
+
import { parseExpression } from '@player-ui/player';
|
|
8
|
+
import { getTokenAtPosition } from './utils';
|
|
9
|
+
|
|
10
|
+
// TODO: replace this with the XLR definitions and types
|
|
11
|
+
interface ExprDetails {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
args: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ExpressionLanguageService implements TemplateLanguageService {
|
|
21
|
+
private logger?: Logger;
|
|
22
|
+
private _expressions = new Map<string, ExprDetails>();
|
|
23
|
+
|
|
24
|
+
constructor(options?: { logger?: Logger }) {
|
|
25
|
+
this.logger = options?.logger;
|
|
26
|
+
|
|
27
|
+
this.setExpressions(
|
|
28
|
+
new Map([
|
|
29
|
+
[
|
|
30
|
+
'test',
|
|
31
|
+
{
|
|
32
|
+
name: 'test',
|
|
33
|
+
description: 'test expression',
|
|
34
|
+
args: [],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
'foo',
|
|
39
|
+
{
|
|
40
|
+
name: 'foo',
|
|
41
|
+
description: 'Test foo expression',
|
|
42
|
+
args: [
|
|
43
|
+
{
|
|
44
|
+
name: 'path',
|
|
45
|
+
type: 'string',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setExpressions(expressionData: Map<string, ExprDetails>) {
|
|
55
|
+
this._expressions = expressionData;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getCompletionsAtPosition(
|
|
59
|
+
context: TemplateContext,
|
|
60
|
+
position: ts.LineAndCharacter
|
|
61
|
+
): ts.CompletionInfo {
|
|
62
|
+
const line = context.text.split(/\n/g)[position.line];
|
|
63
|
+
this.logger?.log(
|
|
64
|
+
`[expression-LSP] getCompletionsAtPosition: ${line} -- ${context.rawText} -- ${context.text}`
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const parsed = parseExpression(line, { strict: false });
|
|
68
|
+
const token = getTokenAtPosition(parsed, position);
|
|
69
|
+
|
|
70
|
+
const completionInfo: ts.CompletionInfo = {
|
|
71
|
+
isGlobalCompletion: false,
|
|
72
|
+
isMemberCompletion: false,
|
|
73
|
+
isNewIdentifierLocation: false,
|
|
74
|
+
entries: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (token?.type === 'Identifier') {
|
|
78
|
+
// get the relevant start of the identifier
|
|
79
|
+
const start = token.location?.start ?? { character: 0 };
|
|
80
|
+
const wordFromStart = line.slice(start.character, position.character);
|
|
81
|
+
const allCompletions = Array.from(this._expressions.keys()).filter(
|
|
82
|
+
(key) => key.startsWith(wordFromStart)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
allCompletions.forEach((c) => {
|
|
86
|
+
completionInfo.entries.push({
|
|
87
|
+
name: c,
|
|
88
|
+
kind: ts.ScriptElementKind.functionElement,
|
|
89
|
+
sortText: c,
|
|
90
|
+
isRecommended: true,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return completionInfo;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getQuickInfoAtPosition(
|
|
99
|
+
context: TemplateContext,
|
|
100
|
+
position: ts.LineAndCharacter
|
|
101
|
+
): ts.QuickInfo | undefined {
|
|
102
|
+
this.logger?.log(`getCompletionsAtPosition: ${context.text}`);
|
|
103
|
+
|
|
104
|
+
const parsed = parseExpression(context.text, { strict: false });
|
|
105
|
+
const token = getTokenAtPosition(parsed, position);
|
|
106
|
+
|
|
107
|
+
if (token?.type === 'Identifier') {
|
|
108
|
+
const expression = this._expressions.get(token.name);
|
|
109
|
+
if (expression) {
|
|
110
|
+
return {
|
|
111
|
+
textSpan: {
|
|
112
|
+
start: token.location?.start.character ?? 0,
|
|
113
|
+
length: token.location?.end.character ?? 0,
|
|
114
|
+
},
|
|
115
|
+
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
116
|
+
kind: ts.ScriptElementKind.functionElement,
|
|
117
|
+
documentation: [
|
|
118
|
+
{
|
|
119
|
+
kind: 'text',
|
|
120
|
+
text: expression.description,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getCompletionEntryDetails(
|
|
131
|
+
context: TemplateContext,
|
|
132
|
+
position: ts.LineAndCharacter,
|
|
133
|
+
name: string
|
|
134
|
+
): ts.CompletionEntryDetails {
|
|
135
|
+
const expression = this._expressions.get(name);
|
|
136
|
+
|
|
137
|
+
const prefix = [
|
|
138
|
+
{
|
|
139
|
+
text: ', ',
|
|
140
|
+
kind: ts.ScriptElementKind.unknown,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const completionDetails: ts.CompletionEntryDetails = {
|
|
145
|
+
name,
|
|
146
|
+
kind: ts.ScriptElementKind.functionElement,
|
|
147
|
+
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
148
|
+
documentation: [
|
|
149
|
+
{
|
|
150
|
+
kind: 'text',
|
|
151
|
+
text: expression?.description ?? 'Some description',
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
displayParts: [
|
|
155
|
+
{
|
|
156
|
+
text: name,
|
|
157
|
+
kind: ts.ScriptElementKind.functionElement,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
text: '(',
|
|
161
|
+
kind: ts.ScriptElementKind.unknown,
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
...(expression?.args.flatMap((arg, index) => [
|
|
165
|
+
...(index === 0 ? [] : prefix),
|
|
166
|
+
{
|
|
167
|
+
text: arg.name,
|
|
168
|
+
kind: ts.ScriptElementKind.parameterElement,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
text: ': ',
|
|
172
|
+
kind: ts.ScriptElementKind.unknown,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
text: arg.type,
|
|
176
|
+
kind: ts.ScriptElementKind.typeParameterElement,
|
|
177
|
+
},
|
|
178
|
+
]) ?? []),
|
|
179
|
+
{
|
|
180
|
+
text: ')',
|
|
181
|
+
kind: ts.ScriptElementKind.unknown,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return completionDetails;
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Position } from 'vscode-languageserver-types';
|
|
2
|
+
import type { ExpressionNode, NodeLocation } from '@player-ui/player';
|
|
3
|
+
|
|
4
|
+
/** Check if the vscode position overlaps with the expression location */
|
|
5
|
+
export function isInRange(position: Position, location: NodeLocation) {
|
|
6
|
+
return (
|
|
7
|
+
position.character >= location.start.character &&
|
|
8
|
+
position.character <= location.end.character
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Find the closest marked token at the given position */
|
|
13
|
+
export function getTokenAtPosition(
|
|
14
|
+
node: ExpressionNode,
|
|
15
|
+
position: Position
|
|
16
|
+
): ExpressionNode | undefined {
|
|
17
|
+
if (node.type === 'CallExpression') {
|
|
18
|
+
const anyArgs = node.args.find((arg) => {
|
|
19
|
+
return getTokenAtPosition(arg, position);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (anyArgs) {
|
|
23
|
+
return anyArgs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const asTarget = getTokenAtPosition(node.callTarget, position);
|
|
27
|
+
if (asTarget) {
|
|
28
|
+
return asTarget;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (node.type === 'Assignment') {
|
|
33
|
+
const asTarget =
|
|
34
|
+
getTokenAtPosition(node.left, position) ??
|
|
35
|
+
getTokenAtPosition(node.right, position);
|
|
36
|
+
if (asTarget) {
|
|
37
|
+
return asTarget;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Lastly check for yourself
|
|
42
|
+
if (node.location && isInRange(position, node.location)) {
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type * as ts from 'typescript/lib/tsserverlibrary';
|
|
2
|
+
|
|
3
|
+
export default class VirtualServiceHost implements ts.LanguageServiceHost {
|
|
4
|
+
private readonly files = new Map<string, string>();
|
|
5
|
+
private readonly typescript: typeof ts;
|
|
6
|
+
private readonly compilerOptions: ts.CompilerOptions;
|
|
7
|
+
private readonly workspacePath: string;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
typescript: typeof ts,
|
|
11
|
+
compilerOptions: ts.CompilerOptions,
|
|
12
|
+
workspacePath: string
|
|
13
|
+
) {
|
|
14
|
+
this.typescript = typescript;
|
|
15
|
+
this.compilerOptions = compilerOptions;
|
|
16
|
+
this.workspacePath = workspacePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
withFile<T>(fileName: string, content: string, callback: () => T): T {
|
|
20
|
+
this.files.set(fileName, content);
|
|
21
|
+
const result = callback();
|
|
22
|
+
this.files.delete(fileName);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getCompilationSettings() {
|
|
27
|
+
return this.compilerOptions;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getScriptFileNames() {
|
|
31
|
+
return Array.from(this.files.keys());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getScriptKind() {
|
|
35
|
+
return this.typescript.ScriptKind.TS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getScriptVersion() {
|
|
39
|
+
return '0';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getScriptSnapshot(fileName: string) {
|
|
43
|
+
const fileText = this.readFile(fileName);
|
|
44
|
+
if (fileText) {
|
|
45
|
+
return this.typescript.ScriptSnapshot.fromString(fileText);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getCurrentDirectory() {
|
|
50
|
+
return this.workspacePath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getDefaultLibFileName(options: ts.CompilerOptions) {
|
|
54
|
+
return this.typescript.getDefaultLibFilePath(options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fileExists(path: string): boolean {
|
|
58
|
+
return path.includes('node_modules')
|
|
59
|
+
? this.typescript.sys.fileExists(path)
|
|
60
|
+
: this.files.has(path);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
readFile(path: string, encoding?: string | undefined): string | undefined {
|
|
64
|
+
return path.includes('node_modules')
|
|
65
|
+
? this.typescript.sys.readFile(path, encoding)
|
|
66
|
+
: this.files.get(path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
useCaseSensitiveFileNames() {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|