@codecanvas-ide/engine 0.1.0 → 0.3.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/README.md +15 -2
- package/dist/codecanvas-engine.js +147 -139
- package/dist/index.js +666 -201
- package/dist/index.js.map +4 -4
- package/dist/types/arrange.d.ts +6 -0
- package/dist/types/canvas.d.ts +4 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/layout.d.ts +7 -0
- package/dist/types/layoutRules.d.ts +26 -1
- package/dist/types/parse.d.ts +8 -1
- package/dist/types/project.d.ts +28 -23
- package/dist/types/projectServer.d.ts +3 -2
- package/dist/types/react.d.ts +30 -0
- package/dist/types/remoteProject.d.ts +3 -1
- package/dist/types/styles.d.ts +1 -1
- package/dist/types/syntax.d.ts +3 -0
- package/dist/types/types.d.ts +15 -2
- package/dist/types/workspace.d.ts +45 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
1
1
|
// src/parse.ts
|
|
2
|
+
import ts3 from "typescript";
|
|
3
|
+
|
|
4
|
+
// src/react.ts
|
|
5
|
+
import ts2 from "typescript";
|
|
6
|
+
|
|
7
|
+
// src/syntax.ts
|
|
2
8
|
import ts from "typescript";
|
|
9
|
+
function unwrap(e) {
|
|
10
|
+
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e)) e = e.expression;
|
|
11
|
+
return e;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/react.ts
|
|
15
|
+
var REACT_MODULE = /^(react|react-dom|react-native)(\/|$)/;
|
|
16
|
+
function fileLanguageOf(path, sf) {
|
|
17
|
+
const script = /\.[cm]?jsx?$/.test(path) ? "js" : "ts";
|
|
18
|
+
const hasJsxExtension = /\.[jt]sx$/.test(path);
|
|
19
|
+
const mayHaveJsx = script === "js" && /<[A-Za-z>]/.test(sf.text);
|
|
20
|
+
const hasJsx = hasJsxExtension || mayHaveJsx && containsJsx(sf);
|
|
21
|
+
const isReact = hasJsx || importsReact(sf);
|
|
22
|
+
return { script, hasJsx, isReact, label: languageLabel(script, hasJsx, isReact) };
|
|
23
|
+
}
|
|
24
|
+
function languageLabel(script, hasJsx, isReact) {
|
|
25
|
+
if (hasJsx) return script === "js" ? "React JSX" : "React TSX";
|
|
26
|
+
if (isReact) return script === "js" ? "React JS" : "React TS";
|
|
27
|
+
return script === "js" ? "JS" : "TS";
|
|
28
|
+
}
|
|
29
|
+
function isJsx(node) {
|
|
30
|
+
return ts2.isJsxElement(node) || ts2.isJsxSelfClosingElement(node) || ts2.isJsxFragment(node);
|
|
31
|
+
}
|
|
32
|
+
function containsJsx(node) {
|
|
33
|
+
return isJsx(node) || !!ts2.forEachChild(node, (child) => containsJsx(child) || void 0);
|
|
34
|
+
}
|
|
35
|
+
function importsReact(sf) {
|
|
36
|
+
return sf.statements.some((statement) => {
|
|
37
|
+
const isImportOrReExport = ts2.isImportDeclaration(statement) || ts2.isExportDeclaration(statement);
|
|
38
|
+
if (isImportOrReExport) return !!statement.moduleSpecifier && ts2.isStringLiteral(statement.moduleSpecifier) && REACT_MODULE.test(statement.moduleSpecifier.text);
|
|
39
|
+
if (ts2.isImportEqualsDeclaration(statement)) return ts2.isExternalModuleReference(statement.moduleReference) && isReactModuleName(statement.moduleReference.expression);
|
|
40
|
+
if (ts2.isVariableStatement(statement)) return statement.declarationList.declarations.some((declaration) => !!declaration.initializer && isRequireOfReact(declaration.initializer));
|
|
41
|
+
return false;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isRequireOfReact(expression) {
|
|
45
|
+
if (ts2.isPropertyAccessExpression(expression)) return isRequireOfReact(expression.expression);
|
|
46
|
+
const isRequireCall = ts2.isCallExpression(expression) && ts2.isIdentifier(expression.expression) && expression.expression.text === "require";
|
|
47
|
+
return isRequireCall && expression.arguments.length === 1 && isReactModuleName(expression.arguments[0]);
|
|
48
|
+
}
|
|
49
|
+
function isReactModuleName(expression) {
|
|
50
|
+
return ts2.isStringLiteral(expression) && REACT_MODULE.test(expression.text);
|
|
51
|
+
}
|
|
52
|
+
var COMPONENT_WRAPPERS = /* @__PURE__ */ new Set(["memo", "forwardRef", "React.memo", "React.forwardRef"]);
|
|
53
|
+
var COMPONENT_BASE_CLASSES = /* @__PURE__ */ new Set(["Component", "PureComponent", "React.Component", "React.PureComponent"]);
|
|
54
|
+
var isComponentName = (name) => /^[A-Z]/.test(name);
|
|
55
|
+
function returnsJsx(fn) {
|
|
56
|
+
if (!fn.body) return false;
|
|
57
|
+
if (!ts2.isBlock(fn.body)) return isJsxValue(fn.body);
|
|
58
|
+
const hasJsxReturn = (node) => {
|
|
59
|
+
if (ts2.isReturnStatement(node)) return !!node.expression && isJsxValue(node.expression);
|
|
60
|
+
if (ts2.isFunctionLike(node) || ts2.isClassLike(node)) return false;
|
|
61
|
+
return !!ts2.forEachChild(node, (child) => hasJsxReturn(child) || void 0);
|
|
62
|
+
};
|
|
63
|
+
return hasJsxReturn(fn.body);
|
|
64
|
+
}
|
|
65
|
+
function isJsxValue(expression) {
|
|
66
|
+
const value = unwrap(expression);
|
|
67
|
+
if (isJsx(value)) return true;
|
|
68
|
+
if (ts2.isConditionalExpression(value)) return isJsxValue(value.whenTrue) || isJsxValue(value.whenFalse);
|
|
69
|
+
if (ts2.isBinaryExpression(value)) return isJsxValue(value.left) || isJsxValue(value.right);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function componentFunctionOf(initializer) {
|
|
73
|
+
const value = unwrap(initializer);
|
|
74
|
+
if (ts2.isArrowFunction(value) || ts2.isFunctionExpression(value)) return returnsJsx(value) ? value : void 0;
|
|
75
|
+
const isWrapperCall = ts2.isCallExpression(value) && value.arguments.length > 0 && COMPONENT_WRAPPERS.has(value.expression.getText());
|
|
76
|
+
if (!isWrapperCall) return void 0;
|
|
77
|
+
const wrapped = unwrap(value.arguments[0]);
|
|
78
|
+
if (ts2.isArrowFunction(wrapped) || ts2.isFunctionExpression(wrapped)) return wrapped;
|
|
79
|
+
return componentFunctionOf(wrapped);
|
|
80
|
+
}
|
|
81
|
+
function isComponentClass(cls) {
|
|
82
|
+
const extendsClause = cls.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
|
|
83
|
+
const baseClass = extendsClause?.types[0]?.expression.getText();
|
|
84
|
+
return !!baseClass && COMPONENT_BASE_CLASSES.has(baseClass);
|
|
85
|
+
}
|
|
86
|
+
var STATE_HOOKS = /* @__PURE__ */ new Set(["useState", "useReducer", "React.useState", "React.useReducer"]);
|
|
87
|
+
function stateHookOf(declaration) {
|
|
88
|
+
const call = declaration.initializer && unwrap(declaration.initializer);
|
|
89
|
+
const isStateHookCall = !!call && ts2.isCallExpression(call) && STATE_HOOKS.has(call.expression.getText());
|
|
90
|
+
if (!isStateHookCall) return void 0;
|
|
91
|
+
if (ts2.isIdentifier(declaration.name)) return { call, value: declaration };
|
|
92
|
+
if (!ts2.isArrayBindingPattern(declaration.name)) return void 0;
|
|
93
|
+
const [value, setter] = declaration.name.elements.map((element) => ts2.isBindingElement(element) && ts2.isIdentifier(element.name) ? element : void 0);
|
|
94
|
+
return value || setter ? { call, value, setter } : void 0;
|
|
95
|
+
}
|
|
96
|
+
var isHookName = (name) => /^use[A-Z0-9]/.test(name);
|
|
97
|
+
|
|
98
|
+
// src/parse.ts
|
|
3
99
|
var PREVIEW_LINES = 6;
|
|
4
100
|
var CONTAINER_PREVIEW_LINES = 3;
|
|
5
101
|
var PREVIEW_WIDTH = 64;
|
|
@@ -10,16 +106,17 @@ function parseFile(path, text) {
|
|
|
10
106
|
return m;
|
|
11
107
|
}
|
|
12
108
|
function parseStructure(path, text, prev) {
|
|
13
|
-
const kind =
|
|
109
|
+
const kind = scriptKindOf(path);
|
|
14
110
|
const old = prev && prev.path === path ? internals.get(prev) : void 0;
|
|
15
111
|
let sf;
|
|
16
112
|
if (old && !old.reused) {
|
|
17
113
|
old.reused = true;
|
|
18
|
-
sf =
|
|
114
|
+
sf = ts3.updateSourceFile(old.sf, text, changeRange(prev.text, text));
|
|
19
115
|
} else {
|
|
20
|
-
sf =
|
|
116
|
+
sf = ts3.createSourceFile(path, text, ts3.ScriptTarget.Latest, true, kind);
|
|
21
117
|
}
|
|
22
|
-
const
|
|
118
|
+
const language = fileLanguageOf(path, sf);
|
|
119
|
+
const b = new Builder(sf, text, language.isReact);
|
|
23
120
|
b.collectStatements(sf.statements, null, 0);
|
|
24
121
|
const syntax = sf.parseDiagnostics ?? [];
|
|
25
122
|
const model = {
|
|
@@ -31,7 +128,8 @@ function parseStructure(path, text, prev) {
|
|
|
31
128
|
syntaxErrors: syntax.length,
|
|
32
129
|
firstErrorLine: -1,
|
|
33
130
|
diagnostics: [],
|
|
34
|
-
indentUnit: detectIndentUnit(text)
|
|
131
|
+
indentUnit: detectIndentUnit(text),
|
|
132
|
+
language
|
|
35
133
|
};
|
|
36
134
|
model.diagnostics = syntax.map((d) => toCodeDiagnostic(model, sf, d, "syntax"));
|
|
37
135
|
model.firstErrorLine = model.diagnostics[0]?.line ?? -1;
|
|
@@ -54,9 +152,9 @@ function toCodeDiagnostic(model, sf, d, source) {
|
|
|
54
152
|
start,
|
|
55
153
|
length: d.length ?? 0,
|
|
56
154
|
line: sf.getLineAndCharacterOfPosition(Math.min(start, sf.text.length)).line + 1,
|
|
57
|
-
message:
|
|
155
|
+
message: ts3.flattenDiagnosticMessageText(d.messageText, "\n"),
|
|
58
156
|
source,
|
|
59
|
-
severity: d.category ===
|
|
157
|
+
severity: d.category === ts3.DiagnosticCategory.Error ? "error" : "warning",
|
|
60
158
|
blockId: blockAt(model, start)?.id ?? null
|
|
61
159
|
};
|
|
62
160
|
}
|
|
@@ -77,15 +175,29 @@ function changeRange(a, b) {
|
|
|
77
175
|
while (s < n && a.charCodeAt(s) === b.charCodeAt(s)) s++;
|
|
78
176
|
let e = 0;
|
|
79
177
|
while (e < n - s && a.charCodeAt(a.length - 1 - e) === b.charCodeAt(b.length - 1 - e)) e++;
|
|
80
|
-
return
|
|
178
|
+
return ts3.createTextChangeRange(ts3.createTextSpan(s, a.length - e - s), b.length - e - s);
|
|
179
|
+
}
|
|
180
|
+
function commonJsExportedFunction(s) {
|
|
181
|
+
if (!ts3.isExpressionStatement(s) || !ts3.isBinaryExpression(s.expression) || s.expression.operatorToken.kind !== ts3.SyntaxKind.EqualsToken) return null;
|
|
182
|
+
const { left, right } = s.expression;
|
|
183
|
+
const fn = unwrap(right);
|
|
184
|
+
const isFunction = ts3.isFunctionExpression(fn) || ts3.isArrowFunction(fn);
|
|
185
|
+
if (!isFunction || !ts3.isPropertyAccessExpression(left) || !ts3.isIdentifier(left.name)) return null;
|
|
186
|
+
const target = left.expression.getText();
|
|
187
|
+
const isExportsObject = target === "exports" || target === "module.exports";
|
|
188
|
+
return isExportsObject ? { assignment: s.expression, exportedName: left.name, fn } : null;
|
|
189
|
+
}
|
|
190
|
+
function scriptKindOf(path) {
|
|
191
|
+
if (/\.[cm]?jsx?$/.test(path)) return path.endsWith("x") ? ts3.ScriptKind.JSX : ts3.ScriptKind.JS;
|
|
192
|
+
return path.endsWith("x") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS;
|
|
81
193
|
}
|
|
82
194
|
function createProgram(sf) {
|
|
83
195
|
const options = {
|
|
84
196
|
noLib: true,
|
|
85
197
|
noResolve: true,
|
|
86
198
|
allowJs: true,
|
|
87
|
-
target:
|
|
88
|
-
jsx:
|
|
199
|
+
target: ts3.ScriptTarget.Latest,
|
|
200
|
+
jsx: ts3.JsxEmit.Preserve,
|
|
89
201
|
types: []
|
|
90
202
|
};
|
|
91
203
|
const host = {
|
|
@@ -101,7 +213,7 @@ function createProgram(sf) {
|
|
|
101
213
|
readFile: () => void 0,
|
|
102
214
|
getDirectories: () => []
|
|
103
215
|
};
|
|
104
|
-
return
|
|
216
|
+
return ts3.createProgram({ rootNames: [sf.fileName], options, host });
|
|
105
217
|
}
|
|
106
218
|
function detectIndentUnit(text) {
|
|
107
219
|
const m = /\n(\t+|[ ]{2,8})\S/.exec(text);
|
|
@@ -109,12 +221,14 @@ function detectIndentUnit(text) {
|
|
|
109
221
|
return m[1].startsWith(" ") ? " " : m[1].length >= 4 && m[1].length % 4 === 0 ? " " : " ";
|
|
110
222
|
}
|
|
111
223
|
var Builder = class {
|
|
112
|
-
constructor(sf, text) {
|
|
224
|
+
constructor(sf, text, isReact) {
|
|
113
225
|
this.sf = sf;
|
|
114
226
|
this.text = text;
|
|
227
|
+
this.isReact = isReact;
|
|
115
228
|
}
|
|
116
229
|
sf;
|
|
117
230
|
text;
|
|
231
|
+
isReact;
|
|
118
232
|
roots = [];
|
|
119
233
|
byId = /* @__PURE__ */ new Map();
|
|
120
234
|
/**
|
|
@@ -126,6 +240,8 @@ var Builder = class {
|
|
|
126
240
|
rangeToBlock = /* @__PURE__ */ new Map();
|
|
127
241
|
/** Every name a usage could resolve to. Other identifiers skip the (slow) checker. */
|
|
128
242
|
names = /* @__PURE__ */ new Set();
|
|
243
|
+
/** Setters of React state (`setCount`), keyed like `declToBlock`: using one writes the state. */
|
|
244
|
+
stateSetters = /* @__PURE__ */ new Set();
|
|
129
245
|
// ---------- structure ----------
|
|
130
246
|
collectStatements(stmts, parent, depth, declarationsOnly = false) {
|
|
131
247
|
let group = [];
|
|
@@ -154,33 +270,40 @@ var Builder = class {
|
|
|
154
270
|
};
|
|
155
271
|
for (let i = 0; i < stmts.length; i++) {
|
|
156
272
|
const s = stmts[i];
|
|
157
|
-
if (
|
|
273
|
+
if (ts3.isFunctionDeclaration(s)) {
|
|
158
274
|
flushGroup();
|
|
159
275
|
const overloads = [s];
|
|
160
276
|
while (!overloads[overloads.length - 1].body) {
|
|
161
277
|
const next = stmts[i + 1];
|
|
162
|
-
if (next &&
|
|
278
|
+
if (next && ts3.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
|
|
163
279
|
overloads.push(next);
|
|
164
280
|
i++;
|
|
165
281
|
} else break;
|
|
166
282
|
}
|
|
167
|
-
|
|
168
|
-
|
|
283
|
+
const name = s.name?.text ?? "default";
|
|
284
|
+
const isComponent = this.isReact && isComponentName(name) && returnsJsx(overloads[overloads.length - 1]);
|
|
285
|
+
this.addFunctionLike(overloads, name, parent, depth, { kind: isComponent ? "component" : "function" });
|
|
286
|
+
} else if (ts3.isClassDeclaration(s)) {
|
|
169
287
|
flushGroup();
|
|
170
288
|
this.addClass(s, s, s.name?.text ?? "default", parent, depth);
|
|
171
|
-
} else if (
|
|
289
|
+
} else if (ts3.isVariableStatement(s)) {
|
|
172
290
|
flushGroup();
|
|
173
291
|
this.addVariableStatement(s, parent, depth, declarationsOnly);
|
|
292
|
+
} else if (commonJsExportedFunction(s)) {
|
|
293
|
+
flushGroup();
|
|
294
|
+
const { assignment, exportedName, fn } = commonJsExportedFunction(s);
|
|
295
|
+
const declNodes = [s, assignment, assignment.left];
|
|
296
|
+
this.addFunctionLike([fn], exportedName.text, parent, depth, { rangeNodes: [s], declNodes, extraTags: ["export"], nameNode: exportedName });
|
|
174
297
|
} else if (declarationsOnly) {
|
|
175
298
|
continue;
|
|
176
|
-
} else if (
|
|
299
|
+
} else if (ts3.isImportDeclaration(s) || ts3.isImportEqualsDeclaration(s)) {
|
|
177
300
|
flushGroup();
|
|
178
301
|
this.addImport(s, parent, depth);
|
|
179
|
-
} else if (
|
|
302
|
+
} else if (ts3.isInterfaceDeclaration(s) || ts3.isTypeAliasDeclaration(s) || ts3.isEnumDeclaration(s)) {
|
|
180
303
|
flushGroup();
|
|
181
|
-
const keyword =
|
|
304
|
+
const keyword = ts3.isInterfaceDeclaration(s) ? "interface" : ts3.isEnumDeclaration(s) ? "enum" : "type";
|
|
182
305
|
const typeParams = "typeParameters" in s && s.typeParameters ? `<${s.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
|
|
183
|
-
const bodyStart =
|
|
306
|
+
const bodyStart = ts3.isTypeAliasDeclaration(s) ? s.type.getStart(this.sf) : this.text.indexOf("{", s.name.end);
|
|
184
307
|
this.add(
|
|
185
308
|
{
|
|
186
309
|
kind: "type",
|
|
@@ -198,7 +321,7 @@ var Builder = class {
|
|
|
198
321
|
parent,
|
|
199
322
|
depth
|
|
200
323
|
);
|
|
201
|
-
} else if (
|
|
324
|
+
} else if (ts3.isModuleDeclaration(s) && s.body && ts3.isModuleBlock(s.body)) {
|
|
202
325
|
flushGroup();
|
|
203
326
|
const body = s.body;
|
|
204
327
|
this.add(
|
|
@@ -228,13 +351,13 @@ var Builder = class {
|
|
|
228
351
|
addImport(s, parent, depth) {
|
|
229
352
|
let spec;
|
|
230
353
|
let names = [];
|
|
231
|
-
if (
|
|
354
|
+
if (ts3.isImportDeclaration(s)) {
|
|
232
355
|
spec = s.moduleSpecifier.text;
|
|
233
356
|
const clause = s.importClause;
|
|
234
357
|
if (clause?.name) names.push(clause.name.text);
|
|
235
358
|
const nb = clause?.namedBindings;
|
|
236
|
-
if (nb &&
|
|
237
|
-
if (nb &&
|
|
359
|
+
if (nb && ts3.isNamespaceImport(nb)) names.push("* as " + nb.name.text);
|
|
360
|
+
if (nb && ts3.isNamedImports(nb)) names.push(...nb.elements.map((e) => e.name.text));
|
|
238
361
|
if (!clause) names = ["(side effect)"];
|
|
239
362
|
} else {
|
|
240
363
|
spec = s.name.text;
|
|
@@ -248,7 +371,7 @@ var Builder = class {
|
|
|
248
371
|
key: "import:" + spec,
|
|
249
372
|
name: spec,
|
|
250
373
|
detail: "",
|
|
251
|
-
tags:
|
|
374
|
+
tags: ts3.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
|
|
252
375
|
rangeNodes: [s],
|
|
253
376
|
declNodes: [s],
|
|
254
377
|
previewRange: null,
|
|
@@ -267,11 +390,11 @@ var Builder = class {
|
|
|
267
390
|
let previewRange = null;
|
|
268
391
|
let insert = null;
|
|
269
392
|
let insertAt = -1;
|
|
270
|
-
if (body &&
|
|
393
|
+
if (body && ts3.isBlock(body)) {
|
|
271
394
|
previewRange = [body.getStart(this.sf) + 1, body.end - 1];
|
|
272
395
|
insert = "body";
|
|
273
396
|
const last = body.statements[body.statements.length - 1];
|
|
274
|
-
insertAt = last &&
|
|
397
|
+
insertAt = last && ts3.isReturnStatement(last) ? this.lineStartOf(last.getStart(this.sf)) : body.end - 1;
|
|
275
398
|
} else if (body) {
|
|
276
399
|
previewRange = [body.getStart(this.sf), body.end];
|
|
277
400
|
}
|
|
@@ -279,22 +402,23 @@ var Builder = class {
|
|
|
279
402
|
const ret = sig.type ? ": " + sig.type.getText() : "";
|
|
280
403
|
const detail = squash(`${typeParams}(${sig.parameters.map((p) => p.getText()).join(", ")})${ret}`);
|
|
281
404
|
const tags = [...opts.extraTags ?? [], ...modifierTags(nodes[0])];
|
|
405
|
+
if (this.isReact && isHookName(name)) tags.push("hook");
|
|
282
406
|
if (nodes.length > 1) tags.push(`${nodes.length - 1} overload${nodes.length > 2 ? "s" : ""}`);
|
|
283
407
|
if (impl.asteriskToken) tags.push("generator");
|
|
284
408
|
this.add(
|
|
285
409
|
{
|
|
286
|
-
kind: "function",
|
|
410
|
+
kind: opts.kind ?? "function",
|
|
287
411
|
key: opts.key ?? name,
|
|
288
412
|
name,
|
|
289
413
|
detail,
|
|
290
414
|
tags,
|
|
291
415
|
rangeNodes: opts.rangeNodes ?? nodes,
|
|
292
416
|
declNodes: opts.declNodes ?? nodes,
|
|
293
|
-
nameNode: impl.name,
|
|
417
|
+
nameNode: opts.nameNode ?? impl.name,
|
|
294
418
|
previewRange,
|
|
295
419
|
insert,
|
|
296
420
|
insertAt,
|
|
297
|
-
children: body &&
|
|
421
|
+
children: body && ts3.isBlock(body) ? (blk) => this.collectStatements(body.statements, blk, depth + 1, true) : void 0
|
|
298
422
|
},
|
|
299
423
|
parent,
|
|
300
424
|
depth
|
|
@@ -306,7 +430,7 @@ var Builder = class {
|
|
|
306
430
|
const typeParams = cls.typeParameters?.length ? `<${cls.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
|
|
307
431
|
this.add(
|
|
308
432
|
{
|
|
309
|
-
kind: "class",
|
|
433
|
+
kind: this.isReact && isComponentClass(cls) ? "component" : "class",
|
|
310
434
|
key: name,
|
|
311
435
|
name,
|
|
312
436
|
detail: squash(typeParams + (heritage ? " " + heritage : "")),
|
|
@@ -326,27 +450,27 @@ var Builder = class {
|
|
|
326
450
|
collectMembers(members, parent, depth) {
|
|
327
451
|
for (let i = 0; i < members.length; i++) {
|
|
328
452
|
const m = members[i];
|
|
329
|
-
if (
|
|
330
|
-
const name =
|
|
453
|
+
if (ts3.isMethodDeclaration(m) || ts3.isConstructorDeclaration(m)) {
|
|
454
|
+
const name = ts3.isConstructorDeclaration(m) ? "constructor" : m.name.getText();
|
|
331
455
|
const overloads = [m];
|
|
332
456
|
while (!overloads[overloads.length - 1].body) {
|
|
333
457
|
const next = members[i + 1];
|
|
334
|
-
const nextName = next && (
|
|
458
|
+
const nextName = next && (ts3.isConstructorDeclaration(next) ? "constructor" : ts3.isMethodDeclaration(next) ? next.name.getText() : null);
|
|
335
459
|
if (nextName === name) {
|
|
336
460
|
overloads.push(next);
|
|
337
461
|
i++;
|
|
338
462
|
} else break;
|
|
339
463
|
}
|
|
340
464
|
this.addFunctionLike(overloads, name, parent, depth);
|
|
341
|
-
} else if (
|
|
465
|
+
} else if (ts3.isGetAccessor(m) || ts3.isSetAccessor(m)) {
|
|
342
466
|
const name = m.name.getText();
|
|
343
|
-
const word =
|
|
467
|
+
const word = ts3.isGetAccessor(m) ? "get" : "set";
|
|
344
468
|
this.addFunctionLike([m], name, parent, depth, { key: `${word} ${name}`, extraTags: [word] });
|
|
345
|
-
} else if (
|
|
469
|
+
} else if (ts3.isPropertyDeclaration(m)) {
|
|
346
470
|
const name = m.name.getText();
|
|
347
471
|
const init = m.initializer && unwrap(m.initializer);
|
|
348
|
-
if (init && (
|
|
349
|
-
this.addFunctionLike([init], name, parent, depth, { rangeNodes: [m], declNodes: [m], extraTags: modifierTags(m) });
|
|
472
|
+
if (init && (ts3.isArrowFunction(init) || ts3.isFunctionExpression(init))) {
|
|
473
|
+
this.addFunctionLike([init], name, parent, depth, { rangeNodes: [m], declNodes: [m], extraTags: modifierTags(m), nameNode: m.name });
|
|
350
474
|
} else {
|
|
351
475
|
this.addVariable(m, m, name, m.type ? ": " + m.type.getText() : "", modifierTags(m), parent, depth, m.initializer);
|
|
352
476
|
}
|
|
@@ -355,25 +479,61 @@ var Builder = class {
|
|
|
355
479
|
}
|
|
356
480
|
addVariableStatement(s, parent, depth, functionsOnly) {
|
|
357
481
|
const decls = s.declarationList.declarations;
|
|
358
|
-
const keyword = s.declarationList.flags &
|
|
482
|
+
const keyword = s.declarationList.flags & ts3.NodeFlags.Const ? "const" : s.declarationList.flags & ts3.NodeFlags.Let ? "let" : "var";
|
|
359
483
|
for (const d of decls) {
|
|
360
484
|
const rangeNode = decls.length === 1 ? s : d;
|
|
361
485
|
const name = d.name.getText();
|
|
362
486
|
const tags = [...modifierTags(s), keyword];
|
|
487
|
+
const stateHook = this.isReact ? stateHookOf(d) : void 0;
|
|
488
|
+
if (stateHook) {
|
|
489
|
+
this.addState(stateHook, rangeNode, tags, parent, depth);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
363
492
|
const init = d.initializer && unwrap(d.initializer);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
493
|
+
const componentFunction = this.isReact && init && isComponentName(name) ? componentFunctionOf(init) : void 0;
|
|
494
|
+
if (componentFunction) {
|
|
495
|
+
this.addFunctionLike([componentFunction], name, parent, depth, { kind: "component", rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
|
|
496
|
+
} else if (init && (ts3.isArrowFunction(init) || ts3.isFunctionExpression(init))) {
|
|
497
|
+
this.addFunctionLike([init], name, parent, depth, { rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
|
|
498
|
+
} else if (init && ts3.isClassExpression(init)) {
|
|
367
499
|
this.addClass(init, rangeNode, name, parent, depth, [d, init], tags);
|
|
368
500
|
} else if (!functionsOnly) {
|
|
369
501
|
this.addVariable(d, rangeNode, name, d.type ? ": " + d.type.getText() : "", tags, parent, depth, d.initializer);
|
|
370
502
|
}
|
|
371
503
|
}
|
|
372
504
|
}
|
|
505
|
+
/** `const [count, setCount] = useState(0)`: a state block named `count`; using `setCount` writes it. */
|
|
506
|
+
addState(hook, rangeNode, tags, parent, depth) {
|
|
507
|
+
const named = hook.value ?? hook.setter;
|
|
508
|
+
const setterName = hook.setter?.name.getText();
|
|
509
|
+
const [firstArg, ...otherArgs] = hook.call.arguments;
|
|
510
|
+
const args = firstArg ? truncate(squash(firstArg.getText()), 24) + (otherArgs.length ? ", \u2026" : "") : "";
|
|
511
|
+
if (hook.setter && setterName) {
|
|
512
|
+
this.stateSetters.add(nodeKey(hook.setter));
|
|
513
|
+
this.names.add(setterName);
|
|
514
|
+
}
|
|
515
|
+
this.add(
|
|
516
|
+
{
|
|
517
|
+
kind: "state",
|
|
518
|
+
key: named.name.getText(),
|
|
519
|
+
name: named.name.getText(),
|
|
520
|
+
detail: `${hook.call.expression.getText()}(${args})`,
|
|
521
|
+
tags: setterName && hook.value ? [...tags, setterName] : tags,
|
|
522
|
+
rangeNodes: [rangeNode],
|
|
523
|
+
declNodes: [hook.value, hook.setter].filter((declaration) => !!declaration),
|
|
524
|
+
nameNode: named.name,
|
|
525
|
+
previewRange: [hook.call.getStart(this.sf), hook.call.end],
|
|
526
|
+
insert: null,
|
|
527
|
+
insertAt: -1
|
|
528
|
+
},
|
|
529
|
+
parent,
|
|
530
|
+
depth
|
|
531
|
+
);
|
|
532
|
+
}
|
|
373
533
|
addVariable(decl, rangeNode, name, type, tags, parent, depth, initializer) {
|
|
374
534
|
const init = initializer && unwrap(initializer);
|
|
375
|
-
const objectChildren = init &&
|
|
376
|
-
(p) =>
|
|
535
|
+
const objectChildren = init && ts3.isObjectLiteralExpression(init) ? init.properties.filter(
|
|
536
|
+
(p) => ts3.isMethodDeclaration(p) || ts3.isPropertyAssignment(p) && (ts3.isArrowFunction(unwrap(p.initializer)) || ts3.isFunctionExpression(unwrap(p.initializer)))
|
|
377
537
|
) : [];
|
|
378
538
|
this.add(
|
|
379
539
|
{
|
|
@@ -391,10 +551,10 @@ var Builder = class {
|
|
|
391
551
|
children: objectChildren.length ? (blk) => {
|
|
392
552
|
for (const p of objectChildren) {
|
|
393
553
|
const pname = p.name.getText();
|
|
394
|
-
if (
|
|
554
|
+
if (ts3.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
|
|
395
555
|
else {
|
|
396
556
|
const fn = unwrap(p.initializer);
|
|
397
|
-
this.addFunctionLike([fn], pname, blk, depth + 1, { rangeNodes: [p], declNodes: [p] });
|
|
557
|
+
this.addFunctionLike([fn], pname, blk, depth + 1, { rangeNodes: [p], declNodes: [p], nameNode: p.name });
|
|
398
558
|
}
|
|
399
559
|
}
|
|
400
560
|
} : void 0
|
|
@@ -453,7 +613,7 @@ var Builder = class {
|
|
|
453
613
|
/** Include doc comments directly above the node (no blank line in between). */
|
|
454
614
|
startWithComments(node) {
|
|
455
615
|
let start = node.getStart(this.sf);
|
|
456
|
-
const comments =
|
|
616
|
+
const comments = ts3.getLeadingCommentRanges(this.text, node.pos) ?? [];
|
|
457
617
|
for (let i = comments.length - 1; i >= 0; i--) {
|
|
458
618
|
const gap = this.text.slice(comments[i].end, start);
|
|
459
619
|
if (/\S/.test(gap) || (gap.match(/\n/g)?.length ?? 0) > 1) break;
|
|
@@ -481,8 +641,8 @@ var Builder = class {
|
|
|
481
641
|
const visit = (node, current) => {
|
|
482
642
|
const blk = this.rangeToBlock.get(nodeKey(node));
|
|
483
643
|
if (blk) current = blk;
|
|
484
|
-
if (
|
|
485
|
-
|
|
644
|
+
if (ts3.isIdentifier(node) && current) this.recordUsage(node, current, checker, byPair, otherFiles);
|
|
645
|
+
ts3.forEachChild(node, (c) => visit(c, current));
|
|
486
646
|
};
|
|
487
647
|
visit(sf, null);
|
|
488
648
|
return [...byPair.values()];
|
|
@@ -491,29 +651,30 @@ var Builder = class {
|
|
|
491
651
|
const isKnownName = this.names.has(id.text) || !!otherFiles?.projectNames.has(id.text);
|
|
492
652
|
if (!isKnownName) return;
|
|
493
653
|
const parent = id.parent;
|
|
494
|
-
const isDeclarationName = !!parent && parent.name === id && !
|
|
654
|
+
const isDeclarationName = !!parent && parent.name === id && !ts3.isPropertyAccessExpression(parent) && !ts3.isShorthandPropertyAssignment(parent);
|
|
495
655
|
if (isDeclarationName) return;
|
|
496
656
|
let sym;
|
|
497
657
|
try {
|
|
498
|
-
sym = parent &&
|
|
658
|
+
sym = parent && ts3.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
|
|
499
659
|
} catch {
|
|
500
660
|
return;
|
|
501
661
|
}
|
|
502
662
|
if (!sym?.declarations) return;
|
|
503
|
-
|
|
663
|
+
let kind = usageKind(id);
|
|
504
664
|
let to;
|
|
505
665
|
let importBlockId;
|
|
506
666
|
for (const d of sym.declarations) {
|
|
507
667
|
to = this.declToBlock.get(nodeKey(d));
|
|
668
|
+
if (this.stateSetters.has(nodeKey(d))) kind = "write";
|
|
508
669
|
if (!to) {
|
|
509
670
|
let n = d;
|
|
510
|
-
while (n && !
|
|
511
|
-
if (n && !
|
|
671
|
+
while (n && !ts3.isImportDeclaration(n) && !ts3.isImportEqualsDeclaration(n) && !ts3.isSourceFile(n)) n = n.parent;
|
|
672
|
+
if (n && !ts3.isSourceFile(n)) importBlockId = to = this.declToBlock.get(nodeKey(n));
|
|
512
673
|
}
|
|
513
674
|
if (to) break;
|
|
514
675
|
}
|
|
515
676
|
if (otherFiles) {
|
|
516
|
-
const isImportedName = !!importBlockId && (sym.flags &
|
|
677
|
+
const isImportedName = !!importBlockId && (sym.flags & ts3.SymbolFlags.Alias) !== 0;
|
|
517
678
|
const target = isImportedName ? safeAliasedSymbol(checker, sym) : to ? void 0 : sym;
|
|
518
679
|
const declaredElsewhere = target?.declarations?.find((d) => d.getSourceFile() !== id.getSourceFile());
|
|
519
680
|
const external = declaredElsewhere && otherFiles.blockInOtherFile(declaredElsewhere);
|
|
@@ -530,6 +691,7 @@ function addUsage(out, usage) {
|
|
|
530
691
|
if (!existing) return void out.set(pairKey, usage);
|
|
531
692
|
existing.count++;
|
|
532
693
|
if (usage.kind === "call") existing.kind = "call";
|
|
694
|
+
else if (usage.kind === "write" && existing.kind === "read") existing.kind = "write";
|
|
533
695
|
}
|
|
534
696
|
function safeAliasedSymbol(checker, symbol) {
|
|
535
697
|
try {
|
|
@@ -542,23 +704,26 @@ function nodeKey(node) {
|
|
|
542
704
|
return `${node.kind}:${node.pos}:${node.end}`;
|
|
543
705
|
}
|
|
544
706
|
function usageKind(id) {
|
|
707
|
+
if (isJsxTagName(id)) return "render";
|
|
545
708
|
let n = id;
|
|
546
|
-
if (
|
|
709
|
+
if (ts3.isPropertyAccessExpression(n.parent) && n.parent.name === n) n = n.parent;
|
|
547
710
|
const p = n.parent;
|
|
548
|
-
if ((
|
|
549
|
-
if (
|
|
550
|
-
if (
|
|
551
|
-
if (
|
|
552
|
-
if ((
|
|
553
|
-
if (
|
|
711
|
+
if ((ts3.isCallExpression(p) || ts3.isNewExpression(p)) && p.expression === n) return "call";
|
|
712
|
+
if (ts3.isTaggedTemplateExpression(p) && p.tag === n) return "call";
|
|
713
|
+
if (ts3.isJsxOpeningLikeElement(p) && p.tagName === n) return "render";
|
|
714
|
+
if (ts3.isBinaryExpression(p) && p.left === n && p.operatorToken.kind >= ts3.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts3.SyntaxKind.LastAssignment) return "write";
|
|
715
|
+
if ((ts3.isPrefixUnaryExpression(p) || ts3.isPostfixUnaryExpression(p)) && (p.operator === ts3.SyntaxKind.PlusPlusToken || p.operator === ts3.SyntaxKind.MinusMinusToken)) return "write";
|
|
716
|
+
if (ts3.isTypeReferenceNode(p) || ts3.isExpressionWithTypeArguments(p) || ts3.isTypeQueryNode(p) || ts3.isQualifiedName(p)) return "type";
|
|
554
717
|
return "read";
|
|
555
718
|
}
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
|
|
719
|
+
function isJsxTagName(id) {
|
|
720
|
+
let tag = id;
|
|
721
|
+
while (ts3.isPropertyAccessExpression(tag.parent) && tag.parent.expression === tag) tag = tag.parent;
|
|
722
|
+
const element = tag.parent;
|
|
723
|
+
return (ts3.isJsxOpeningLikeElement(element) || ts3.isJsxClosingElement(element)) && element.tagName === tag;
|
|
559
724
|
}
|
|
560
725
|
function modifierTags(n) {
|
|
561
|
-
const mods =
|
|
726
|
+
const mods = ts3.canHaveModifiers(n) ? ts3.getModifiers(n) ?? [] : [];
|
|
562
727
|
return mods.map((m) => m.getText());
|
|
563
728
|
}
|
|
564
729
|
function squash(s) {
|
|
@@ -606,9 +771,106 @@ function makePreview(text, range, children, max) {
|
|
|
606
771
|
}
|
|
607
772
|
|
|
608
773
|
// src/project.ts
|
|
609
|
-
import
|
|
774
|
+
import ts5 from "typescript";
|
|
775
|
+
|
|
776
|
+
// src/workspace.ts
|
|
777
|
+
import ts4 from "typescript";
|
|
778
|
+
function parseConfig(config, projectPaths) {
|
|
779
|
+
const configFileName = "/" + config.path;
|
|
780
|
+
const json = ts4.parseConfigFileTextToJson(configFileName, config.text);
|
|
781
|
+
if (json.error || !json.config) return null;
|
|
782
|
+
const readConfig = (name) => name === configFileName ? config.text : config.extendedFiles?.[name.slice(1)];
|
|
783
|
+
const host = {
|
|
784
|
+
useCaseSensitiveFileNames: true,
|
|
785
|
+
readDirectory: (rootDir, extensions, excludes, includes) => matchingPaths(projectPaths, rootDir, extensions, excludes ?? [], includes ?? ["**/*"]),
|
|
786
|
+
fileExists: (name) => readConfig(name) !== void 0,
|
|
787
|
+
readFile: readConfig
|
|
788
|
+
};
|
|
789
|
+
const folder = config.path.slice(0, config.path.lastIndexOf("/") + 1);
|
|
790
|
+
const parsed = ts4.parseJsonConfigFileContent(json.config, host, "/" + folder, void 0, configFileName);
|
|
791
|
+
const included = new Set(parsed.fileNames.map((name) => name.slice(1)));
|
|
792
|
+
return { path: config.path, folder, options: parsed.options, includes: (projectPath) => included.has(projectPath) };
|
|
793
|
+
}
|
|
794
|
+
function configForFile(projectPath, configs) {
|
|
795
|
+
const above = configs.filter((config) => projectPath.startsWith(config.folder));
|
|
796
|
+
const nearestFirst = [...above].sort((a, b) => b.folder.length - a.folder.length);
|
|
797
|
+
return nearestFirst.find((config) => config.includes(projectPath)) ?? nearestFirst[0] ?? null;
|
|
798
|
+
}
|
|
799
|
+
function workspacePackagePaths(packageFiles, hasFile) {
|
|
800
|
+
const paths = {};
|
|
801
|
+
for (const [packagePath, text] of Object.entries(packageFiles)) {
|
|
802
|
+
const manifest = parsePackageJson(text);
|
|
803
|
+
if (!manifest?.name) continue;
|
|
804
|
+
const folder = packagePath.slice(0, packagePath.lastIndexOf("/") + 1);
|
|
805
|
+
const entry = packageEntries(manifest).flatMap((named) => [named, ...sourceOfBuildOutput(named)]).concat(["src/index.ts", "src/index.tsx", "index.ts", "index.tsx"]).map((relative) => folder + relative.replace(/^\.\//, "")).find(hasFile);
|
|
806
|
+
if (entry) paths[manifest.name] = ["/" + entry];
|
|
807
|
+
paths[manifest.name + "/*"] = ["/" + folder + "src/*", "/" + folder + "*"];
|
|
808
|
+
}
|
|
809
|
+
return paths;
|
|
810
|
+
}
|
|
811
|
+
function parsePackageJson(text) {
|
|
812
|
+
try {
|
|
813
|
+
const value = JSON.parse(text);
|
|
814
|
+
return value && typeof value === "object" ? value : null;
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function packageEntries(manifest) {
|
|
820
|
+
const rootExport = manifest.exports && typeof manifest.exports === "object" && "." in manifest.exports ? manifest.exports["."] : manifest.exports;
|
|
821
|
+
return [manifest.types, manifest.typings, ...exportTargets(rootExport), manifest.module, manifest.main].filter((entry) => typeof entry === "string");
|
|
822
|
+
}
|
|
823
|
+
function exportTargets(target) {
|
|
824
|
+
if (typeof target === "string") return [target];
|
|
825
|
+
if (Array.isArray(target)) return target.flatMap(exportTargets);
|
|
826
|
+
if (target && typeof target === "object") return Object.values(target).flatMap(exportTargets);
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
function sourceOfBuildOutput(entry) {
|
|
830
|
+
const isBuildOutput = /\.(d\.ts|d\.mts|js|mjs|cjs|jsx)$/.test(entry);
|
|
831
|
+
if (!isBuildOutput) return [];
|
|
832
|
+
const withoutExtension = entry.replace(/\.(d\.ts|d\.mts|js|mjs|cjs|jsx)$/, "");
|
|
833
|
+
const inSourceFolder = withoutExtension.replace(/(^|\/)(dist|lib|build|out|esm|cjs)\//, "$1src/");
|
|
834
|
+
return [inSourceFolder, withoutExtension].flatMap((base) => [base + ".ts", base + ".tsx"]);
|
|
835
|
+
}
|
|
836
|
+
function matchingPaths(projectPaths, rootDir, extensions, excludes, includes) {
|
|
837
|
+
const base = rootDir.replace(/^\//, "").replace(/\/?$/, "/").replace(/^\/$/, "");
|
|
838
|
+
const includePatterns = includes.map((spec) => configPattern(base, spec, "files"));
|
|
839
|
+
const excludePatterns = excludes.map((spec) => configPattern(base, spec, "filesAndFolders"));
|
|
840
|
+
return projectPaths.filter((path) => path.startsWith(base) && extensions.some((extension) => path.endsWith(extension))).filter((path) => includePatterns.some((pattern) => pattern.test(path)) && !excludePatterns.some((pattern) => pattern.test(path))).map((path) => "/" + path);
|
|
841
|
+
}
|
|
842
|
+
function configPattern(base, spec, matching) {
|
|
843
|
+
const absolute = spec.startsWith("/") ? spec.slice(1) : normalizePath(base + spec.replace(/^\.\//, ""));
|
|
844
|
+
const lastPart = absolute.slice(absolute.lastIndexOf("/") + 1);
|
|
845
|
+
const isFolder = !/[*?]/.test(lastPart) && !/\.[^.]+$/.test(lastPart);
|
|
846
|
+
const pattern = isFolder ? absolute.replace(/\/?$/, "/**/*") : absolute;
|
|
847
|
+
let source = "";
|
|
848
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
849
|
+
const char = pattern[index];
|
|
850
|
+
if (pattern.startsWith("**/", index)) {
|
|
851
|
+
source += "(?:[^/]+/)*";
|
|
852
|
+
index += 2;
|
|
853
|
+
} else if (char === "*") source += "[^/]*";
|
|
854
|
+
else if (char === "?") source += "[^/]";
|
|
855
|
+
else source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
856
|
+
}
|
|
857
|
+
return new RegExp(`^${source}${matching === "filesAndFolders" ? "(?:/.*)?" : ""}$`);
|
|
858
|
+
}
|
|
859
|
+
function normalizePath(path) {
|
|
860
|
+
const parts = [];
|
|
861
|
+
for (const part of path.split("/")) {
|
|
862
|
+
if (part === "..") parts.pop();
|
|
863
|
+
else if (part !== "." && part !== "") parts.push(part);
|
|
864
|
+
}
|
|
865
|
+
return parts.join("/");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/project.ts
|
|
610
869
|
var LIB_DIR = "/__lib__/";
|
|
611
870
|
var DEFAULT_LIB = ["lib.es2022.d.ts", "lib.dom.d.ts"];
|
|
871
|
+
var NO_CONFIG = "";
|
|
872
|
+
var REQUIRE_CALL = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/;
|
|
873
|
+
var RE_EXPORT = /^export\s+(?:type\s+)?(?:\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s*from\s*(['"])([^'"]+)\1/;
|
|
612
874
|
var Project = class {
|
|
613
875
|
constructor(files = {}, projectOptions = {}) {
|
|
614
876
|
this.projectOptions = projectOptions;
|
|
@@ -620,11 +882,14 @@ var Project = class {
|
|
|
620
882
|
projectVersion = 0;
|
|
621
883
|
libFiles = {};
|
|
622
884
|
hasLib = false;
|
|
623
|
-
|
|
624
|
-
service;
|
|
885
|
+
libOptions = {};
|
|
625
886
|
projectNamesCache = null;
|
|
626
|
-
|
|
627
|
-
|
|
887
|
+
workspace = null;
|
|
888
|
+
configPrograms = null;
|
|
889
|
+
/** Language services by config path, created when a file of that config is first resolved. */
|
|
890
|
+
services = /* @__PURE__ */ new Map();
|
|
891
|
+
/** Shared by the services, so a file several programs include is parsed once. */
|
|
892
|
+
documentRegistry = ts5.createDocumentRegistry(true, "/");
|
|
628
893
|
/** Changes when files are added or removed, or the settings change: where imports point may change then. */
|
|
629
894
|
fileSetVersion = 0;
|
|
630
895
|
/** Load the standard library later, e.g. once it has been downloaded. Type errors show from then on. */
|
|
@@ -634,11 +899,16 @@ var Project = class {
|
|
|
634
899
|
}
|
|
635
900
|
/** Use the folder's tsconfig.json (or none, with null). */
|
|
636
901
|
setConfigFile(config) {
|
|
637
|
-
this.
|
|
902
|
+
this.setWorkspace(config ? { configFiles: [config] } : null);
|
|
903
|
+
}
|
|
904
|
+
/** Use the folder's config files and workspace packages (or none, with null). */
|
|
905
|
+
setWorkspace(workspace) {
|
|
906
|
+
this.workspace = workspace;
|
|
638
907
|
this.reconfigure();
|
|
639
908
|
}
|
|
640
909
|
reconfigure() {
|
|
641
|
-
this.service.dispose();
|
|
910
|
+
for (const service of this.services.values()) service.dispose();
|
|
911
|
+
this.services.clear();
|
|
642
912
|
this.configure(this.projectOptions);
|
|
643
913
|
this.projectVersion++;
|
|
644
914
|
this.fileSetVersion++;
|
|
@@ -647,23 +917,66 @@ var Project = class {
|
|
|
647
917
|
this.libFiles = options.libFiles ?? {};
|
|
648
918
|
const libEntries = (options.lib ?? DEFAULT_LIB).filter((name) => name in this.libFiles);
|
|
649
919
|
this.hasLib = libEntries.length > 0;
|
|
650
|
-
this.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
920
|
+
this.libOptions = this.hasLib ? { lib: libEntries, noLib: false } : { noLib: true };
|
|
921
|
+
}
|
|
922
|
+
/** Options for a program: the defaults, then the config's, then `ProjectOptions.compilerOptions`. */
|
|
923
|
+
compilerOptions(configOptions, packagePaths) {
|
|
924
|
+
const paths = { ...packagePaths, ...configOptions.paths };
|
|
925
|
+
return {
|
|
926
|
+
target: ts5.ScriptTarget.ES2022,
|
|
927
|
+
module: ts5.ModuleKind.ESNext,
|
|
928
|
+
moduleResolution: ts5.ModuleResolutionKind.Bundler,
|
|
654
929
|
strict: true,
|
|
655
|
-
|
|
656
|
-
jsx: ts2.JsxEmit.Preserve,
|
|
930
|
+
jsx: ts5.JsxEmit.Preserve,
|
|
657
931
|
skipLibCheck: true,
|
|
658
932
|
allowImportingTsExtensions: true,
|
|
659
|
-
...
|
|
660
|
-
...
|
|
933
|
+
...configOptions,
|
|
934
|
+
...Object.keys(paths).length ? { paths } : {},
|
|
935
|
+
...this.projectOptions.compilerOptions,
|
|
936
|
+
// JavaScript files are part of the project even where a tsconfig.json leaves `allowJs` off.
|
|
937
|
+
allowJs: true,
|
|
661
938
|
// The project loads no @types packages, its standard library is the one given here, and it never emits.
|
|
662
939
|
types: [],
|
|
663
940
|
noEmit: true,
|
|
664
|
-
...this.
|
|
941
|
+
...this.libOptions
|
|
665
942
|
};
|
|
666
|
-
|
|
943
|
+
}
|
|
944
|
+
/** Which config each file uses, and the options of each. Worked out again when files are added or removed. */
|
|
945
|
+
programs() {
|
|
946
|
+
if (this.configPrograms?.fileSetVersion === this.fileSetVersion) return this.configPrograms;
|
|
947
|
+
const projectPaths = [...this.files.keys()];
|
|
948
|
+
const packagePaths = workspacePackagePaths(this.workspace?.packageFiles ?? {}, (path) => this.files.has(path));
|
|
949
|
+
const configs = (this.workspace?.configFiles ?? []).map((config) => parseConfig(config, projectPaths)).filter((config) => !!config);
|
|
950
|
+
const optionsByConfig = /* @__PURE__ */ new Map([[NO_CONFIG, this.compilerOptions({}, packagePaths)]]);
|
|
951
|
+
for (const config of configs) optionsByConfig.set(config.path, this.compilerOptions(config.options, packagePaths));
|
|
952
|
+
const pathsByConfig = /* @__PURE__ */ new Map();
|
|
953
|
+
const configOfPath = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const path of projectPaths) {
|
|
955
|
+
const configPath = configForFile(path, configs)?.path ?? NO_CONFIG;
|
|
956
|
+
configOfPath.set(path, configPath);
|
|
957
|
+
pathsByConfig.set(configPath, [...pathsByConfig.get(configPath) ?? [], path]);
|
|
958
|
+
}
|
|
959
|
+
this.configPrograms = { fileSetVersion: this.fileSetVersion, optionsByConfig, pathsByConfig, configOfPath };
|
|
960
|
+
return this.configPrograms;
|
|
961
|
+
}
|
|
962
|
+
configOf(path) {
|
|
963
|
+
return this.programs().configOfPath.get(path) ?? NO_CONFIG;
|
|
964
|
+
}
|
|
965
|
+
/** The options a file's imports resolve with: its own config's, whichever program includes it. */
|
|
966
|
+
optionsOfFile(path) {
|
|
967
|
+
const { optionsByConfig } = this.programs();
|
|
968
|
+
return optionsByConfig.get(path ? this.configOf(path) : NO_CONFIG) ?? optionsByConfig.get(NO_CONFIG);
|
|
969
|
+
}
|
|
970
|
+
serviceOf(path) {
|
|
971
|
+
return this.serviceOfConfig(this.configOf(path));
|
|
972
|
+
}
|
|
973
|
+
serviceOfConfig(configPath) {
|
|
974
|
+
let service = this.services.get(configPath);
|
|
975
|
+
if (!service) {
|
|
976
|
+
service = ts5.createLanguageService(this.createHost(configPath), this.documentRegistry);
|
|
977
|
+
this.services.set(configPath, service);
|
|
978
|
+
}
|
|
979
|
+
return service;
|
|
667
980
|
}
|
|
668
981
|
// ---------------------------------------------------------------- files (fast path)
|
|
669
982
|
/** Add or update a file. Parses only this file, incrementally from its previous version. */
|
|
@@ -702,7 +1015,7 @@ var Project = class {
|
|
|
702
1015
|
const file = this.files.get(path);
|
|
703
1016
|
if (!file) return [];
|
|
704
1017
|
if (file.usages && file.usagesProjectVersion === this.projectVersion) return file.usages;
|
|
705
|
-
const program = this.
|
|
1018
|
+
const program = this.serviceOf(path).getProgram();
|
|
706
1019
|
const sf = program?.getSourceFile(toFileName(path));
|
|
707
1020
|
const builder = builderOf(file.model);
|
|
708
1021
|
if (!program || !sf || !builder || sf.text !== file.text) return [];
|
|
@@ -713,7 +1026,7 @@ var Project = class {
|
|
|
713
1026
|
/** Usages in other files that point at blocks in this file. */
|
|
714
1027
|
usagesInto(path) {
|
|
715
1028
|
const incoming = [];
|
|
716
|
-
for (const otherPath of this.
|
|
1029
|
+
for (const otherPath of this.importersThroughReExports(path)) {
|
|
717
1030
|
for (const usage of this.usages(otherPath)) {
|
|
718
1031
|
if (usage.toPath === path) incoming.push({ ...usage, fromPath: otherPath });
|
|
719
1032
|
}
|
|
@@ -726,9 +1039,10 @@ var Project = class {
|
|
|
726
1039
|
if (!file) return [];
|
|
727
1040
|
const hasSyntaxErrors = file.model.diagnostics.length > 0;
|
|
728
1041
|
if (hasSyntaxErrors || !this.hasLib) return file.model.diagnostics;
|
|
729
|
-
const
|
|
1042
|
+
const service = this.serviceOf(path);
|
|
1043
|
+
const sf = service.getProgram()?.getSourceFile(toFileName(path));
|
|
730
1044
|
if (!sf || sf.text !== file.text) return [];
|
|
731
|
-
return
|
|
1045
|
+
return service.getSemanticDiagnostics(toFileName(path)).map((d) => toCodeDiagnostic(file.model, sf, d, "type"));
|
|
732
1046
|
}
|
|
733
1047
|
/**
|
|
734
1048
|
* Text edits that rename a block and every usage of it, in every file of the project.
|
|
@@ -745,29 +1059,54 @@ var Project = class {
|
|
|
745
1059
|
if (namesVisibleFrom(file.model, block).has(newName)) return { ok: false, error: `${newName} already exists here` };
|
|
746
1060
|
if (file.model.diagnostics.length) return { ok: false, error: `Fix the syntax error on line ${file.model.firstErrorLine} first` };
|
|
747
1061
|
const fileName2 = toFileName(path);
|
|
748
|
-
const info = this.
|
|
1062
|
+
const info = this.serviceOf(path).getRenameInfo(fileName2, block.nameStart, { allowRenameOfImportPath: false });
|
|
749
1063
|
if (!info.canRename) return { ok: false, error: info.localizedErrorMessage };
|
|
750
|
-
const locations = this.service.findRenameLocations(fileName2, block.nameStart, false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
|
|
751
1064
|
const edits = {};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1065
|
+
const renamedLocations = /* @__PURE__ */ new Set();
|
|
1066
|
+
for (const configPath of this.configsUsing(path)) {
|
|
1067
|
+
const service = this.serviceOfConfig(configPath);
|
|
1068
|
+
if (!service.getProgram()?.getSourceFile(fileName2)) continue;
|
|
1069
|
+
const renameStarts = [{ fileName: fileName2, position: block.nameStart }];
|
|
1070
|
+
const visitedStarts = /* @__PURE__ */ new Set();
|
|
1071
|
+
for (let start = renameStarts.pop(); start; start = renameStarts.pop()) {
|
|
1072
|
+
const startKey = `${start.fileName}:${start.position}`;
|
|
1073
|
+
if (visitedStarts.has(startKey)) continue;
|
|
1074
|
+
visitedStarts.add(startKey);
|
|
1075
|
+
const locations = service.findRenameLocations(start.fileName, start.position, false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
|
|
1076
|
+
for (const location of locations) {
|
|
1077
|
+
const locationPath = fromFileName(location.fileName);
|
|
1078
|
+
if (!locationPath) continue;
|
|
1079
|
+
if (isAliasInSpecifier(location)) renameStarts.push({ fileName: location.fileName, position: location.textSpan.start });
|
|
1080
|
+
const locationKey = `${location.fileName}:${location.textSpan.start}`;
|
|
1081
|
+
if (renamedLocations.has(locationKey)) continue;
|
|
1082
|
+
renamedLocations.add(locationKey);
|
|
1083
|
+
const text = isAliasInSpecifier(location) ? newName : (location.prefixText ?? "") + newName + (location.suffixText ?? "");
|
|
1084
|
+
(edits[locationPath] ??= []).push({ start: location.textSpan.start, end: location.textSpan.start + location.textSpan.length, text });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
757
1087
|
}
|
|
1088
|
+
for (const fileEdits of Object.values(edits)) fileEdits.sort((a, b) => a.start - b.start);
|
|
758
1089
|
return { ok: true, edits };
|
|
759
1090
|
}
|
|
760
1091
|
// ---------------------------------------------------------------- internals
|
|
761
|
-
/**
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1092
|
+
/** The configs of a file and of the files that can use its blocks: its importers, also through re-exports. */
|
|
1093
|
+
configsUsing(path) {
|
|
1094
|
+
const owner = this.configOf(path);
|
|
1095
|
+
const importerConfigs = this.importersThroughReExports(path).map((importer) => this.configOf(importer));
|
|
1096
|
+
return [owner, ...new Set(importerConfigs.filter((configPath) => configPath !== owner))];
|
|
1097
|
+
}
|
|
1098
|
+
/** Files that import `path`, or import a file that re-exports from it (an `index.ts` of a folder or package). */
|
|
1099
|
+
importersThroughReExports(path) {
|
|
1100
|
+
const importers = /* @__PURE__ */ new Set();
|
|
1101
|
+
const reExporters = [path];
|
|
1102
|
+
for (let reExported = reExporters.pop(); reExported !== void 0; reExported = reExporters.pop()) {
|
|
1103
|
+
for (const importer of this.importersOf(reExported)) {
|
|
1104
|
+
if (importers.has(importer) || importer === path) continue;
|
|
1105
|
+
importers.add(importer);
|
|
1106
|
+
if (this.importsOf(importer).reExportedPaths.has(reExported)) reExporters.push(importer);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return [...importers];
|
|
771
1110
|
}
|
|
772
1111
|
otherFileResolver() {
|
|
773
1112
|
return {
|
|
@@ -793,29 +1132,38 @@ var Project = class {
|
|
|
793
1132
|
importersOf(path) {
|
|
794
1133
|
const importers = [];
|
|
795
1134
|
for (const [otherPath, file] of this.files) {
|
|
796
|
-
if (otherPath !== path && this.
|
|
1135
|
+
if (otherPath !== path && this.importsOf(otherPath, file).importedPaths.has(path)) importers.push(otherPath);
|
|
797
1136
|
}
|
|
798
1137
|
return importers;
|
|
799
1138
|
}
|
|
800
|
-
|
|
801
|
-
if (file.
|
|
802
|
-
const
|
|
1139
|
+
importsOf(path, file = this.files.get(path)) {
|
|
1140
|
+
if (file.imports?.fileSetVersion === this.fileSetVersion && file.imports.fileVersion === file.version) return file.imports;
|
|
1141
|
+
const importedPaths = /* @__PURE__ */ new Set();
|
|
1142
|
+
const reExportedPaths = /* @__PURE__ */ new Set();
|
|
803
1143
|
const host = this.moduleResolutionHost();
|
|
1144
|
+
const resolve = (specifier) => {
|
|
1145
|
+
const result = ts5.resolveModuleName(specifier, toFileName(path), this.optionsOfFile(path), host).resolvedModule;
|
|
1146
|
+
return result ? fromFileName(result.resolvedFileName) : null;
|
|
1147
|
+
};
|
|
804
1148
|
for (const block of file.model.roots) {
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
const
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1149
|
+
const code = block.kind === "statement" || block.kind === "variable" ? file.text.slice(block.start, block.end) : "";
|
|
1150
|
+
const reExportSpecifier = block.kind === "statement" ? RE_EXPORT.exec(code)?.[2] : void 0;
|
|
1151
|
+
const requiredSpecifier = block.kind === "variable" ? REQUIRE_CALL.exec(code)?.[2] : void 0;
|
|
1152
|
+
const specifier = block.kind === "import" ? block.name : reExportSpecifier ?? requiredSpecifier;
|
|
1153
|
+
const target = specifier ? resolve(specifier) : null;
|
|
1154
|
+
if (!target) continue;
|
|
1155
|
+
importedPaths.add(target);
|
|
1156
|
+
if (reExportSpecifier) reExportedPaths.add(target);
|
|
1157
|
+
}
|
|
1158
|
+
file.imports = { importedPaths, reExportedPaths, fileSetVersion: this.fileSetVersion, fileVersion: file.version };
|
|
1159
|
+
return file.imports;
|
|
813
1160
|
}
|
|
814
1161
|
moduleResolutionHost() {
|
|
815
1162
|
return {
|
|
816
1163
|
fileExists: (name) => this.fileExists(name),
|
|
817
1164
|
readFile: (name) => this.readFile(name),
|
|
818
|
-
directoryExists: (dir) => this.directoryExists(dir)
|
|
1165
|
+
directoryExists: (dir) => this.directoryExists(dir),
|
|
1166
|
+
getCurrentDirectory: () => "/"
|
|
819
1167
|
};
|
|
820
1168
|
}
|
|
821
1169
|
fileExists(name) {
|
|
@@ -834,11 +1182,12 @@ var Project = class {
|
|
|
834
1182
|
for (const path of this.files.keys()) if (path.startsWith(prefix)) return true;
|
|
835
1183
|
return false;
|
|
836
1184
|
}
|
|
837
|
-
|
|
1185
|
+
/** The host of one config's program. Its files are those that use the config; their imports come in from the rest. */
|
|
1186
|
+
createHost(configPath) {
|
|
838
1187
|
return {
|
|
839
|
-
getCompilationSettings: () => this.
|
|
1188
|
+
getCompilationSettings: () => this.programs().optionsByConfig.get(configPath) ?? this.programs().optionsByConfig.get(NO_CONFIG),
|
|
840
1189
|
getProjectVersion: () => String(this.projectVersion),
|
|
841
|
-
getScriptFileNames: () =>
|
|
1190
|
+
getScriptFileNames: () => (this.programs().pathsByConfig.get(configPath) ?? []).map(toFileName),
|
|
842
1191
|
getScriptVersion: (name) => {
|
|
843
1192
|
if (name.startsWith(LIB_DIR)) return "0";
|
|
844
1193
|
const path = fromFileName(name);
|
|
@@ -847,7 +1196,7 @@ var Project = class {
|
|
|
847
1196
|
getScriptSnapshot: (name) => {
|
|
848
1197
|
if (name.startsWith(LIB_DIR)) {
|
|
849
1198
|
const text = this.libFiles[name.slice(LIB_DIR.length)];
|
|
850
|
-
return text === void 0 ? void 0 :
|
|
1199
|
+
return text === void 0 ? void 0 : ts5.ScriptSnapshot.fromString(text);
|
|
851
1200
|
}
|
|
852
1201
|
const path = fromFileName(name);
|
|
853
1202
|
const file = path ? this.files.get(path) : void 0;
|
|
@@ -856,6 +1205,13 @@ var Project = class {
|
|
|
856
1205
|
return file.snapshot;
|
|
857
1206
|
},
|
|
858
1207
|
getCurrentDirectory: () => "/",
|
|
1208
|
+
// A file another package's program pulls in keeps its own package's aliases.
|
|
1209
|
+
resolveModuleNameLiterals: (moduleLiterals, containingFile) => {
|
|
1210
|
+
const containingPath = fromFileName(containingFile);
|
|
1211
|
+
const options = this.optionsOfFile(containingPath);
|
|
1212
|
+
const host = this.moduleResolutionHost();
|
|
1213
|
+
return moduleLiterals.map((literal) => ts5.resolveModuleName(literal.text, containingFile, options, host));
|
|
1214
|
+
},
|
|
859
1215
|
getDefaultLibFileName: () => LIB_DIR + "lib.d.ts",
|
|
860
1216
|
fileExists: (name) => this.fileExists(name),
|
|
861
1217
|
readFile: (name) => this.readFile(name),
|
|
@@ -865,6 +1221,9 @@ var Project = class {
|
|
|
865
1221
|
};
|
|
866
1222
|
}
|
|
867
1223
|
};
|
|
1224
|
+
function isAliasInSpecifier(location) {
|
|
1225
|
+
return /\bas\s*$/.test(location.prefixText ?? "") || /^\s*as\b/.test(location.suffixText ?? "");
|
|
1226
|
+
}
|
|
868
1227
|
var TextSnapshot = class _TextSnapshot {
|
|
869
1228
|
constructor(text) {
|
|
870
1229
|
this.text = text;
|
|
@@ -887,7 +1246,7 @@ function changedRegion(before, after) {
|
|
|
887
1246
|
while (start < shorter && before.charCodeAt(start) === after.charCodeAt(start)) start++;
|
|
888
1247
|
let fromEnd = 0;
|
|
889
1248
|
while (fromEnd < shorter - start && before.charCodeAt(before.length - 1 - fromEnd) === after.charCodeAt(after.length - 1 - fromEnd)) fromEnd++;
|
|
890
|
-
return
|
|
1249
|
+
return ts5.createTextChangeRange(ts5.createTextSpan(start, before.length - fromEnd - start), after.length - fromEnd - start);
|
|
891
1250
|
}
|
|
892
1251
|
function toFileName(path) {
|
|
893
1252
|
return "/" + path;
|
|
@@ -911,26 +1270,12 @@ function namesVisibleFrom(model, block) {
|
|
|
911
1270
|
return names;
|
|
912
1271
|
}
|
|
913
1272
|
function isValidIdentifier(name) {
|
|
914
|
-
const scanner =
|
|
1273
|
+
const scanner = ts5.createScanner(ts5.ScriptTarget.Latest, true, ts5.LanguageVariant.Standard, name);
|
|
915
1274
|
const token = scanner.scan();
|
|
916
1275
|
const isWholeText = scanner.getTokenEnd() === name.length && name.length > 0;
|
|
917
|
-
const isNameToken = token ===
|
|
1276
|
+
const isNameToken = token === ts5.SyntaxKind.Identifier || token > ts5.SyntaxKind.LastReservedWord && token <= ts5.SyntaxKind.LastKeyword;
|
|
918
1277
|
return isWholeText && isNameToken;
|
|
919
1278
|
}
|
|
920
|
-
function compilerOptionsOf(config) {
|
|
921
|
-
const configFileName = toFileName(config.path);
|
|
922
|
-
const json = ts2.parseConfigFileTextToJson(configFileName, config.text);
|
|
923
|
-
if (json.error || !json.config) return {};
|
|
924
|
-
const readConfig = (name) => name === configFileName ? config.text : config.extendedFiles?.[fromFileName(name) ?? ""];
|
|
925
|
-
const host = {
|
|
926
|
-
useCaseSensitiveFileNames: true,
|
|
927
|
-
readDirectory: () => [],
|
|
928
|
-
fileExists: (name) => readConfig(name) !== void 0,
|
|
929
|
-
readFile: readConfig
|
|
930
|
-
};
|
|
931
|
-
const configFolder = configFileName.slice(0, configFileName.lastIndexOf("/") + 1);
|
|
932
|
-
return ts2.parseJsonConfigFileContent(json.config, host, configFolder, void 0, configFileName).options;
|
|
933
|
-
}
|
|
934
1279
|
|
|
935
1280
|
// src/remoteProject.ts
|
|
936
1281
|
var RemoteProject = class {
|
|
@@ -966,6 +1311,10 @@ var RemoteProject = class {
|
|
|
966
1311
|
setConfigFile(config) {
|
|
967
1312
|
void this.callServer("setConfigFile", [config]);
|
|
968
1313
|
}
|
|
1314
|
+
/** The folder's config files (one per package in a monorepo) and package.json files (or none, with null). */
|
|
1315
|
+
setWorkspace(workspace) {
|
|
1316
|
+
void this.callServer("setWorkspace", [workspace]);
|
|
1317
|
+
}
|
|
969
1318
|
getModel(path) {
|
|
970
1319
|
const file = this.files.get(path);
|
|
971
1320
|
if (!file) return void 0;
|
|
@@ -988,7 +1337,7 @@ var RemoteProject = class {
|
|
|
988
1337
|
};
|
|
989
1338
|
|
|
990
1339
|
// src/projectServer.ts
|
|
991
|
-
var SERVER_METHODS = ["setLibFiles", "setConfigFile", "setFiles", "setFile", "removeFile", "resolve", "renameBlock"];
|
|
1340
|
+
var SERVER_METHODS = ["setLibFiles", "setConfigFile", "setWorkspace", "setFiles", "setFile", "removeFile", "resolve", "renameBlock"];
|
|
992
1341
|
var ProjectServer = class {
|
|
993
1342
|
project = new Project();
|
|
994
1343
|
setLibFiles(libFiles) {
|
|
@@ -997,6 +1346,9 @@ var ProjectServer = class {
|
|
|
997
1346
|
setConfigFile(config) {
|
|
998
1347
|
this.project.setConfigFile(config);
|
|
999
1348
|
}
|
|
1349
|
+
setWorkspace(workspace) {
|
|
1350
|
+
this.project.setWorkspace(workspace);
|
|
1351
|
+
}
|
|
1000
1352
|
setFiles(files) {
|
|
1001
1353
|
for (const [path, text] of Object.entries(files)) this.project.setFile(path, text);
|
|
1002
1354
|
}
|
|
@@ -1051,7 +1403,7 @@ function connectProjectWorker(port) {
|
|
|
1051
1403
|
}
|
|
1052
1404
|
|
|
1053
1405
|
// src/edits.ts
|
|
1054
|
-
import
|
|
1406
|
+
import ts6 from "typescript";
|
|
1055
1407
|
function applyEdit(text, e) {
|
|
1056
1408
|
return text.slice(0, e.start) + e.text + text.slice(e.end);
|
|
1057
1409
|
}
|
|
@@ -1178,13 +1530,13 @@ function toTopLevel(code) {
|
|
|
1178
1530
|
function renameDeclaration(code, inClass, name) {
|
|
1179
1531
|
const prefix = inClass ? "class __cc__ {\n" : "";
|
|
1180
1532
|
const src = prefix + code + (inClass ? "\n}" : "");
|
|
1181
|
-
const sf =
|
|
1533
|
+
const sf = ts6.createSourceFile("snippet.ts", src, ts6.ScriptTarget.Latest, true);
|
|
1182
1534
|
let target;
|
|
1183
1535
|
const first = sf.statements[0];
|
|
1184
|
-
if (inClass && first &&
|
|
1185
|
-
else if (first && (
|
|
1536
|
+
if (inClass && first && ts6.isClassDeclaration(first)) target = first.members[0]?.name;
|
|
1537
|
+
else if (first && (ts6.isFunctionDeclaration(first) || ts6.isClassDeclaration(first) || ts6.isInterfaceDeclaration(first) || ts6.isTypeAliasDeclaration(first) || ts6.isEnumDeclaration(first)))
|
|
1186
1538
|
target = first.name;
|
|
1187
|
-
else if (first &&
|
|
1539
|
+
else if (first && ts6.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
|
|
1188
1540
|
if (!target) return code;
|
|
1189
1541
|
const s = target.getStart(sf) - prefix.length;
|
|
1190
1542
|
return code.slice(0, s) + name + code.slice(target.end - prefix.length);
|
|
@@ -1196,26 +1548,26 @@ function must(model, id) {
|
|
|
1196
1548
|
}
|
|
1197
1549
|
function clipFromText(code) {
|
|
1198
1550
|
const text = code.replace(/\s+$/, "");
|
|
1199
|
-
const sf =
|
|
1551
|
+
const sf = ts6.createSourceFile("clip.ts", text, ts6.ScriptTarget.Latest, true);
|
|
1200
1552
|
const first = sf.statements[0];
|
|
1201
1553
|
const diags = sf.parseDiagnostics ?? [];
|
|
1202
1554
|
if (first && !diags.length) {
|
|
1203
1555
|
let name;
|
|
1204
1556
|
let kind = "statement";
|
|
1205
|
-
if (
|
|
1206
|
-
else if (
|
|
1207
|
-
else if (
|
|
1208
|
-
else if (
|
|
1557
|
+
if (ts6.isFunctionDeclaration(first)) name = first.name?.text, kind = "function";
|
|
1558
|
+
else if (ts6.isClassDeclaration(first)) name = first.name?.text, kind = "class";
|
|
1559
|
+
else if (ts6.isVariableStatement(first)) name = first.declarationList.declarations[0]?.name.getText(), kind = "variable";
|
|
1560
|
+
else if (ts6.isInterfaceDeclaration(first) || ts6.isTypeAliasDeclaration(first) || ts6.isEnumDeclaration(first)) name = first.name.text, kind = "type";
|
|
1209
1561
|
if (name) return { code: text, name, kind, fromClass: false };
|
|
1210
1562
|
}
|
|
1211
|
-
const wrapped =
|
|
1563
|
+
const wrapped = ts6.createSourceFile("clip.ts", `class __cc__ {
|
|
1212
1564
|
${text}
|
|
1213
|
-
}`,
|
|
1565
|
+
}`, ts6.ScriptTarget.Latest, true);
|
|
1214
1566
|
const cls = wrapped.statements[0];
|
|
1215
1567
|
const wd = wrapped.parseDiagnostics ?? [];
|
|
1216
|
-
if (cls &&
|
|
1568
|
+
if (cls && ts6.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
|
|
1217
1569
|
const m = cls.members[0];
|
|
1218
|
-
return { code: text, name: m.name.getText(wrapped), kind:
|
|
1570
|
+
return { code: text, name: m.name.getText(wrapped), kind: ts6.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
|
|
1219
1571
|
}
|
|
1220
1572
|
return { code: text, name: "", kind: "statement", fromClass: false };
|
|
1221
1573
|
}
|
|
@@ -1373,7 +1725,7 @@ function resolveConflict(text, hunk, choice) {
|
|
|
1373
1725
|
var BAND_ORDER = ["data", "types", "main", "bottom"];
|
|
1374
1726
|
var ZONE_RULES = [
|
|
1375
1727
|
{ zone: "left", description: "Imports on the left", matches: (block) => block.kind === "import" },
|
|
1376
|
-
{ zone: "data", description: "Constants
|
|
1728
|
+
{ zone: "data", description: "Constants, other data and React state at the top", matches: (block) => block.kind === "variable" || block.kind === "state" },
|
|
1377
1729
|
{ zone: "types", description: "Then interfaces, types and enums", matches: (block) => block.kind === "type" },
|
|
1378
1730
|
{ zone: "bottom", description: "Code that runs when the file loads, at the bottom", matches: (block) => block.kind === "statement" },
|
|
1379
1731
|
{ zone: "main", description: "Functions, classes and the rest in between", matches: () => true }
|
|
@@ -1406,8 +1758,64 @@ var RELATEDNESS_RULES = [
|
|
|
1406
1758
|
score: (a, b) => firstWord(a.name) && firstWord(a.name) === firstWord(b.name) ? 1 : 0
|
|
1407
1759
|
}
|
|
1408
1760
|
];
|
|
1761
|
+
var PLACEMENT_RULES = [
|
|
1762
|
+
{
|
|
1763
|
+
description: "A getter right above its setter, a setter right below its getter",
|
|
1764
|
+
spot(block, placed) {
|
|
1765
|
+
const siblings = [block, ...placed.map((sibling) => sibling.block)];
|
|
1766
|
+
for (const rule of STACK_RULES) {
|
|
1767
|
+
for (const [top, bottom] of rule.pairs(siblings)) {
|
|
1768
|
+
const placedBottom = top === block && placed.find((sibling) => sibling.block === bottom);
|
|
1769
|
+
if (placedBottom) return { sibling: placedBottom, side: "above" };
|
|
1770
|
+
const placedTop = bottom === block && placed.find((sibling) => sibling.block === top);
|
|
1771
|
+
if (placedTop) return { sibling: placedTop, side: "below" };
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return void 0;
|
|
1775
|
+
}
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
description: "Like a newspaper: a function goes in the column right of its caller, after the function called before it",
|
|
1779
|
+
spot(block, placed, factsOf) {
|
|
1780
|
+
if (!isFunctionLike(block)) return void 0;
|
|
1781
|
+
const caller = placed.find((sibling) => isFunctionLike(sibling.block) && factsOf(sibling.block).calls.includes(block.id));
|
|
1782
|
+
if (!caller) return void 0;
|
|
1783
|
+
const calls = factsOf(caller.block).calls;
|
|
1784
|
+
const isInCallerColumn = (sibling) => sibling !== caller && sibling.x >= caller.x + caller.w;
|
|
1785
|
+
const calledBefore = calls.slice(0, calls.indexOf(block.id)).reverse();
|
|
1786
|
+
const previousInColumn = calledBefore.map((id) => placed.find((sibling) => sibling.block.id === id && isInCallerColumn(sibling))).find(Boolean);
|
|
1787
|
+
return previousInColumn ? { sibling: previousInColumn, side: "below" } : { sibling: caller, side: "right" };
|
|
1788
|
+
}
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
description: "An import, constant or type below the last one of its kind before it in the file",
|
|
1792
|
+
spot(block, placed) {
|
|
1793
|
+
const zone = zoneOf(block);
|
|
1794
|
+
if (zone === "main") return void 0;
|
|
1795
|
+
const sameZone = placed.filter((sibling) => zoneOf(sibling.block) === zone);
|
|
1796
|
+
const lastBefore = sameZone.filter((sibling) => sibling.block.start < block.start).at(-1) ?? sameZone[0];
|
|
1797
|
+
return lastBefore ? { sibling: lastBefore, side: "below" } : void 0;
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
description: "Below the block it is most related to",
|
|
1802
|
+
spot(block, placed, factsOf) {
|
|
1803
|
+
let best;
|
|
1804
|
+
let bestScore = 0;
|
|
1805
|
+
for (const sibling of placed) {
|
|
1806
|
+
const score = RELATEDNESS_RULES.reduce((total, rule) => total + rule.score(block, sibling.block, factsOf), 0);
|
|
1807
|
+
if (score > bestScore) {
|
|
1808
|
+
best = sibling;
|
|
1809
|
+
bestScore = score;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return best ? { sibling: best, side: "below" } : void 0;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
];
|
|
1816
|
+
var zoneOf = (block) => ZONE_RULES.find((rule) => rule.matches(block)).zone;
|
|
1409
1817
|
var NEWSPAPER_DEPTH = 3;
|
|
1410
|
-
var isFunctionLike = (block) => block.kind === "function";
|
|
1818
|
+
var isFunctionLike = (block) => block.kind === "function" || block.kind === "component";
|
|
1411
1819
|
function accessedName(block, accessor) {
|
|
1412
1820
|
if (block.kind !== "function") return void 0;
|
|
1413
1821
|
if (block.tags.includes(accessor)) return block.name;
|
|
@@ -1467,8 +1875,19 @@ function arrangeSiblings(blocks, sizeOf, text, gap, isFile) {
|
|
|
1467
1875
|
}
|
|
1468
1876
|
return positions;
|
|
1469
1877
|
}
|
|
1878
|
+
function newBlockSpots(siblings, text) {
|
|
1879
|
+
let factsById;
|
|
1880
|
+
const factsOf = (sibling) => factsById.get(sibling.id);
|
|
1881
|
+
return (block, placed) => {
|
|
1882
|
+
factsById ??= siblingFacts(siblings, text);
|
|
1883
|
+
for (const rule of PLACEMENT_RULES) {
|
|
1884
|
+
const spot = rule.spot(block, placed, factsOf);
|
|
1885
|
+
if (spot) return spot;
|
|
1886
|
+
}
|
|
1887
|
+
return void 0;
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1470
1890
|
function stackedUnits(blocks) {
|
|
1471
|
-
const zoneOf = (block) => ZONE_RULES.find((rule) => rule.matches(block)).zone;
|
|
1472
1891
|
const unitOf = /* @__PURE__ */ new Map();
|
|
1473
1892
|
const units = blocks.map((block, order) => {
|
|
1474
1893
|
const unit = { members: [block], arrangement: { block: block.id }, zone: zoneOf(block), order };
|
|
@@ -1550,6 +1969,7 @@ function orderByRelatedness(units, factsOf) {
|
|
|
1550
1969
|
function siblingFacts(blocks, text) {
|
|
1551
1970
|
const idsByName = /* @__PURE__ */ new Map();
|
|
1552
1971
|
for (const block of blocks) idsByName.set(block.name, [...idsByName.get(block.name) ?? [], block.id]);
|
|
1972
|
+
const componentIds = new Set(blocks.filter((block) => block.kind === "component").map((block) => block.id));
|
|
1553
1973
|
const facts = /* @__PURE__ */ new Map();
|
|
1554
1974
|
for (const block of blocks) {
|
|
1555
1975
|
const code = codeWithoutChildren(text, [block.start, block.end], block.children);
|
|
@@ -1559,9 +1979,11 @@ function siblingFacts(blocks, text) {
|
|
|
1559
1979
|
const ids = idsByName.get(match[0])?.filter((id) => id !== block.id);
|
|
1560
1980
|
if (!ids?.length) continue;
|
|
1561
1981
|
const isCall = /^\s*\(/.test(code.slice(match.index + match[0].length, match.index + match[0].length + 40));
|
|
1982
|
+
const isJsxTag = /<\/?$/.test(code.slice(Math.max(0, match.index - 2), match.index));
|
|
1562
1983
|
for (const id of ids) {
|
|
1563
1984
|
uses.add(id);
|
|
1564
|
-
|
|
1985
|
+
const isRender = isJsxTag && componentIds.has(id);
|
|
1986
|
+
if ((isCall || isRender) && !calls.includes(id)) calls.push(id);
|
|
1565
1987
|
}
|
|
1566
1988
|
}
|
|
1567
1989
|
facts.set(block.id, { calls, uses });
|
|
@@ -1672,11 +2094,18 @@ function computeGeometry(model, layout, hints = {}, opts = {}) {
|
|
|
1672
2094
|
placed.push(g);
|
|
1673
2095
|
} else unplaced.push(g);
|
|
1674
2096
|
}
|
|
2097
|
+
const spotByRules = newBlockSpots(sibs.map((sibling) => sibling.block), model.text);
|
|
1675
2098
|
for (const g of unplaced) {
|
|
1676
|
-
const idx = sibs.indexOf(g);
|
|
1677
|
-
const prev = sibs.slice(0, idx).reverse().find((s) => placed.includes(s));
|
|
1678
2099
|
const hint = hints[g.id];
|
|
1679
|
-
const
|
|
2100
|
+
const ruleSpot = hint || !placed.length ? void 0 : spotByRules(g.block, placed);
|
|
2101
|
+
let p;
|
|
2102
|
+
if (hint) p = freeSpotNear(hint, g, placed, !isFile);
|
|
2103
|
+
else if (ruleSpot?.side === "below") p = freeSpotDown(pointBeside(ruleSpot, g), g, placed, !isFile);
|
|
2104
|
+
else if (ruleSpot) p = freeSpotNear(pointBeside(ruleSpot, g), g, placed, !isFile);
|
|
2105
|
+
else {
|
|
2106
|
+
const previousPlacedInFile = sibs.slice(0, sibs.indexOf(g)).reverse().find((sibling) => placed.includes(sibling));
|
|
2107
|
+
p = findFreeSpot(g, placed, previousPlacedInFile, !isFile);
|
|
2108
|
+
}
|
|
1680
2109
|
g.x = p.x;
|
|
1681
2110
|
g.y = p.y;
|
|
1682
2111
|
assigned[g.id] = p;
|
|
@@ -1709,6 +2138,26 @@ function computeGeometry(model, layout, hints = {}, opts = {}) {
|
|
|
1709
2138
|
};
|
|
1710
2139
|
return { geo, order, frame, assigned };
|
|
1711
2140
|
}
|
|
2141
|
+
function placeGhostBlocks(ghosts, ghostGeometry, currentModel, current) {
|
|
2142
|
+
const positions = /* @__PURE__ */ new Map();
|
|
2143
|
+
const placedGhostsByParent = /* @__PURE__ */ new Map();
|
|
2144
|
+
for (const ghost of ghosts) {
|
|
2145
|
+
const ghostGeo = ghostGeometry.geo.get(ghost.id);
|
|
2146
|
+
if (!ghostGeo) continue;
|
|
2147
|
+
const parentId = ghost.parentId ?? null;
|
|
2148
|
+
const parentGeo = parentId ? current.geo.get(parentId) : void 0;
|
|
2149
|
+
if (parentId && !parentGeo) continue;
|
|
2150
|
+
if (parentGeo && (parentGeo.hidden || parentGeo.collapsed)) continue;
|
|
2151
|
+
const currentSiblings = parentId ? currentModel.blocks.get(parentId).children : currentModel.roots;
|
|
2152
|
+
const placedGhosts = placedGhostsByParent.get(parentId) ?? [];
|
|
2153
|
+
const taken = [...currentSiblings.map((sibling) => current.geo.get(sibling.id)), ...placedGhosts];
|
|
2154
|
+
const spot = freeSpotNear({ x: ghostGeo.x, y: ghostGeo.y }, ghostGeo, taken, !!parentGeo);
|
|
2155
|
+
placedGhosts.push({ ...spot, w: ghostGeo.w, h: ghostGeo.h });
|
|
2156
|
+
placedGhostsByParent.set(parentId, placedGhosts);
|
|
2157
|
+
positions.set(ghost.id, parentGeo ? { x: parentGeo.ax + M.pad + spot.x, y: parentGeo.ay + parentGeo.contentTop + spot.y } : spot);
|
|
2158
|
+
}
|
|
2159
|
+
return positions;
|
|
2160
|
+
}
|
|
1712
2161
|
function pushApart(sibs, assigned) {
|
|
1713
2162
|
for (let iter = 0; iter < 100; iter++) {
|
|
1714
2163
|
let moved = false;
|
|
@@ -1729,6 +2178,12 @@ function pushApart(sibs, assigned) {
|
|
|
1729
2178
|
if (!moved) break;
|
|
1730
2179
|
}
|
|
1731
2180
|
}
|
|
2181
|
+
function pointBeside(spot, size) {
|
|
2182
|
+
const { sibling, side } = spot;
|
|
2183
|
+
if (side === "right") return { x: sibling.x + sibling.w + M.gap, y: sibling.y };
|
|
2184
|
+
if (side === "below") return { x: sibling.x, y: sibling.y + sibling.h + M.gap };
|
|
2185
|
+
return { x: sibling.x, y: sibling.y - size.h - M.gap };
|
|
2186
|
+
}
|
|
1732
2187
|
function headerChars(b) {
|
|
1733
2188
|
const tags = b.tags.filter((t) => t !== "export").join(" ");
|
|
1734
2189
|
return 4 + b.name.length + Math.min(b.detail.length, 40) + (tags ? tags.length + 2 : 0) + 3;
|
|
@@ -1771,6 +2226,12 @@ function freeSpotNear(at, g, placed, clampPositive) {
|
|
|
1771
2226
|
const bottom = Math.max(...others.map((p) => p.y + p.h));
|
|
1772
2227
|
return round({ x: start.x, y: bottom + M.gap });
|
|
1773
2228
|
}
|
|
2229
|
+
function freeSpotDown(at, g, placed, clampPositive) {
|
|
2230
|
+
const spot = clampPositive ? { x: Math.max(0, at.x), y: Math.max(0, at.y) } : { ...at };
|
|
2231
|
+
const blockCoveringSpot = () => placed.find((p) => overlaps({ x: spot.x, y: spot.y, w: g.w, h: g.h }, p));
|
|
2232
|
+
for (let covering = blockCoveringSpot(); covering; covering = blockCoveringSpot()) spot.y = covering.y + covering.h + M.gap;
|
|
2233
|
+
return round(spot);
|
|
2234
|
+
}
|
|
1774
2235
|
function dist(a, b) {
|
|
1775
2236
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
1776
2237
|
}
|
|
@@ -1859,6 +2320,8 @@ var CSS = (
|
|
|
1859
2320
|
--k-import: #6b7280; --k-import-head: #3b3f46; --k-import-bg: #25272c;
|
|
1860
2321
|
--k-class: #34b27a; --k-class-head: #1d6b4a; --k-class-bg: rgba(52,178,122,.08);
|
|
1861
2322
|
--k-function: #ec5fa5; --k-function-head: #8f2f62; --k-function-bg: rgba(236,95,165,.09);
|
|
2323
|
+
--k-component: #22b8cf; --k-component-head: #146c7a; --k-component-bg: rgba(34,184,207,.09);
|
|
2324
|
+
--k-state: #f0883e; --k-state-head: #8a4614; --k-state-bg: rgba(240,136,62,.09);
|
|
1862
2325
|
--k-variable: #4b8ef7; --k-variable-head: #23508f; --k-variable-bg: rgba(75,142,247,.09);
|
|
1863
2326
|
--k-type: #d4a92a; --k-type-head: #6e5714; --k-type-bg: rgba(212,169,42,.08);
|
|
1864
2327
|
--k-namespace: #9b8cf8; --k-namespace-head: #4d4291; --k-namespace-bg: rgba(155,140,248,.08);
|
|
@@ -1881,6 +2344,8 @@ var CSS = (
|
|
|
1881
2344
|
--k-import: #6b7280; --k-import-head: #4b5058; --k-import-bg: #eceef1;
|
|
1882
2345
|
--k-class: #1f9963; --k-class-head: #1f9963; --k-class-bg: rgba(31,153,99,.07);
|
|
1883
2346
|
--k-function: #d63e8c; --k-function-head: #d63e8c; --k-function-bg: rgba(214,62,140,.07);
|
|
2347
|
+
--k-component: #0f8fa3; --k-component-head: #0f8fa3; --k-component-bg: rgba(15,143,163,.07);
|
|
2348
|
+
--k-state: #d4691c; --k-state-head: #d4691c; --k-state-bg: rgba(212,105,28,.07);
|
|
1884
2349
|
--k-variable: #2f6fe0; --k-variable-head: #2f6fe0; --k-variable-bg: rgba(47,111,224,.07);
|
|
1885
2350
|
--k-type: #b58a0c; --k-type-head: #a87f08; --k-type-bg: rgba(181,138,12,.08);
|
|
1886
2351
|
--k-namespace: #6f5ce6; --k-namespace-head: #6f5ce6; --k-namespace-bg: rgba(111,92,230,.07);
|
|
@@ -1900,6 +2365,8 @@ var CSS = (
|
|
|
1900
2365
|
.cc-block.k-import { --k: var(--k-import); --kh: var(--k-import-head); --kbg: var(--k-import-bg); }
|
|
1901
2366
|
.cc-block.k-class { --k: var(--k-class); --kh: var(--k-class-head); --kbg: var(--k-class-bg); }
|
|
1902
2367
|
.cc-block.k-function { --k: var(--k-function); --kh: var(--k-function-head); --kbg: var(--k-function-bg); }
|
|
2368
|
+
.cc-block.k-component { --k: var(--k-component); --kh: var(--k-component-head); --kbg: var(--k-component-bg); }
|
|
2369
|
+
.cc-block.k-state { --k: var(--k-state); --kh: var(--k-state-head); --kbg: var(--k-state-bg); }
|
|
1903
2370
|
.cc-block.k-variable { --k: var(--k-variable); --kh: var(--k-variable-head); --kbg: var(--k-variable-bg); }
|
|
1904
2371
|
.cc-block.k-type { --k: var(--k-type); --kh: var(--k-type-head); --kbg: var(--k-type-bg); }
|
|
1905
2372
|
.cc-block.k-namespace { --k: var(--k-namespace); --kh: var(--k-namespace-head); --kbg: var(--k-namespace-bg); }
|
|
@@ -1929,6 +2396,7 @@ var CSS = (
|
|
|
1929
2396
|
.cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .45; }
|
|
1930
2397
|
.cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: rgba(255,255,255,.06); }
|
|
1931
2398
|
.cc-mini > i.k-class { border-color: var(--k-class); } .cc-mini > i.k-function { border-color: var(--k-function); }
|
|
2399
|
+
.cc-mini > i.k-component { border-color: var(--k-component); } .cc-mini > i.k-state { border-color: var(--k-state); }
|
|
1932
2400
|
.cc-mini > i.k-variable { border-color: var(--k-variable); } .cc-mini > i.k-type { border-color: var(--k-type); }
|
|
1933
2401
|
|
|
1934
2402
|
.cc-err {
|
|
@@ -2024,6 +2492,7 @@ var CSS = (
|
|
|
2024
2492
|
.cc-btn:hover { background: rgba(127,127,127,.16); }
|
|
2025
2493
|
.cc-btn.danger { color: var(--cc-danger); }
|
|
2026
2494
|
.cc-btn.primary { background: var(--cc-accent); color: #fff; }
|
|
2495
|
+
.cc-language { padding: 0 8px; font: 600 11px/30px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); color: var(--cc-muted); white-space: nowrap; }
|
|
2027
2496
|
.cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 2px; flex: none; }
|
|
2028
2497
|
.cc-refs {
|
|
2029
2498
|
position: absolute; left: 50%; bottom: 64px; transform: translateX(-50%); z-index: 21; width: min(360px, calc(100% - 24px)); max-height: 45%;
|
|
@@ -2073,6 +2542,8 @@ var KIND_LABEL = {
|
|
|
2073
2542
|
import: "import",
|
|
2074
2543
|
class: "class",
|
|
2075
2544
|
function: "fn",
|
|
2545
|
+
component: "component",
|
|
2546
|
+
state: "state",
|
|
2076
2547
|
variable: "var",
|
|
2077
2548
|
type: "type",
|
|
2078
2549
|
namespace: "ns",
|
|
@@ -2102,7 +2573,7 @@ var CodeCanvas = class {
|
|
|
2102
2573
|
this.blocksEl = el("div", "cc-blocks");
|
|
2103
2574
|
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2104
2575
|
this.svg.classList.add("cc-edges");
|
|
2105
|
-
setHTML(this.svg, `<defs>${["import", "class", "function", "variable", "type", "namespace", "statement"].map(
|
|
2576
|
+
setHTML(this.svg, `<defs>${["import", "class", "function", "component", "state", "variable", "type", "namespace", "statement"].map(
|
|
2106
2577
|
(k) => `<marker id="cc-arrow-${k}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="var(--k-${k})"/></marker>`
|
|
2107
2578
|
).join("")}</defs>`);
|
|
2108
2579
|
this.world.append(this.frameEl, this.blocksEl, this.svg);
|
|
@@ -2166,8 +2637,7 @@ var CodeCanvas = class {
|
|
|
2166
2637
|
diffCache = null;
|
|
2167
2638
|
/** Merge conflicts in the text. The canvas then shows its current side, marking the blocks they touch. */
|
|
2168
2639
|
conflicts = null;
|
|
2169
|
-
|
|
2170
|
-
removedBlockEls = [];
|
|
2640
|
+
ghostBlockEls = { removed: [], incoming: [] };
|
|
2171
2641
|
/** Errors from the host (`setDiagnostics`), shown instead of the project's type errors. */
|
|
2172
2642
|
hostDiagnostics = null;
|
|
2173
2643
|
renaming = null;
|
|
@@ -2549,25 +3019,10 @@ var CodeCanvas = class {
|
|
|
2549
3019
|
}
|
|
2550
3020
|
/** Blocks only the incoming side of a conflict has, drawn dashed where they would go. */
|
|
2551
3021
|
renderIncomingBlocks() {
|
|
2552
|
-
for (const incomingEl of this.incomingBlockEls) incomingEl.remove();
|
|
2553
|
-
this.incomingBlockEls = [];
|
|
2554
3022
|
const conflicts = this.conflicts;
|
|
2555
|
-
|
|
2556
|
-
if (!conflicts?.incomingOnly.length || !geometry) return;
|
|
3023
|
+
if (!conflicts?.incomingOnly.length || !this.geom) return this.drawGhostBlocks("incoming", [], null);
|
|
2557
3024
|
conflicts.geometry ??= computeGeometry(conflicts.incomingModel, this.layout, {}, { resolveOverlaps: false });
|
|
2558
|
-
|
|
2559
|
-
const incomingGeo = conflicts.geometry.geo.get(block.id);
|
|
2560
|
-
const parentGeo = block.parentId ? geometry.geo.get(block.parentId) : void 0;
|
|
2561
|
-
if (!incomingGeo || parentGeo && (parentGeo.hidden || parentGeo.collapsed)) continue;
|
|
2562
|
-
const x = parentGeo ? parentGeo.ax + M.pad + incomingGeo.x : incomingGeo.ax;
|
|
2563
|
-
const y = parentGeo ? parentGeo.ay + parentGeo.contentTop + incomingGeo.y : incomingGeo.ay;
|
|
2564
|
-
const incomingEl = el("div", `cc-block k-${block.kind} cc-conflict-incoming`);
|
|
2565
|
-
setHTML(incomingEl, this.blockHTML(block, { ...incomingGeo, collapsed: true }, "", "incoming"));
|
|
2566
|
-
Object.assign(incomingEl.style, { transform: `translate(${x}px,${y}px)`, width: incomingGeo.w + "px", height: incomingGeo.h + "px", zIndex: String(block.depth + 1) });
|
|
2567
|
-
incomingEl.title = `${block.name}: only in the incoming side of a merge conflict`;
|
|
2568
|
-
this.blocksEl.appendChild(incomingEl);
|
|
2569
|
-
this.incomingBlockEls.push(incomingEl);
|
|
2570
|
-
}
|
|
3025
|
+
this.drawGhostBlocks("incoming", conflicts.incomingOnly, conflicts.geometry);
|
|
2571
3026
|
}
|
|
2572
3027
|
/** A changed block's preview: its changed lines with a line of context, as many as fit. */
|
|
2573
3028
|
changedLinesPreview(diff, block) {
|
|
@@ -2582,24 +3037,26 @@ var CodeCanvas = class {
|
|
|
2582
3037
|
}
|
|
2583
3038
|
/** Removed blocks stay visible, in red, where they were in the base version. They can't be selected. */
|
|
2584
3039
|
renderRemovedBlocks(diff) {
|
|
2585
|
-
|
|
2586
|
-
this.removedBlockEls = [];
|
|
2587
|
-
const geometry = this.geom;
|
|
2588
|
-
if (!diff?.removed.length || !this.diffBase || !geometry) return;
|
|
3040
|
+
if (!diff?.removed.length || !this.diffBase || !this.geom) return this.drawGhostBlocks("removed", [], null);
|
|
2589
3041
|
this.diffBase.geometry ??= computeGeometry(diff.base, this.diffBase.layout ?? this.layout, {}, { resolveOverlaps: false });
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
3042
|
+
this.drawGhostBlocks("removed", diff.removed, this.diffBase.geometry);
|
|
3043
|
+
}
|
|
3044
|
+
/** Blocks of another version (removed, or incoming in a conflict): drawn, but not part of this file. */
|
|
3045
|
+
drawGhostBlocks(kind, ghosts, ghostGeometry) {
|
|
3046
|
+
for (const ghostEl of this.ghostBlockEls[kind]) ghostEl.remove();
|
|
3047
|
+
this.ghostBlockEls[kind] = [];
|
|
3048
|
+
if (!ghostGeometry || !this.geom || !this.view) return;
|
|
3049
|
+
const positions = placeGhostBlocks(ghosts, ghostGeometry, this.view, this.geom);
|
|
3050
|
+
for (const block of ghosts) {
|
|
3051
|
+
const position = positions.get(block.id);
|
|
3052
|
+
const ghostGeo = ghostGeometry.geo.get(block.id);
|
|
3053
|
+
if (!position || !ghostGeo) continue;
|
|
3054
|
+
const ghostEl = el("div", `cc-block k-${block.kind} ${kind === "removed" ? "cc-diff-removed" : "cc-conflict-incoming"}`);
|
|
3055
|
+
setHTML(ghostEl, this.blockHTML(block, { ...ghostGeo, collapsed: true }, "", kind));
|
|
3056
|
+
Object.assign(ghostEl.style, { transform: `translate(${position.x}px,${position.y}px)`, width: ghostGeo.w + "px", height: ghostGeo.h + "px", zIndex: String(block.depth + 1) });
|
|
3057
|
+
ghostEl.title = kind === "removed" ? `${block.name}: removed` : `${block.name}: only in the incoming side of a merge conflict`;
|
|
3058
|
+
this.blocksEl.appendChild(ghostEl);
|
|
3059
|
+
this.ghostBlockEls[kind].push(ghostEl);
|
|
2603
3060
|
}
|
|
2604
3061
|
}
|
|
2605
3062
|
/** Diagnostics grouped by the block that shows them: the innermost visible block containing each error. */
|
|
@@ -3378,7 +3835,8 @@ var CodeCanvas = class {
|
|
|
3378
3835
|
el("div", "cc-sep"),
|
|
3379
3836
|
...firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [],
|
|
3380
3837
|
button("Edit", () => this.openEditor(b.id)),
|
|
3381
|
-
button("Copy", () => this.copy(b.id))
|
|
3838
|
+
button("Copy", () => this.copy(b.id)),
|
|
3839
|
+
...this.languageLabelParts()
|
|
3382
3840
|
);
|
|
3383
3841
|
return this.renderRefs();
|
|
3384
3842
|
}
|
|
@@ -3409,8 +3867,15 @@ var CodeCanvas = class {
|
|
|
3409
3867
|
button("+", () => this.zoomBy(1.25))
|
|
3410
3868
|
);
|
|
3411
3869
|
}
|
|
3870
|
+
bar.append(...this.languageLabelParts());
|
|
3412
3871
|
this.renderRefs();
|
|
3413
3872
|
}
|
|
3873
|
+
languageLabelParts() {
|
|
3874
|
+
if (!this.view) return [];
|
|
3875
|
+
const label = el("span", "cc-language");
|
|
3876
|
+
label.textContent = this.view.language.label;
|
|
3877
|
+
return [el("div", "cc-sep"), label];
|
|
3878
|
+
}
|
|
3414
3879
|
toggleRefs() {
|
|
3415
3880
|
this.refsOpen = !this.refsOpen;
|
|
3416
3881
|
this.renderBar();
|
|
@@ -3450,7 +3915,7 @@ var CodeCanvas = class {
|
|
|
3450
3915
|
btn.style.setProperty("--k", `var(--k-${block?.kind ?? "statement"})`);
|
|
3451
3916
|
const name = block && model ? displayName(model, block) : blockId;
|
|
3452
3917
|
const label = isOtherFile ? `<span class="cc-file">${esc(fileName(path))} \u203A</span> ${esc(name)}` : esc(name);
|
|
3453
|
-
setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
|
|
3918
|
+
setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind === "render" ? "renders" : usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
|
|
3454
3919
|
btn.onclick = () => isOtherFile ? this.opts.onOpenBlock?.(path, blockId) : this.reveal(blockId);
|
|
3455
3920
|
btn.disabled = isOtherFile && !this.opts.onOpenBlock;
|
|
3456
3921
|
return btn;
|