@neocompose/cli 0.26.0 → 0.26.2

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/neo.mjs CHANGED
@@ -2143,16 +2143,28 @@ function complete(snapshot, position) {
2143
2143
  candidates = switchCaseCandidates;
2144
2144
  } else if (tail && (tail.kind === "punctuation" && tail.text === "." || tail.kind === "operator" && tail.text === "?.")) {
2145
2145
  const receiverTokens = expressionTokensBefore(tokens, tokens.length - 1);
2146
- const resolved = resolveChain(snapshot, receiverTokens, offset);
2147
- candidates = completionItemsForResolution(snapshot, resolved, word);
2146
+ if (receiverTokens.length === 0 || isContextualCompletionDot(snapshot.source.text, word.start)) {
2147
+ candidates = contextualEnumCompletionItems(
2148
+ snapshot,
2149
+ expectedTypeAt(snapshot, word.start),
2150
+ word
2151
+ );
2152
+ } else {
2153
+ const resolved = resolveChain(snapshot, receiverTokens, offset);
2154
+ candidates = completionItemsForResolution(snapshot, resolved, word);
2155
+ }
2148
2156
  } else if (tail?.kind === "identifier" && tail.text === NEOSCRIPT_CONSTRUCTOR_KEYWORD) {
2149
- candidates = constructorCompletionItems(snapshot, word);
2157
+ candidates = constructorCompletionItems(
2158
+ snapshot,
2159
+ word,
2160
+ expectedTypeAt(snapshot, word.start)
2161
+ );
2150
2162
  } else if (isTypePosition(tokens)) {
2151
2163
  candidates = typeCompletionItems(snapshot, word);
2152
2164
  } else if (isLambdaParameterPosition(tokens)) {
2153
2165
  candidates = [];
2154
2166
  } else {
2155
- candidates = topLevelCompletionItems(snapshot, offset, word);
2167
+ candidates = contextualCompletionItems(snapshot, offset, word);
2156
2168
  }
2157
2169
  return {
2158
2170
  isIncomplete: hasUnclosedConstruct(snapshot.lexed.tokens, offset),
@@ -2230,12 +2242,33 @@ function signatureHelp(snapshot, position) {
2230
2242
  if (call.kind === "constructor") {
2231
2243
  const type = resolution?.staticType;
2232
2244
  const constructor2 = type?.constructorSignature;
2233
- if (!type || !constructor2) return null;
2245
+ const declared = type?.declaredConstructors ?? [];
2246
+ if (!type) return null;
2234
2247
  const activeParameter2 = countTopLevelCommas(
2235
2248
  snapshot.source.text,
2236
2249
  call.openParenOffset + 1,
2237
2250
  offset
2238
2251
  );
2252
+ if (declared.length > 0) {
2253
+ return {
2254
+ signatures: declared.map((candidate) => ({
2255
+ label: `${type.name}(${candidate.parameters.map(
2256
+ (parameter4) => `${formatType(parameter4.type, snapshot.project)} ${parameter4.name}`
2257
+ ).join(", ")})`,
2258
+ ...candidate.documentation ? { documentation: candidate.documentation } : {},
2259
+ parameters: candidate.parameters.map((parameter4) => ({
2260
+ label: `${formatType(parameter4.type, snapshot.project)} ${parameter4.name}`,
2261
+ ...parameter4.documentation ? { documentation: parameter4.documentation } : {}
2262
+ }))
2263
+ })),
2264
+ activeSignature: 0,
2265
+ activeParameter: Math.min(
2266
+ activeParameter2,
2267
+ Math.max(0, (declared[0]?.parameters.length ?? 1) - 1)
2268
+ )
2269
+ };
2270
+ }
2271
+ if (!constructor2) return null;
2239
2272
  return {
2240
2273
  signatures: [
2241
2274
  {
@@ -2283,19 +2316,18 @@ function inlayHints(snapshot, range2) {
2283
2316
  const hints = [];
2284
2317
  for (const call of snapshot.parsed.calls) {
2285
2318
  if (call.kind !== "constructor") continue;
2286
- const constructor2 = snapshot.project.typeByName.get(
2287
- call.name
2288
- )?.constructorSignature;
2289
- if (!constructor2) continue;
2319
+ const type = snapshot.project.typeByName.get(call.name);
2320
+ const parameterNames = constructorParameterNames(type);
2321
+ if (!parameterNames) continue;
2290
2322
  for (let index = 0; index < call.argumentRanges.length; index++) {
2291
2323
  const argumentRange = call.argumentRanges[index];
2292
- const parameter4 = constructor2.parameters[index];
2293
- if (!argumentRange || !parameter4) continue;
2324
+ const parameterName = parameterNames[index];
2325
+ if (!argumentRange || !parameterName) continue;
2294
2326
  const position = firstNonWhitespacePosition(snapshot, argumentRange);
2295
2327
  if (range2 && !positionInRange(position, range2)) continue;
2296
2328
  hints.push({
2297
2329
  position,
2298
- label: `${parameter4.name}:`,
2330
+ label: `${parameterName}:`,
2299
2331
  kind: "parameter",
2300
2332
  paddingRight: true
2301
2333
  });
@@ -2303,6 +2335,22 @@ function inlayHints(snapshot, range2) {
2303
2335
  }
2304
2336
  return hints;
2305
2337
  }
2338
+ function constructorParameterNames(type) {
2339
+ const declared = type?.declaredConstructors ?? [];
2340
+ if (declared.length > 0) {
2341
+ const arity = Math.max(
2342
+ ...declared.map((constructor2) => constructor2.parameters.length)
2343
+ );
2344
+ return Array.from({ length: arity }, (_, index) => {
2345
+ const names = new Set(
2346
+ declared.map((constructor2) => constructor2.parameters[index]?.name)
2347
+ );
2348
+ const [name] = names;
2349
+ return names.size === 1 && name ? name : null;
2350
+ });
2351
+ }
2352
+ return type?.constructorSignature?.parameters.map((parameter4) => parameter4.name) ?? null;
2353
+ }
2306
2354
  function documentSymbols(snapshot) {
2307
2355
  const unitSymbols = snapshot.parsed.units.map((unit) => ({
2308
2356
  name: unit.kind,
@@ -2439,8 +2487,9 @@ function isValidNeoIdentifier(name) {
2439
2487
  function isValidRenameIdentifier(name) {
2440
2488
  return isValidNeoIdentifier(name);
2441
2489
  }
2442
- function topLevelCompletionItems(snapshot, offset, word) {
2490
+ function contextualCompletionItems(snapshot, offset, word) {
2443
2491
  const items = [];
2492
+ const expectedType = expectedTypeAt(snapshot, word.start);
2444
2493
  if (insideCatchBody(snapshot, offset)) {
2445
2494
  items.push(
2446
2495
  completion(
@@ -2454,7 +2503,12 @@ function topLevelCompletionItems(snapshot, offset, word) {
2454
2503
  )
2455
2504
  );
2456
2505
  }
2506
+ const statementStart = isStatementStart(snapshot.lexed.tokens, word.start);
2457
2507
  for (const keyword of NEOSCRIPT_KEYWORDS) {
2508
+ if (!statementStart && statementOnlyKeyword(keyword)) continue;
2509
+ if (!statementStart && expectedType !== null && !literalKeywordMatchesExpectedType(keyword, expectedType)) {
2510
+ continue;
2511
+ }
2458
2512
  items.push(
2459
2513
  completion(
2460
2514
  keyword,
@@ -2467,6 +2521,7 @@ function topLevelCompletionItems(snapshot, offset, word) {
2467
2521
  );
2468
2522
  }
2469
2523
  for (const snippet of NEOSCRIPT_STATEMENT_SNIPPETS) {
2524
+ if (!statementStart) continue;
2470
2525
  items.push(
2471
2526
  completion(
2472
2527
  snippet.label,
@@ -2479,60 +2534,282 @@ function topLevelCompletionItems(snapshot, offset, word) {
2479
2534
  )
2480
2535
  );
2481
2536
  }
2482
- items.push(
2483
- completion(
2484
- NEOSCRIPT_INFERRED_LOCAL_KEYWORD,
2485
- "snippet",
2486
- "var ${1:name} = ${0:value};",
2487
- "Inferred local declaration",
2488
- word,
2489
- snapshot,
2490
- "snippet"
2491
- )
2492
- );
2493
- items.push(
2494
- completion(
2495
- NEOSCRIPT_CONSTRUCTOR_KEYWORD,
2496
- "snippet",
2497
- "new ${1:ClassName}(${0})",
2498
- "Construct a Class value",
2499
- word,
2500
- snapshot,
2501
- "snippet"
2502
- )
2503
- );
2504
- for (const primitive3 of NEOSCRIPT_PRIMITIVE_TYPES) {
2537
+ if (statementStart) {
2505
2538
  items.push(
2506
2539
  completion(
2507
- primitive3,
2508
- "keyword",
2509
- primitive3,
2510
- "Primitive type",
2540
+ NEOSCRIPT_INFERRED_LOCAL_KEYWORD,
2541
+ "snippet",
2542
+ "var ${1:name} = ${0:value};",
2543
+ "Inferred local declaration",
2511
2544
  word,
2512
- snapshot
2545
+ snapshot,
2546
+ "snippet"
2547
+ )
2548
+ );
2549
+ }
2550
+ const expectedNamed = expectedType?.kind === "named" ? snapshot.project.typeById.get(expectedType.typeId) : void 0;
2551
+ if (expectedType === null || expectedNamed?.kind === "class" || expectedNamed?.kind === "interface") {
2552
+ items.push(
2553
+ completion(
2554
+ NEOSCRIPT_CONSTRUCTOR_KEYWORD,
2555
+ "snippet",
2556
+ "new ${1:ClassName}(${0})",
2557
+ "Construct a Class value",
2558
+ word,
2559
+ snapshot,
2560
+ "snippet"
2561
+ )
2562
+ );
2563
+ }
2564
+ if (statementStart && expectedType === null) {
2565
+ for (const primitive3 of NEOSCRIPT_PRIMITIVE_TYPES) {
2566
+ items.push(
2567
+ completion(
2568
+ primitive3,
2569
+ "keyword",
2570
+ primitive3,
2571
+ "Primitive type",
2572
+ word,
2573
+ snapshot
2574
+ )
2575
+ );
2576
+ }
2577
+ items.push(
2578
+ completion(
2579
+ "Dictionary",
2580
+ "snippet",
2581
+ "Dictionary<string, ${1:T}>",
2582
+ "Dictionary type",
2583
+ word,
2584
+ snapshot,
2585
+ "snippet"
2513
2586
  )
2514
2587
  );
2515
2588
  }
2516
- items.push(
2517
- completion(
2518
- "Dictionary",
2519
- "snippet",
2520
- "Dictionary<string, ${1:T}>",
2521
- "Dictionary type",
2522
- word,
2523
- snapshot,
2524
- "snippet"
2525
- )
2526
- );
2527
2589
  for (const symbol of scopeAt(snapshot, offset)) {
2590
+ if (expectedType !== null && !isNeoScriptTypeAssignable(
2591
+ symbol.returnType ?? symbol.type,
2592
+ expectedType,
2593
+ snapshot.project
2594
+ )) {
2595
+ continue;
2596
+ }
2528
2597
  items.push(symbolCompletion(symbol, word, snapshot));
2529
2598
  }
2530
- for (const type of snapshot.project.typeByName.values()) {
2531
- if (type.kind === "builtin" && type.name.startsWith("__")) continue;
2532
- items.push(typeCompletion(type, word, snapshot));
2599
+ if (statementStart && expectedType === null) {
2600
+ for (const type of snapshot.project.typeByName.values()) {
2601
+ if (type.kind === "builtin" && type.name.startsWith("__")) continue;
2602
+ items.push(typeCompletion(type, word, snapshot));
2603
+ }
2533
2604
  }
2534
2605
  return items;
2535
2606
  }
2607
+ function contextualEnumCompletionItems(snapshot, expected, word) {
2608
+ if (expected?.kind !== "named") return [];
2609
+ const type = snapshot.project.typeById.get(expected.typeId);
2610
+ if (type?.kind !== "enum") return [];
2611
+ return type.members.filter((member) => member.kind === "enumMember").map((member) => {
2612
+ const item = symbolCompletion(member, word, snapshot);
2613
+ return {
2614
+ ...item,
2615
+ label: `.${member.name}`,
2616
+ insertText: member.name,
2617
+ textEdit: {
2618
+ range: snapshot.source.range(word.start, word.end),
2619
+ newText: member.name
2620
+ }
2621
+ };
2622
+ });
2623
+ }
2624
+ function isContextualCompletionDot(text, wordStart) {
2625
+ if (text[wordStart - 1] !== ".") return false;
2626
+ let cursor = wordStart - 2;
2627
+ while (cursor >= 0 && /\s/.test(text[cursor] ?? "")) cursor--;
2628
+ const beforeDot = text[cursor];
2629
+ if (beforeDot !== void 0 && /[A-Za-z0-9_]/.test(beforeDot)) {
2630
+ const precedingWord = /[A-Za-z_][A-Za-z0-9_]*$/.exec(
2631
+ text.slice(0, cursor + 1)
2632
+ )?.[0];
2633
+ return precedingWord === "return" || precedingWord === "case";
2634
+ }
2635
+ return beforeDot === void 0 || !/[A-Za-z0-9_\])}]/.test(beforeDot);
2636
+ }
2637
+ function expectedTypeAt(snapshot, offset) {
2638
+ const call = [...snapshot.parsed.calls].filter(
2639
+ (candidate) => candidate.openParenOffset < offset && (candidate.closeParenOffset === void 0 || candidate.closeParenOffset >= offset)
2640
+ ).sort((left, right) => right.openParenOffset - left.openParenOffset)[0];
2641
+ if (call) {
2642
+ const reference2 = snapshot.parsed.references.find(
2643
+ (candidate) => candidate.range.start.line === call.nameRange.start.line && candidate.range.start.character === call.nameRange.start.character
2644
+ );
2645
+ const resolution = reference2 ? resolveReference(snapshot, reference2) : null;
2646
+ const index = countTopLevelCommas(
2647
+ snapshot.source.text,
2648
+ call.openParenOffset + 1,
2649
+ offset
2650
+ );
2651
+ const parameterType = call.kind === "constructor" ? constructorParameterTypeAt(resolution?.staticType, index, snapshot) : resolution?.symbol?.parameters?.[index]?.type;
2652
+ if (parameterType) {
2653
+ return expectedCollectionElementType(
2654
+ parameterType,
2655
+ snapshot.lexed.tokens,
2656
+ call.openParenOffset + 1,
2657
+ offset
2658
+ );
2659
+ }
2660
+ }
2661
+ const local = snapshot.parsed.locals.filter((candidate) => {
2662
+ const first = candidate.initializerTokens?.[0];
2663
+ return !candidate.inferred && first !== void 0 && first.start <= offset && offset <= snapshot.source.offsetAt(candidate.declarationRange.end);
2664
+ }).sort((left, right) => right.scopeStart - left.scopeStart)[0];
2665
+ if (local) {
2666
+ const first = local.initializerTokens?.[0];
2667
+ return expectedCollectionElementType(
2668
+ resolveDeclaredType(local, snapshot),
2669
+ snapshot.lexed.tokens,
2670
+ first?.start ?? offset,
2671
+ offset
2672
+ );
2673
+ }
2674
+ const tokens = significantTokensBefore(snapshot.lexed.tokens, offset);
2675
+ const boundary = lastStatementBoundary(tokens);
2676
+ for (let index = tokens.length - 1; index > boundary; index--) {
2677
+ const token = tokens[index];
2678
+ if (token?.kind !== "operator" || token.text !== "=") continue;
2679
+ const left = expressionTokensBefore(tokens, index);
2680
+ const resolution = resolveChain(snapshot, left, offset);
2681
+ if (resolution) {
2682
+ return expectedCollectionElementType(
2683
+ resolution.type,
2684
+ tokens,
2685
+ token.end,
2686
+ offset
2687
+ );
2688
+ }
2689
+ break;
2690
+ }
2691
+ for (let index = tokens.length - 1; index > boundary; index--) {
2692
+ const token = tokens[index];
2693
+ if (token?.kind !== "operator" || token.text !== "==" && token.text !== "!=") {
2694
+ continue;
2695
+ }
2696
+ const left = expressionTokensBefore(tokens, index);
2697
+ const type = resolveChain(snapshot, left, offset)?.type ?? inferExpressionType(left, snapshot, left[0]?.start ?? offset);
2698
+ if (!isUnknownExpectedType(type)) return type;
2699
+ }
2700
+ const futureComparison = expectedTypeFromComparisonRight(snapshot, offset);
2701
+ if (futureComparison) return futureComparison;
2702
+ const tail = tokens.at(-1);
2703
+ if (tail?.kind === "operator" && ["!", "&&", "||"].includes(tail.text) || tail?.kind === "punctuation" && tail.text === "(" && tokens.at(-2)?.kind === "keyword" && ["if", "while"].includes(tokens.at(-2)?.text ?? "")) {
2704
+ return { kind: "primitive", name: "bool" };
2705
+ }
2706
+ const throwToken = tokens.slice(boundary + 1).find((token) => token.kind === "keyword" && token.text === "throw");
2707
+ if (throwToken) return { kind: "primitive", name: "string" };
2708
+ const returnToken = tokens.slice(boundary + 1).find((token) => token.kind === "keyword" && token.text === "return");
2709
+ if (returnToken && snapshot.context.returnType) {
2710
+ return expectedCollectionElementType(
2711
+ snapshot.context.returnType,
2712
+ tokens,
2713
+ returnToken.end,
2714
+ offset
2715
+ );
2716
+ }
2717
+ return null;
2718
+ }
2719
+ function expectedTypeFromComparisonRight(snapshot, offset) {
2720
+ const tokens = snapshot.lexed.tokens.filter(
2721
+ (token) => token.kind !== "comment" && token.kind !== "eof"
2722
+ );
2723
+ let operatorIndex = -1;
2724
+ for (let index = 0; index < tokens.length; index++) {
2725
+ const token = tokens[index];
2726
+ if (!token || token.start < offset) continue;
2727
+ if (token.kind === "punctuation" && [";", "{", "}"].includes(token.text)) {
2728
+ break;
2729
+ }
2730
+ if (token.kind === "operator" && (token.text === "==" || token.text === "!=")) {
2731
+ operatorIndex = index;
2732
+ break;
2733
+ }
2734
+ }
2735
+ if (operatorIndex < 0) return null;
2736
+ const right = [];
2737
+ let depth = 0;
2738
+ for (let index = operatorIndex + 1; index < tokens.length; index++) {
2739
+ const token = tokens[index];
2740
+ if (!token) continue;
2741
+ if (token.kind === "punctuation" && ["(", "["].includes(token.text)) {
2742
+ depth++;
2743
+ } else if (token.kind === "punctuation" && [")", "]"].includes(token.text)) {
2744
+ if (depth === 0) break;
2745
+ depth--;
2746
+ }
2747
+ if (depth === 0 && token.kind === "punctuation" && [";", ",", "{"].includes(token.text)) {
2748
+ break;
2749
+ }
2750
+ right.push(token);
2751
+ }
2752
+ const type = inferExpressionType(right, snapshot, right[0]?.start ?? offset);
2753
+ return isUnknownExpectedType(type) ? null : type;
2754
+ }
2755
+ function isUnknownExpectedType(type) {
2756
+ return type.kind === "primitive" && type.name === "unknown";
2757
+ }
2758
+ function constructorParameterTypeAt(type, index, snapshot) {
2759
+ const declared = (type?.declaredConstructors ?? []).map((constructor2) => constructor2.parameters[index]?.type).filter(
2760
+ (candidate) => candidate !== void 0
2761
+ );
2762
+ if ((type?.declaredConstructors?.length ?? 0) === 0) {
2763
+ return type?.constructorSignature?.parameters[index]?.parameterType;
2764
+ }
2765
+ if (declared.length === 0) return void 0;
2766
+ const first = declared[0];
2767
+ return declared.every(
2768
+ (candidate) => isNeoScriptTypeAssignable(candidate, first, snapshot.project) && isNeoScriptTypeAssignable(first, candidate, snapshot.project)
2769
+ ) ? first : void 0;
2770
+ }
2771
+ function expectedCollectionElementType(initial, tokens, start, end) {
2772
+ let expected = initial;
2773
+ const stack = [];
2774
+ for (const token of tokens) {
2775
+ if (token.start < start || token.end > end || token.kind !== "punctuation") {
2776
+ continue;
2777
+ }
2778
+ if (token.text === "[") {
2779
+ stack.push(expected);
2780
+ if (expected.kind === "list" || expected.kind === "set") {
2781
+ expected = expected.elementType;
2782
+ }
2783
+ } else if (token.text === "]") {
2784
+ expected = stack.pop() ?? expected;
2785
+ }
2786
+ }
2787
+ return expected;
2788
+ }
2789
+ function lastStatementBoundary(tokens) {
2790
+ for (let index = tokens.length - 1; index >= 0; index--) {
2791
+ const token = tokens[index];
2792
+ if (token?.kind === "punctuation" && (token.text === ";" || token.text === "{" || token.text === "}")) {
2793
+ return index;
2794
+ }
2795
+ }
2796
+ return -1;
2797
+ }
2798
+ function isStatementStart(tokens, offset) {
2799
+ const significant = significantTokensBefore(tokens, offset);
2800
+ const tail = significant.at(-1);
2801
+ return tail === void 0 || tail.kind === "punctuation" && (tail.text === ";" || tail.text === "{" || tail.text === "}") || tail.kind === "punctuation" && tail.text === ":";
2802
+ }
2803
+ function statementOnlyKeyword(keyword) {
2804
+ return !["true", "false", "null", "is"].includes(keyword);
2805
+ }
2806
+ function literalKeywordMatchesExpectedType(keyword, expected) {
2807
+ if (keyword === "true" || keyword === "false") {
2808
+ return expected.kind === "primitive" && expected.name === "bool";
2809
+ }
2810
+ if (keyword === "null") return expected.nullable === true;
2811
+ return false;
2812
+ }
2536
2813
  function catchFilterCompletionItems(snapshot, offset, word) {
2537
2814
  const prefix = snapshot.source.text.slice(0, offset);
2538
2815
  if (!/\bcatch\s*\(\s*string\s+[A-Za-z_][A-Za-z0-9_]*\s*\)\s*$/.test(prefix)) {
@@ -2682,16 +2959,24 @@ function typeCompletionItems(snapshot, word) {
2682
2959
  }
2683
2960
  return items;
2684
2961
  }
2685
- function constructorCompletionItems(snapshot, word) {
2962
+ function constructorCompletionItems(snapshot, word, expected = null) {
2686
2963
  return [...snapshot.project.typeByName.values()].flatMap((type) => {
2687
2964
  const signature = type.constructorSignature;
2688
- if (!signature) return [];
2965
+ const declared = type.declaredConstructors ?? [];
2966
+ if (!signature && declared.length === 0) return [];
2967
+ if (expected !== null && !isNeoScriptTypeAssignable(
2968
+ { kind: "named", typeId: type.id },
2969
+ expected,
2970
+ snapshot.project
2971
+ )) {
2972
+ return [];
2973
+ }
2689
2974
  const insertText = `${type.name}(`;
2690
2975
  return [
2691
2976
  {
2692
2977
  label: type.name,
2693
2978
  kind: "class",
2694
- detail: formatConstructorSignature(type.name, signature, snapshot),
2979
+ detail: declared.length > 0 ? `${type.name} \u2014 ${declared.length} declared constructor${declared.length === 1 ? "" : "s"}` : signature ? formatConstructorSignature(type.name, signature, snapshot) : type.name,
2695
2980
  ...type.documentation ? { documentation: type.documentation } : {},
2696
2981
  insertText,
2697
2982
  textEdit: {
@@ -2764,6 +3049,22 @@ function membersForType(snapshot, type) {
2764
3049
  return [];
2765
3050
  }
2766
3051
  function resolveReference(snapshot, reference2) {
3052
+ if (!reference2.member && (isTypeAnnotationReference(snapshot, reference2) || isConstructorTypeReference(snapshot, reference2))) {
3053
+ const type = snapshot.project.typeByName.get(reference2.name);
3054
+ if (type) {
3055
+ return {
3056
+ type: { kind: "named", typeId: type.id },
3057
+ staticType: type
3058
+ };
3059
+ }
3060
+ }
3061
+ if (reference2.member && isContextualCompletionDot(snapshot.source.text, reference2.start)) {
3062
+ const contextual = resolveContextualExpectedEnumReference(
3063
+ snapshot,
3064
+ reference2
3065
+ );
3066
+ if (contextual) return contextual;
3067
+ }
2767
3068
  const tokens = snapshot.lexed.tokens.filter(
2768
3069
  (token) => token.kind !== "comment" && token.kind !== "eof" && token.end <= reference2.end
2769
3070
  );
@@ -2814,6 +3115,16 @@ function resolveContextualSwitchEnumReference(snapshot, reference2) {
2814
3115
  );
2815
3116
  return symbol ? { type: symbol.type, symbol } : null;
2816
3117
  }
3118
+ function resolveContextualExpectedEnumReference(snapshot, reference2) {
3119
+ const expected = expectedTypeAt(snapshot, reference2.start);
3120
+ if (expected?.kind !== "named") return null;
3121
+ const type = snapshot.project.typeById.get(expected.typeId);
3122
+ if (type?.kind !== "enum") return null;
3123
+ const symbol = type.members.find(
3124
+ (member) => member.kind === "enumMember" && member.name === reference2.name
3125
+ );
3126
+ return symbol ? { type: symbol.type, symbol } : null;
3127
+ }
2817
3128
  function switchCaseEnumContext(snapshot, offset) {
2818
3129
  for (const statement of snapshot.parsed.switches) {
2819
3130
  for (const section of statement.sections) {
@@ -3067,6 +3378,19 @@ function scopeAt(snapshot, offset) {
3067
3378
  )
3068
3379
  );
3069
3380
  }
3381
+ const declaringType = snapshot.context.declaringType;
3382
+ if (snapshot.context.implicitMemberAccess === true && declaringType?.kind === "named") {
3383
+ const owner = snapshot.project.typeById.get(declaringType.typeId);
3384
+ if (owner) {
3385
+ for (const member of owner.members) {
3386
+ if (snapshot.context.staticMember === true && member.static !== true) {
3387
+ continue;
3388
+ }
3389
+ if (!isMemberCompletionAccessible(snapshot, owner, member)) continue;
3390
+ result.push(withScope(member, 0, snapshot.source.text.length));
3391
+ }
3392
+ }
3393
+ }
3070
3394
  if (effectiveDocumentKind(snapshot, offset) === "setter" && snapshot.context.returnType) {
3071
3395
  result.push(
3072
3396
  withScope(
@@ -3903,6 +4227,11 @@ function isTypeAnnotationReference(snapshot, reference2) {
3903
4227
  (local) => local.typeTokens.some((token) => token.start === reference2.start)
3904
4228
  );
3905
4229
  }
4230
+ function isConstructorTypeReference(snapshot, reference2) {
4231
+ return snapshot.parsed.calls.some(
4232
+ (call) => call.kind === "constructor" && call.nameRange.start.line === reference2.range.start.line && call.nameRange.start.character === reference2.range.start.character
4233
+ );
4234
+ }
3906
4235
  var IDENTIFIER_PATTERN, RESERVED_NAMES;
3907
4236
  var init_analyzer = __esm({
3908
4237
  "../packages/neoscript-language/src/analyzer.ts"() {
@@ -10150,7 +10479,9 @@ var init_strict_resolver = __esm({
10150
10479
  pointer: operand.pointer
10151
10480
  },
10152
10481
  type: isNullable(operand.type) ? { ...operand.type, nullable: false } : operand.type,
10153
- ...operand.writability ? { writability: operand.writability } : {}
10482
+ ...operand.writability ? { writability: operand.writability } : {},
10483
+ ...operand.entryWritability ? { entryWritability: operand.entryWritability } : {},
10484
+ ...operand.writeRoot ? { writeRoot: operand.writeRoot } : {}
10154
10485
  };
10155
10486
  }
10156
10487
  case "coalesce": {
@@ -10999,7 +11330,7 @@ var init_strict_resolver = __esm({
10999
11330
  };
11000
11331
  }
11001
11332
  const writability = this.memberWritability(member, receiver);
11002
- const entryWritability = member.lookup?.multiselect === true ? this.lookupEntryWritability(member, receiver) : void 0;
11333
+ const entryWritability = member.lookup ? this.lookupEntryWritability(member, receiver) : void 0;
11003
11334
  const lookupWire = member.lookup?.multiselect === true && memberType2.kind === "set" ? {
11004
11335
  type: 9 /* Lookup */,
11005
11336
  required: !isNullable(memberType2),
@@ -11094,12 +11425,9 @@ var init_strict_resolver = __esm({
11094
11425
  }
11095
11426
  if (!member.writable) return void 0;
11096
11427
  if (!member.lookup) {
11097
- return member.writability ? toWritability(member.writability) : receiver.writability ?? "save" /* Save */;
11428
+ return member.writability ? toWritability(member.writability) : receiver.entryWritability ?? receiver.writability ?? "save" /* Save */;
11098
11429
  }
11099
- if (member.lookup.multiselect) {
11100
- return member.writability ? toWritability(member.writability) : receiver.writability ?? "save" /* Save */;
11101
- }
11102
- return this.lookupEntryWritability(member, receiver);
11430
+ return member.writability ? toWritability(member.writability) : receiver.writability ?? "save" /* Save */;
11103
11431
  }
11104
11432
  lookupEntryWritability(member, receiver) {
11105
11433
  if (!member.lookup) return "readOnly" /* ReadOnly */;
@@ -21593,6 +21921,7 @@ function compileProjectSourceBodies(documents) {
21593
21921
  const graph = buildProjectGraph(documents);
21594
21922
  const projectIndex = createProjectIndex(graph.project);
21595
21923
  const bodies = [];
21924
+ const contexts = [];
21596
21925
  const diagnostics = [];
21597
21926
  for (const [uri, document] of documents) {
21598
21927
  if (document.kind !== "definition") continue;
@@ -21637,6 +21966,7 @@ function compileProjectSourceBodies(documents) {
21637
21966
  },
21638
21967
  projectIndex,
21639
21968
  bodies,
21969
+ contexts,
21640
21970
  diagnostics
21641
21971
  });
21642
21972
  }
@@ -21672,6 +22002,7 @@ function compileProjectSourceBodies(documents) {
21672
22002
  },
21673
22003
  projectIndex,
21674
22004
  bodies,
22005
+ contexts,
21675
22006
  diagnostics
21676
22007
  });
21677
22008
  }
@@ -21712,6 +22043,7 @@ function compileProjectSourceBodies(documents) {
21712
22043
  },
21713
22044
  projectIndex,
21714
22045
  bodies,
22046
+ contexts,
21715
22047
  diagnostics
21716
22048
  });
21717
22049
  continue;
@@ -21736,15 +22068,24 @@ function compileProjectSourceBodies(documents) {
21736
22068
  context: { ...baseContext2, kind: unit },
21737
22069
  projectIndex,
21738
22070
  bodies,
22071
+ contexts,
21739
22072
  diagnostics
21740
22073
  });
21741
22074
  }
21742
22075
  }
21743
22076
  }
21744
22077
  }
21745
- return { project: graph.project, bodies, diagnostics };
22078
+ return { project: graph.project, bodies, contexts, diagnostics };
21746
22079
  }
21747
22080
  function compileBody(args) {
22081
+ args.contexts.push({
22082
+ uri: args.uri,
22083
+ ownerName: args.owner.declaration.name,
22084
+ memberName: args.memberName,
22085
+ unit: args.unit,
22086
+ range: args.range,
22087
+ context: args.context
22088
+ });
21748
22089
  const source = new SourceText(args.document.sourceText);
21749
22090
  const start = source.offsetAt(args.range.start);
21750
22091
  const end = source.offsetAt(args.range.end);
@@ -24064,6 +24405,7 @@ function analyzeNeoProjectSources(inputs, parsedDocuments = /* @__PURE__ */ new
24064
24405
  symbols,
24065
24406
  project: bodyCompilation.project,
24066
24407
  compiledBodies: bodyCompilation.bodies,
24408
+ bodyContexts: bodyCompilation.contexts,
24067
24409
  diagnostics
24068
24410
  };
24069
24411
  }
@@ -24891,9 +25233,25 @@ var init_quick_fixes = __esm({
24891
25233
 
24892
25234
  // ../packages/neoscript-language/src/project-source-language-features.ts
24893
25235
  function projectDiagnostics(analysis, uri) {
24894
- return analysis.diagnostics.filter((diagnostic) => diagnostic.uri === uri).map(({ uri: _uri, ...diagnostic }) => diagnostic);
25236
+ return analysis.diagnostics.filter((diagnostic) => diagnostic.uri === uri).map((diagnostic) => ({
25237
+ range: diagnostic.range,
25238
+ severity: diagnostic.severity,
25239
+ message: diagnostic.message,
25240
+ ...diagnostic.code === void 0 ? {} : { code: diagnostic.code },
25241
+ ...diagnostic.source === void 0 ? {} : { source: diagnostic.source },
25242
+ ...diagnostic.relatedInformation === void 0 ? {} : { relatedInformation: diagnostic.relatedInformation },
25243
+ ...diagnostic.suggestions === void 0 ? {} : { suggestions: diagnostic.suggestions }
25244
+ }));
24895
25245
  }
24896
25246
  function projectCompletions(analysis, document, position) {
25247
+ const body = projectBodySnapshotAt(analysis, document, position);
25248
+ if (body) return complete(body, position);
25249
+ const annotations = projectAnnotationCompletions(
25250
+ analysis,
25251
+ document,
25252
+ position
25253
+ );
25254
+ if (annotations) return { isIncomplete: false, items: annotations };
24897
25255
  const members = projectMemberCompletions(analysis, document, position);
24898
25256
  if (members) {
24899
25257
  return {
@@ -24978,13 +25336,126 @@ function projectCompletions(analysis, document, position) {
24978
25336
  if (initializerMembers) {
24979
25337
  return { isIncomplete: false, items: initializerMembers };
24980
25338
  }
25339
+ const contextualEnum = projectContextualEnumCompletions(
25340
+ analysis,
25341
+ document,
25342
+ position
25343
+ );
25344
+ if (contextualEnum) {
25345
+ return { isIncomplete: false, items: contextualEnum };
25346
+ }
25347
+ return projectFallbackCompletions(analysis, document, position);
25348
+ }
25349
+ function projectAnnotationCompletions(analysis, document, position) {
25350
+ const source = new SourceText(document.text);
25351
+ const offset = source.offsetAt(position);
25352
+ const word = projectWordRange(document.text, offset);
25353
+ if (document.text[word.start - 1] !== "@") return null;
25354
+ if (initializerRootAt(analysis, document, position)) return [];
25355
+ const names = projectAnnotationNamesAt(analysis, document, position);
25356
+ return names.map((name) => ({
25357
+ label: `@${name}`,
25358
+ kind: "snippet",
25359
+ insertText: name,
25360
+ textEdit: {
25361
+ range: source.range(word.start, word.end),
25362
+ newText: name
25363
+ }
25364
+ }));
25365
+ }
25366
+ function projectAnnotationNamesAt(analysis, document, position) {
25367
+ const source = analysis.documents.get(document.uri);
25368
+ if (!source) return [];
25369
+ if (document.languageId === "neoflow") {
25370
+ return ["id", "primary", "incomplete"];
25371
+ }
25372
+ const offset = new SourceText(document.text).offsetAt(position);
25373
+ const followingDeclaration = /^\s*(class|interface|enum)\b([^{}]*)/.exec(
25374
+ document.text.slice(offset)
25375
+ );
25376
+ if (followingDeclaration?.[1] === "class") {
25377
+ return [
25378
+ "id",
25379
+ "hidden",
25380
+ "settings",
25381
+ "storage",
25382
+ "relations",
25383
+ .../\bDialogue\b/.test(followingDeclaration[2] ?? "") ? ["incomplete"] : []
25384
+ ];
25385
+ }
25386
+ if (followingDeclaration) return ["id"];
25387
+ const containing = declarationContaining(source, position);
25388
+ if (containing?.kind === "enum") return ["id"];
25389
+ if (containing?.kind === "class" || containing?.kind === "interface") {
25390
+ if (positionCompare(position, containing.nameRange.start) < 0) {
25391
+ return projectDeclarationAnnotationNames(containing);
25392
+ }
25393
+ const member = containing.members.find(
25394
+ (candidate) => rangeContains(candidate.range, position) || positionCompare(position, candidate.range.start) <= 0
25395
+ );
25396
+ return projectMemberAnnotationNames(member);
25397
+ }
25398
+ const following = source.declarations.find(
25399
+ (declaration) => positionCompare(position, declaration.range.start) <= 0
25400
+ );
25401
+ if (following) return projectDeclarationAnnotationNames(following);
25402
+ return ["id"];
25403
+ }
25404
+ function projectDeclarationAnnotationNames(declaration) {
25405
+ if (declaration.kind !== "class") return ["id"];
25406
+ return [
25407
+ "id",
25408
+ "hidden",
25409
+ "settings",
25410
+ "storage",
25411
+ "relations",
25412
+ ...declaration.baseTypes.some((type) => type.name === "Dialogue") ? ["incomplete"] : []
25413
+ ];
25414
+ }
25415
+ function projectMemberAnnotationNames(member) {
25416
+ const names = ["id", "settings", "storage", "locked"];
25417
+ if (member?.type.name === "List") names.push("index", "column");
25418
+ return names;
25419
+ }
25420
+ function projectContextualEnumCompletions(analysis, document, position) {
25421
+ const source = new SourceText(document.text);
25422
+ const offset = source.offsetAt(position);
25423
+ const word = projectWordRange(document.text, offset);
25424
+ if (!isContextualDot(document.text, word.start)) return null;
25425
+ const typeName = constructionSiteAt(
25426
+ analysis,
25427
+ document,
25428
+ position
25429
+ )?.expectedTypeName;
25430
+ if (!typeName) return null;
25431
+ const type = analysis.project.types.find(
25432
+ (candidate) => candidate.kind === "enum" && candidate.name === typeName
25433
+ );
25434
+ if (!type) return null;
25435
+ return type.members.filter((member) => member.kind === "enumMember").map((member) => ({
25436
+ label: `.${member.name}`,
25437
+ kind: "enumMember",
25438
+ detail: type.name,
25439
+ insertText: member.name,
25440
+ textEdit: {
25441
+ range: source.range(word.start, word.end),
25442
+ newText: member.name
25443
+ },
25444
+ symbolId: member.id
25445
+ }));
25446
+ }
25447
+ function projectFallbackCompletions(analysis, document, position) {
24981
25448
  const items = /* @__PURE__ */ new Map();
24982
- for (const keyword of [
24983
- "@id",
24984
- "@settings",
24985
- "@storage",
24986
- "@relations",
24987
- "@primary",
25449
+ const construction = constructionSiteAt(analysis, document, position);
25450
+ const recoveredInitializerType = projectInitializerExpectedTypeNameAt(
25451
+ document,
25452
+ position
25453
+ );
25454
+ const expression = construction !== null || recoveredInitializerType !== null;
25455
+ const expectedTypeName = construction?.expectedTypeName ?? recoveredInitializerType;
25456
+ const afterNew = expression && projectAfterNewAt(document, position);
25457
+ const typePosition = projectTypePositionAt(analysis, document, position);
25458
+ const keywords = expression ? afterNew ? [] : ["new"] : typePosition ? [] : [
24988
25459
  "class",
24989
25460
  "interface",
24990
25461
  "enum",
@@ -24994,16 +25465,20 @@ function projectCompletions(analysis, document, position) {
24994
25465
  "override",
24995
25466
  "readonly",
24996
25467
  "virtual",
24997
- "static",
24998
- "new"
24999
- ]) {
25468
+ "static"
25469
+ ];
25470
+ for (const keyword of keywords) {
25000
25471
  items.set(keyword, {
25001
25472
  label: keyword,
25002
- kind: keyword.startsWith("@") ? "snippet" : "keyword",
25473
+ kind: "keyword",
25003
25474
  insertText: keyword
25004
25475
  });
25005
25476
  }
25006
- for (const typeName of NEOSCRIPT_BUILTIN_TYPES) {
25477
+ for (const typeName of [
25478
+ ...NEOSCRIPT_PRIMITIVE_TYPES,
25479
+ ...NEOSCRIPT_BUILTIN_TYPES
25480
+ ]) {
25481
+ if (expression) continue;
25007
25482
  items.set(typeName, {
25008
25483
  label: typeName,
25009
25484
  kind: "class",
@@ -25011,10 +25486,31 @@ function projectCompletions(analysis, document, position) {
25011
25486
  insertText: typeName
25012
25487
  });
25013
25488
  }
25489
+ const source = analysis.documents.get(document.uri);
25490
+ const owner = source ? declarationContaining(source, position)?.name : void 0;
25014
25491
  for (const symbol of analysis.symbols) {
25015
25492
  if (symbol.scopeRange && (symbol.location.uri !== document.uri || !rangeContains(symbol.scopeRange, position))) {
25016
25493
  continue;
25017
25494
  }
25495
+ const isType = symbol.kind === "class" || symbol.kind === "interface" || symbol.kind === "enum" || symbol.kind === "genericParameter";
25496
+ if (typePosition && !isType) continue;
25497
+ if (expression) {
25498
+ if (afterNew) {
25499
+ if (!isType || symbol.kind !== "class" || expectedTypeName !== null && !projectTypeNameAssignable(analysis, symbol.name, expectedTypeName)) {
25500
+ continue;
25501
+ }
25502
+ } else {
25503
+ const isValue = symbol.kind === "parameter" || symbol.kind === "flowBinding" || symbol.kind === "graphChild" || symbol.kind === "global" || symbol.kind === "member" && symbol.ownerName === owner;
25504
+ if (!isValue) continue;
25505
+ if (expectedTypeName !== null && (symbol.detail === void 0 || !projectTypeNameAssignable(
25506
+ analysis,
25507
+ symbol.detail,
25508
+ expectedTypeName
25509
+ ))) {
25510
+ continue;
25511
+ }
25512
+ }
25513
+ }
25018
25514
  const kind = projectCompletionKind(symbol);
25019
25515
  items.set(symbol.name, {
25020
25516
  label: symbol.name,
@@ -25026,6 +25522,168 @@ function projectCompletions(analysis, document, position) {
25026
25522
  }
25027
25523
  return { isIncomplete: false, items: [...items.values()] };
25028
25524
  }
25525
+ function projectInitializerExpectedTypeNameAt(document, position) {
25526
+ const prefix = documentPrefix(document.text, position);
25527
+ const line = prefix.slice(prefix.lastIndexOf("\n") + 1);
25528
+ return /^(?:\s*@\w+(?:\([^)]*\))?)*\s*(?:(?:public|protected|private|abstract|async|native|override|readonly|sealed|static|virtual)\s+)*([A-Za-z_][A-Za-z0-9_]*)(?:\s*<[^>]*>)?\s+[A-Za-z_][A-Za-z0-9_]*\s*=\s*[^;]*$/.exec(
25529
+ line
25530
+ )?.[1] ?? null;
25531
+ }
25532
+ function projectAfterNewAt(document, position) {
25533
+ const tokens = tokensBeforePosition(document.text, position);
25534
+ let cursor = tokens.length - 1;
25535
+ const current = tokens[cursor];
25536
+ const offset = new SourceText(document.text).offsetAt(position);
25537
+ if (current?.kind === "identifier" && current.text !== "new" && /[A-Za-z0-9_]$/.test(document.text.slice(0, offset))) {
25538
+ cursor--;
25539
+ }
25540
+ return tokens[cursor]?.text === "new";
25541
+ }
25542
+ function projectTypeNameAssignable(analysis, sourceName, targetName) {
25543
+ if (sourceName === targetName) return true;
25544
+ if (sourceName === "int" && (targetName === "float" || targetName === "decimal")) {
25545
+ return true;
25546
+ }
25547
+ const source = analysis.project.types.find(
25548
+ (type) => type.name === sourceName
25549
+ );
25550
+ const target = analysis.project.types.find(
25551
+ (type) => type.name === targetName
25552
+ );
25553
+ if (!source || !target) return false;
25554
+ const visited = /* @__PURE__ */ new Set();
25555
+ const derivesFrom = (typeId) => {
25556
+ if (typeId === target.id) return true;
25557
+ if (visited.has(typeId)) return false;
25558
+ visited.add(typeId);
25559
+ const type = analysis.project.types.find(
25560
+ (candidate) => candidate.id === typeId
25561
+ );
25562
+ return [
25563
+ ...type?.baseTypeIds ?? [],
25564
+ ...type?.interfaceTypeIds ?? []
25565
+ ].some(derivesFrom);
25566
+ };
25567
+ return derivesFrom(source.id);
25568
+ }
25569
+ function projectBodySnapshotAt(analysis, document, position) {
25570
+ const body = analysis.bodyContexts.find(
25571
+ (candidate) => candidate.uri === document.uri && rangeContains(candidate.range, position)
25572
+ );
25573
+ return body ? projectBodySnapshot(analysis, document, body) : null;
25574
+ }
25575
+ function projectBodySnapshot(analysis, document, body) {
25576
+ let snapshots = PROJECT_BODY_SNAPSHOT_CACHE.get(analysis);
25577
+ if (!snapshots) {
25578
+ snapshots = /* @__PURE__ */ new Map();
25579
+ PROJECT_BODY_SNAPSHOT_CACHE.set(analysis, snapshots);
25580
+ }
25581
+ const cached = snapshots.get(body);
25582
+ if (cached) return cached;
25583
+ const source = new SourceText(document.text);
25584
+ const start = source.offsetAt(body.range.start);
25585
+ const end = source.offsetAt(body.range.end);
25586
+ const text = maskOutsideProjectBody(document.text, start, end);
25587
+ const syntax = analyzeNeoScriptSyntax(text, body.context.kind);
25588
+ const context = { ...body.context, project: analysis.project };
25589
+ const snapshot = {
25590
+ uri: document.uri,
25591
+ source: syntax.lexed.source,
25592
+ lexed: syntax.lexed,
25593
+ parsed: syntax.parsed,
25594
+ context,
25595
+ project: buildProjectIndexWithBuiltins(context)
25596
+ };
25597
+ snapshots.set(body, snapshot);
25598
+ return snapshot;
25599
+ }
25600
+ function maskOutsideProjectBody(text, start, end) {
25601
+ let masked = "";
25602
+ for (let index = 0; index < text.length; index++) {
25603
+ const character = text[index] ?? "";
25604
+ masked += index >= start && index < end || character === "\n" || character === "\r" ? character : " ";
25605
+ }
25606
+ return masked;
25607
+ }
25608
+ function projectWordRange(text, offset) {
25609
+ let start = offset;
25610
+ let end = offset;
25611
+ while (start > 0 && /[A-Za-z0-9_]/.test(text[start - 1] ?? "")) start--;
25612
+ while (end < text.length && /[A-Za-z0-9_]/.test(text[end] ?? "")) end++;
25613
+ return { start, end };
25614
+ }
25615
+ function isContextualDot(text, wordStart) {
25616
+ if (text[wordStart - 1] !== ".") return false;
25617
+ let cursor = wordStart - 2;
25618
+ while (cursor >= 0 && /\s/.test(text[cursor] ?? "")) cursor--;
25619
+ const beforeDot = text[cursor];
25620
+ if (beforeDot !== void 0 && /[A-Za-z0-9_]/.test(beforeDot)) {
25621
+ const precedingWord = /[A-Za-z_][A-Za-z0-9_]*$/.exec(
25622
+ text.slice(0, cursor + 1)
25623
+ )?.[0];
25624
+ return precedingWord === "return" || precedingWord === "case";
25625
+ }
25626
+ return beforeDot === void 0 || !/[A-Za-z0-9_\])}]/.test(beforeDot);
25627
+ }
25628
+ function projectTypePositionAt(analysis, document, position) {
25629
+ const source = analysis.documents.get(document.uri);
25630
+ if (source && sourceTypeAt(source, position)) return true;
25631
+ const prefix = documentPrefix(document.text, position);
25632
+ const line = prefix.slice(prefix.lastIndexOf("\n") + 1);
25633
+ return /(?:^|[{:;,])\s*(?:(?:public|protected|private|abstract|async|native|override|readonly|sealed|static|virtual)\s+)*[A-Za-z_][A-Za-z0-9_]*(?:\s*<[^;={}()]*)?$/.test(
25634
+ line
25635
+ );
25636
+ }
25637
+ function sourceTypeAt(document, position) {
25638
+ const contains2 = (type) => {
25639
+ for (const argument2 of type.typeArguments) {
25640
+ const nested = contains2(argument2);
25641
+ if (nested) return nested;
25642
+ }
25643
+ return rangeContains(type.range, position) ? type : null;
25644
+ };
25645
+ const parameterType = (parameters) => {
25646
+ for (const parameter4 of parameters ?? []) {
25647
+ const match = contains2(parameter4.type);
25648
+ if (match) return match;
25649
+ }
25650
+ return null;
25651
+ };
25652
+ for (const declaration of document.declarations) {
25653
+ if (declaration.kind === "global") {
25654
+ const match = contains2(declaration.type);
25655
+ if (match) return match;
25656
+ continue;
25657
+ }
25658
+ if (declaration.kind === "enum") continue;
25659
+ for (const base of declaration.baseTypes) {
25660
+ const match = contains2(base);
25661
+ if (match) return match;
25662
+ }
25663
+ if (declaration.kind === "class") {
25664
+ const header = parameterType(declaration.headerParameters);
25665
+ if (header) return header;
25666
+ for (const generic of declaration.genericParameters) {
25667
+ if (!generic.constraint) continue;
25668
+ const match = contains2(generic.constraint);
25669
+ if (match) return match;
25670
+ }
25671
+ for (const constructor2 of declaration.constructors) {
25672
+ const match = parameterType(constructor2.parameters);
25673
+ if (match) return match;
25674
+ }
25675
+ }
25676
+ for (const member of declaration.members) {
25677
+ const memberType2 = contains2(member.type);
25678
+ if (memberType2) return memberType2;
25679
+ if (member.kind === "function") {
25680
+ const match = parameterType(member.parameters);
25681
+ if (match) return match;
25682
+ }
25683
+ }
25684
+ }
25685
+ return null;
25686
+ }
25029
25687
  function projectMemberCompletions(analysis, document, position) {
25030
25688
  const tokens = projectTokens(document).filter(
25031
25689
  (token) => token.kind !== "eof" && token.kind !== "comment" && positionCompare(token.range.start, position) <= 0
@@ -25263,6 +25921,8 @@ function activeNamedArgument(text, position) {
25263
25921
  return /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*\.[A-Za-z0-9_]*$/.exec(prefix)?.[1] ?? null;
25264
25922
  }
25265
25923
  function projectHover(analysis, document, position) {
25924
+ const body = projectBodySnapshotAt(analysis, document, position);
25925
+ if (body) return hover(body, position);
25266
25926
  const resolved = projectSymbolAt(analysis, document, position);
25267
25927
  if (!resolved) return null;
25268
25928
  const owner = resolved.symbol.ownerName ? ` on \`${resolved.symbol.ownerName}\`` : "";
@@ -25310,6 +25970,8 @@ Construct with \`new(${parameters})\` \u2014 this class declares a required cons
25310
25970
  ${lines.join("\n\n")}`;
25311
25971
  }
25312
25972
  function projectDefinition(analysis, document, position) {
25973
+ const body = projectBodySnapshotAt(analysis, document, position);
25974
+ if (body) return definition(body, position);
25313
25975
  const resolved = projectSymbolAt(analysis, document, position);
25314
25976
  return resolved ? [resolved.symbol.location] : [];
25315
25977
  }
@@ -25376,6 +26038,8 @@ function requiredDestinationEdit(document, initializer, destination) {
25376
26038
  return open && close ? trailingBlockReturnEdit(document.text, open, close, destination) : null;
25377
26039
  }
25378
26040
  function projectSignatureHelp(analysis, document, position) {
26041
+ const body = projectBodySnapshotAt(analysis, document, position);
26042
+ if (body) return signatureHelp(body, position);
25379
26043
  const tokens = tokensBeforePosition(document.text, position);
25380
26044
  const openIndex = activeCallOpenIndex(tokens, position);
25381
26045
  if (openIndex < 0) return null;
@@ -25437,6 +26101,9 @@ function projectSignatureHelp(analysis, document, position) {
25437
26101
  };
25438
26102
  }
25439
26103
  function projectInlayHints(analysis, document, range2) {
26104
+ const bodyContexts = analysis.bodyContexts.filter(
26105
+ (body) => body.uri === document.uri
26106
+ );
25440
26107
  const tokens = lex(document.text).tokens.filter(
25441
26108
  (token) => token.kind !== "comment" && token.kind !== "eof"
25442
26109
  );
@@ -25451,6 +26118,9 @@ function projectInlayHints(analysis, document, range2) {
25451
26118
  if (!callee || callee.kind !== "identifier" && callee.kind !== "type") {
25452
26119
  continue;
25453
26120
  }
26121
+ if (bodyContexts.some((body) => rangeContains(body.range, callee.range.start))) {
26122
+ continue;
26123
+ }
25454
26124
  if (!open || open.text !== "(") continue;
25455
26125
  const beforeCallee = tokens[index - 1];
25456
26126
  if (beforeCallee?.text === "@") continue;
@@ -25477,6 +26147,14 @@ function projectInlayHints(analysis, document, range2) {
25477
26147
  });
25478
26148
  }
25479
26149
  }
26150
+ for (const body of bodyContexts) {
26151
+ const snapshot = projectBodySnapshot(analysis, document, body);
26152
+ hints.push(
26153
+ ...inlayHints(snapshot, range2).filter(
26154
+ (hint) => rangeContains(body.range, hint.position)
26155
+ )
26156
+ );
26157
+ }
25480
26158
  return hints;
25481
26159
  }
25482
26160
  function projectCallParameterNames(analysis, document, callee, beforeCallee) {
@@ -25678,8 +26356,14 @@ function symbolsDeclaredAt(index, uri, range2) {
25678
26356
  function projectSemanticTokens(document, analysis) {
25679
26357
  const index = buildProjectSymbolIndex(analysis.symbols);
25680
26358
  const tokens = projectTokens(document);
25681
- return tokens.flatMap((token, tokenIndex) => {
26359
+ const bodyContexts = analysis.bodyContexts.filter(
26360
+ (body) => body.uri === document.uri
26361
+ );
26362
+ const result = tokens.flatMap((token, tokenIndex) => {
25682
26363
  if (token.kind === "eof" || token.kind === "error") return [];
26364
+ if (bodyContexts.some((body) => rangeContains(body.range, token.range.start))) {
26365
+ return [];
26366
+ }
25683
26367
  const declaration = symbolsDeclaredAt(index, document.uri, token.range)[0];
25684
26368
  const resolved = token.kind === "identifier" ? indexedProjectSymbolAt(analysis, document, token.range.start, index)?.symbol : void 0;
25685
26369
  let type;
@@ -25706,6 +26390,17 @@ function projectSemanticTokens(document, analysis) {
25706
26390
  }
25707
26391
  ];
25708
26392
  });
26393
+ for (const body of bodyContexts) {
26394
+ const snapshot = projectBodySnapshot(analysis, document, body);
26395
+ result.push(
26396
+ ...semanticTokens(snapshot).filter(
26397
+ (token) => rangeContains(body.range, token.range.start)
26398
+ )
26399
+ );
26400
+ }
26401
+ return result.sort(
26402
+ (left, right) => positionCompare(left.range.start, right.range.start)
26403
+ );
25709
26404
  }
25710
26405
  function isProjectContextualKeyword(tokens, tokenIndex) {
25711
26406
  const token = tokens[tokenIndex];
@@ -25730,6 +26425,17 @@ function indexedProjectSymbolAt(analysis, document, position, index) {
25730
26425
  if (exact.length === 1) return { token, symbol: exact[0] };
25731
26426
  const candidates = index.byName.get(token.text) ?? [];
25732
26427
  if (candidates.length === 0) return null;
26428
+ const sourceTokens = projectTokens(document);
26429
+ const tokenIndex = sourceTokens.findIndex(
26430
+ (candidate) => candidate.start === token.start && candidate.end === token.end
26431
+ );
26432
+ const source = analysis.documents.get(document.uri);
26433
+ if (source && sourceTypeAt(source, token.range.start) || sourceTokens[tokenIndex - 1]?.text === "new") {
26434
+ const types = candidates.filter(
26435
+ (symbol) => symbol.kind === "class" || symbol.kind === "interface" || symbol.kind === "enum" || symbol.kind === "genericParameter"
26436
+ );
26437
+ if (types.length === 1) return { token, symbol: types[0] };
26438
+ }
25733
26439
  const scoped = candidates.filter(
25734
26440
  (symbol) => symbol.scopeRange && symbol.location.uri === document.uri && rangeContains(symbol.scopeRange, token.range.start)
25735
26441
  ).sort(
@@ -25741,10 +26447,6 @@ function indexedProjectSymbolAt(analysis, document, position, index) {
25741
26447
  return { token, symbol: visibleCandidates[0] };
25742
26448
  }
25743
26449
  if (visibleCandidates.length === 0) return null;
25744
- const sourceTokens = projectTokens(document);
25745
- const tokenIndex = sourceTokens.findIndex(
25746
- (candidate) => candidate.start === token.start && candidate.end === token.end
25747
- );
25748
26450
  if (sourceTokens[tokenIndex + 1]?.text === "=" && sourceTokens[tokenIndex + 2]?.text !== "=") {
25749
26451
  const constructed = constructionMemberSymbol(
25750
26452
  analysis,
@@ -25754,7 +26456,6 @@ function indexedProjectSymbolAt(analysis, document, position, index) {
25754
26456
  );
25755
26457
  if (constructed) return { token, symbol: constructed };
25756
26458
  }
25757
- const source = analysis.documents.get(document.uri);
25758
26459
  const owner = source ? declarationContaining(source, token.range.start)?.name : void 0;
25759
26460
  if (owner) {
25760
26461
  const owned = visibleCandidates.filter(
@@ -26273,10 +26974,11 @@ function projectFunctionsNamed(analysis, name) {
26273
26974
  }
26274
26975
  return result;
26275
26976
  }
26276
- var CONSTRUCTION_INDEX_CACHE;
26977
+ var PROJECT_BODY_SNAPSHOT_CACHE, CONSTRUCTION_INDEX_CACHE;
26277
26978
  var init_project_source_language_features = __esm({
26278
26979
  "../packages/neoscript-language/src/project-source-language-features.ts"() {
26279
26980
  "use strict";
26981
+ init_analyzer();
26280
26982
  init_lexer();
26281
26983
  init_language_spec();
26282
26984
  init_project_source_semantics();
@@ -26284,6 +26986,8 @@ var init_project_source_language_features = __esm({
26284
26986
  init_project_source_tokens();
26285
26987
  init_quick_fixes();
26286
26988
  init_source_text();
26989
+ init_syntax();
26990
+ PROJECT_BODY_SNAPSHOT_CACHE = /* @__PURE__ */ new WeakMap();
26287
26991
  CONSTRUCTION_INDEX_CACHE = /* @__PURE__ */ new WeakMap();
26288
26992
  }
26289
26993
  });
@@ -42908,6 +43612,7 @@ function evaluateLiteralContainer(args) {
42908
43612
  }
42909
43613
  return {
42910
43614
  literal: literal2,
43615
+ ...evaluated.genericBindings === void 0 ? {} : { genericBindings: evaluated.genericBindings },
42911
43616
  ...evaluated.provisionalRootId === void 0 ? {} : { provisionalRootId: evaluated.provisionalRootId },
42912
43617
  ...evaluated.existingValueRow === void 0 ? {} : { existingValueRow: evaluated.existingValueRow }
42913
43618
  };
@@ -42930,6 +43635,9 @@ function buildDefaultMemberValue(args) {
42930
43635
  return evaluated.existingValueRow;
42931
43636
  }
42932
43637
  const value2 = buildNewValue(args.projectId, evaluated.literal);
43638
+ if (evaluated.genericBindings !== void 0) {
43639
+ value2.genericBindings = { ...evaluated.genericBindings };
43640
+ }
42933
43641
  if (evaluated.provisionalRootId !== void 0) {
42934
43642
  retargetDelegateReceiverValueIds(
42935
43643
  [value2, ...args.createdValues],
@@ -43316,6 +44024,9 @@ function cloneDefaultValueForMember(args) {
43316
44024
  return evaluated.existingValueRow;
43317
44025
  }
43318
44026
  const evaluatedRow = buildNewValue(args.projectId, evaluated.literal);
44027
+ if (evaluated.genericBindings !== void 0) {
44028
+ evaluatedRow.genericBindings = { ...evaluated.genericBindings };
44029
+ }
43319
44030
  if (evaluated.provisionalRootId !== void 0) {
43320
44031
  retargetDelegateReceiverValueIds(
43321
44032
  [evaluatedRow, ...args.createdValues],
@@ -50450,15 +51161,14 @@ function commentEndIndex(source, index) {
50450
51161
  return null;
50451
51162
  }
50452
51163
  function stripInitializerComments(source) {
50453
- const lines = [{ text: "", removedComment: false }];
51164
+ const lines = [{ parts: [], removedComment: false }];
50454
51165
  let quote6 = null;
50455
51166
  let escaped = false;
50456
51167
  let index = 0;
51168
+ let segmentStart = 0;
50457
51169
  while (index < source.length) {
50458
51170
  const character = source[index];
50459
- const line = lines[lines.length - 1];
50460
51171
  if (quote6 !== null) {
50461
- line.text += character;
50462
51172
  if (escaped) escaped = false;
50463
51173
  else if (character === "\\") escaped = true;
50464
51174
  else if (character === quote6) quote6 = null;
@@ -50467,20 +51177,34 @@ function stripInitializerComments(source) {
50467
51177
  }
50468
51178
  const afterComment = commentEndIndex(source, index);
50469
51179
  if (afterComment !== null) {
51180
+ const line = lines[lines.length - 1];
51181
+ if (segmentStart < index) {
51182
+ line.parts.push(source.slice(segmentStart, index));
51183
+ }
50470
51184
  line.removedComment = true;
50471
51185
  index = afterComment;
51186
+ segmentStart = index;
50472
51187
  continue;
50473
51188
  }
50474
51189
  if (character === "\n") {
50475
- lines.push({ text: "", removedComment: false });
51190
+ if (segmentStart < index) {
51191
+ lines[lines.length - 1].parts.push(source.slice(segmentStart, index));
51192
+ }
51193
+ lines.push({ parts: [], removedComment: false });
50476
51194
  index += 1;
51195
+ segmentStart = index;
50477
51196
  continue;
50478
51197
  }
50479
51198
  if (character === '"' || character === "'") quote6 = character;
50480
- line.text += character;
50481
51199
  index += 1;
50482
51200
  }
50483
- return lines.filter((line) => !line.removedComment || line.text.trim().length > 0).map((line) => line.text.trimEnd()).join("\n").trim();
51201
+ if (segmentStart < source.length) {
51202
+ lines[lines.length - 1].parts.push(source.slice(segmentStart));
51203
+ }
51204
+ return lines.map((line) => ({
51205
+ text: line.parts.join("").trimEnd(),
51206
+ removedComment: line.removedComment
51207
+ })).filter((line) => !line.removedComment || line.text.trim().length > 0).map((line) => line.text).join("\n").trim();
50484
51208
  }
50485
51209
  function normalizeBodySource(source) {
50486
51210
  const trimmed = source.trim();
@@ -56652,6 +57376,12 @@ var init_NeoScriptScope = __esm({
56652
57376
  setLocal(bindingId, value) {
56653
57377
  this.#bindings.set(bindingId, value);
56654
57378
  }
57379
+ resetInvocationLocals(parameterCount) {
57380
+ if (this.#bindings.size > parameterCount) this.#bindings.clear();
57381
+ if (this.#readonlyBindingErrors.size > 0) {
57382
+ this.#readonlyBindingErrors.clear();
57383
+ }
57384
+ }
56655
57385
  *keys() {
56656
57386
  const inherited = /* @__PURE__ */ new Set();
56657
57387
  if (this.parent !== null) {
@@ -61881,23 +62611,30 @@ function evalFunction(fn, scope, ctx) {
61881
62611
  const innerFn = fn.info.function;
61882
62612
  const isList = Array.isArray(c);
61883
62613
  const out = isList ? [] : {};
62614
+ const useFreshCallbackScope = ctx.__collectionCallbackStrategy === "fresh";
62615
+ const callbackScope = useFreshCallbackScope ? null : createChildScope(scope);
62616
+ const callbackOptions = useFreshCallbackScope ? null : evaluationOptions(ctx, false);
62617
+ const callbackParameterCount = innerFn.parameters.length === 1 || innerFn.parameters.length === 2 ? innerFn.parameters.length : 0;
61884
62618
  iterateCollection(c, ctx, (entry, key, valueId) => {
61885
62619
  consumeBudget(ctx, "workUnits", 1, "work unit");
61886
- const innerScope = pushParams(
61887
- scope,
61888
- innerFn.parameters,
61889
- [
61890
- isList ? key : key,
61891
- // first param: index for list, key for dict
61892
- entry
61893
- ],
61894
- isList
61895
- );
62620
+ const innerScope = callbackScope ?? pushParams(scope, innerFn.parameters, [key, entry], isList);
62621
+ if (callbackScope !== null) {
62622
+ callbackScope.resetInvocationLocals(callbackParameterCount);
62623
+ if (callbackParameterCount === 1) {
62624
+ callbackScope.setLocal(innerFn.parameters[0].id, entry);
62625
+ } else if (callbackParameterCount === 2) {
62626
+ callbackScope.setLocal(
62627
+ innerFn.parameters[0].id,
62628
+ isList ? Number(key) : String(key)
62629
+ );
62630
+ callbackScope.setLocal(innerFn.parameters[1].id, entry);
62631
+ }
62632
+ }
61896
62633
  const result = evalInstructions(
61897
62634
  innerFn.instructions,
61898
62635
  innerScope,
61899
62636
  ctx,
61900
- evaluationOptions(ctx, false)
62637
+ callbackOptions ?? evaluationOptions(ctx, false)
61901
62638
  );
61902
62639
  if (result.kind === "return" && result.value === true) {
61903
62640
  consumeBudget(
@@ -61922,24 +62659,35 @@ function evalFunction(fn, scope, ctx) {
61922
62659
  const innerFn = fn.info.function ?? null;
61923
62660
  const isList = Array.isArray(c);
61924
62661
  const sentinel = /* @__PURE__ */ Symbol("not-found");
62662
+ const useFreshCallbackScope = ctx.__collectionCallbackStrategy === "fresh";
62663
+ const callbackScope = innerFn === null || useFreshCallbackScope ? null : createChildScope(scope);
62664
+ const callbackOptions = innerFn === null || useFreshCallbackScope ? null : evaluationOptions(ctx, false);
62665
+ const callbackParameterCount = innerFn !== null && (innerFn.parameters.length === 1 || innerFn.parameters.length === 2) ? innerFn.parameters.length : 0;
61925
62666
  let found = sentinel;
61926
62667
  iterateCollection(c, ctx, (entry, key) => {
61927
- if (!innerFn) {
62668
+ if (innerFn === null) {
61928
62669
  found = entry;
61929
62670
  return 1 /* Break */;
61930
62671
  }
61931
62672
  consumeBudget(ctx, "workUnits", 1, "work unit");
61932
- const innerScope = pushParams(
61933
- scope,
61934
- innerFn.parameters,
61935
- [key, entry],
61936
- isList
61937
- );
62673
+ const innerScope = callbackScope ?? pushParams(scope, innerFn.parameters, [key, entry], isList);
62674
+ if (callbackScope !== null) {
62675
+ callbackScope.resetInvocationLocals(callbackParameterCount);
62676
+ if (callbackParameterCount === 1) {
62677
+ callbackScope.setLocal(innerFn.parameters[0].id, entry);
62678
+ } else if (callbackParameterCount === 2) {
62679
+ callbackScope.setLocal(
62680
+ innerFn.parameters[0].id,
62681
+ isList ? Number(key) : String(key)
62682
+ );
62683
+ callbackScope.setLocal(innerFn.parameters[1].id, entry);
62684
+ }
62685
+ }
61938
62686
  const result = evalInstructions(
61939
62687
  innerFn.instructions,
61940
62688
  innerScope,
61941
62689
  ctx,
61942
- evaluationOptions(ctx, false)
62690
+ callbackOptions ?? evaluationOptions(ctx, false)
61943
62691
  );
61944
62692
  if (result.kind === "return" && result.value === true) {
61945
62693
  found = entry;
@@ -61957,22 +62705,33 @@ function evalFunction(fn, scope, ctx) {
61957
62705
  }
61958
62706
  case "select" /* select */: {
61959
62707
  const c = evalPointer(fn.info.collectionPointer, scope, ctx);
62708
+ const useFreshCallbackScope = ctx.__collectionCallbackStrategy === "fresh";
62709
+ const callbackScope = useFreshCallbackScope ? null : createChildScope(scope);
62710
+ const callbackOptions = useFreshCallbackScope ? null : evaluationOptions(ctx, false);
61960
62711
  const innerFn = fn.info.function;
62712
+ const callbackParameterCount = innerFn.parameters.length === 1 || innerFn.parameters.length === 2 ? innerFn.parameters.length : 0;
61961
62713
  const isList = Array.isArray(c);
61962
62714
  const out = [];
61963
62715
  iterateCollection(c, ctx, (entry, key) => {
61964
62716
  consumeBudget(ctx, "workUnits", 1, "work unit");
61965
- const innerScope = pushParams(
61966
- scope,
61967
- innerFn.parameters,
61968
- [key, entry],
61969
- isList
61970
- );
62717
+ const innerScope = callbackScope ?? pushParams(scope, innerFn.parameters, [key, entry], isList);
62718
+ if (callbackScope !== null) {
62719
+ callbackScope.resetInvocationLocals(callbackParameterCount);
62720
+ if (callbackParameterCount === 1) {
62721
+ callbackScope.setLocal(innerFn.parameters[0].id, entry);
62722
+ } else if (callbackParameterCount === 2) {
62723
+ callbackScope.setLocal(
62724
+ innerFn.parameters[0].id,
62725
+ isList ? Number(key) : String(key)
62726
+ );
62727
+ callbackScope.setLocal(innerFn.parameters[1].id, entry);
62728
+ }
62729
+ }
61971
62730
  const result = evalInstructions(
61972
62731
  innerFn.instructions,
61973
62732
  innerScope,
61974
62733
  ctx,
61975
- evaluationOptions(ctx, false)
62734
+ callbackOptions ?? evaluationOptions(ctx, false)
61976
62735
  );
61977
62736
  if (result.kind === "return") {
61978
62737
  consumeBudget(
@@ -62168,6 +62927,43 @@ function validateConstructorFieldSlot(args) {
62168
62927
  requiredWithoutDefault: member.required && member.defaultValue == null
62169
62928
  };
62170
62929
  }
62930
+ function constructionGenericSlotsForMember(rawMember, ctx, instanceEnv, visitedMemberIds = /* @__PURE__ */ new Set()) {
62931
+ const member = substituteMember(
62932
+ rawMember,
62933
+ instanceEnv,
62934
+ ctx.vm.members
62935
+ );
62936
+ const memberId = Reflect.get(member, "id");
62937
+ if (typeof memberId === "string" && visitedMemberIds.has(memberId)) {
62938
+ return [];
62939
+ }
62940
+ const nextVisited = new Set(visitedMemberIds);
62941
+ if (typeof memberId === "string") nextVisited.add(memberId);
62942
+ if (isMemberClassBase(member)) {
62943
+ const classArguments2 = member.classArguments ?? {};
62944
+ return Object.keys(classArguments2).length === 0 ? [] : [{ classId: member.classId, classArguments: classArguments2 }];
62945
+ }
62946
+ if (!isMemberListBase(member) && !isMemberDictionaryBase(member)) return [];
62947
+ const entryMember = evalMemberById(ctx.vm, member.entryMemberId);
62948
+ if (entryMember === null) return [];
62949
+ return constructionGenericSlotsForMember(
62950
+ resolveMember2(entryMember, ctx.vm.members),
62951
+ ctx,
62952
+ instanceEnv,
62953
+ nextVisited
62954
+ );
62955
+ }
62956
+ function withConstructionGenericSlots(member, ctx, instanceEnv, evaluate) {
62957
+ const slots = constructionGenericSlotsForMember(member, ctx, instanceEnv);
62958
+ if (slots.length === 0) return evaluate();
62959
+ const previousSlots = ctx.__constructionGenericSlots;
62960
+ ctx.__constructionGenericSlots = [...previousSlots ?? [], ...slots];
62961
+ try {
62962
+ return evaluate();
62963
+ } finally {
62964
+ ctx.__constructionGenericSlots = previousSlots;
62965
+ }
62966
+ }
62171
62967
  function evaluateConstructorFields(descriptor, scope, ctx) {
62172
62968
  const schemaClass2 = descriptor.schemaClass;
62173
62969
  const evaluatedValues = descriptor.fields.map((validated) => {
@@ -62176,7 +62972,13 @@ function evaluateConstructorFields(descriptor, scope, ctx) {
62176
62972
  `Constructor field '${validated.schemaKey}' on '${schemaClass2.name}' has no call-site expression to evaluate.`
62177
62973
  );
62178
62974
  }
62179
- return evalPointer(validated.valuePointer, scope, ctx);
62975
+ const valuePointer = validated.valuePointer;
62976
+ return withConstructionGenericSlots(
62977
+ validated.member,
62978
+ ctx,
62979
+ descriptor.instanceEnv,
62980
+ () => evalPointer(valuePointer, scope, ctx)
62981
+ );
62180
62982
  });
62181
62983
  return descriptor.fields.map((validated, index) => {
62182
62984
  let value = evaluatedValues[index];
@@ -62214,6 +63016,14 @@ function syntheticConstructorMember(schemaClass2, classArguments2) {
62214
63016
  accessModifierKind: "public"
62215
63017
  };
62216
63018
  }
63019
+ function stampConstructedRootGenericBindings(root, descriptor) {
63020
+ const stamp = {};
63021
+ for (const parameter4 of descriptor.schemaClass.genericParams ?? []) {
63022
+ const binding = descriptor.instanceEnv.get(parameter4.id);
63023
+ if (binding?.kind === "member") stamp[parameter4.id] = binding.memberId;
63024
+ }
63025
+ if (Object.keys(stamp).length > 0) root.genericBindings = stamp;
63026
+ }
62217
63027
  function applyConstructorFields(args) {
62218
63028
  const {
62219
63029
  root,
@@ -62551,7 +63361,8 @@ function constructClassValueWithinFrame(descriptor, scope, ctx) {
62551
63361
  valuesByClassId: /* @__PURE__ */ new Map(),
62552
63362
  missingScopeReason: "no-constructor"
62553
63363
  },
62554
- classId
63364
+ classId,
63365
+ descriptor.instanceEnv
62555
63366
  ),
62556
63367
  constructorRoot: {
62557
63368
  providedSchemaKeys: new Set(
@@ -62560,6 +63371,7 @@ function constructClassValueWithinFrame(descriptor, scope, ctx) {
62560
63371
  }
62561
63372
  });
62562
63373
  root.classId = classId;
63374
+ stampConstructedRootGenericBindings(root, descriptor);
62563
63375
  if (typeof root.value !== "object" || root.value === null || Array.isArray(root.value)) {
62564
63376
  throw new Error(
62565
63377
  `Class constructor for '${schemaClass2.name}' produced a non-record root.`
@@ -62918,7 +63730,7 @@ function evaluateBaseInitializerFields(args) {
62918
63730
  return { validated, value };
62919
63731
  });
62920
63732
  }
62921
- function constructionInitEvaluator(ctx, createdValues, argumentScopes, constructedClassId) {
63733
+ function constructionInitEvaluator(ctx, createdValues, argumentScopes, constructedClassId, instanceEnv) {
62922
63734
  const indexes = constructorInitializerIndexes(ctx);
62923
63735
  const inheritanceChain = resolveInheritanceChain(
62924
63736
  constructedClassId,
@@ -62940,36 +63752,19 @@ function constructionInitEvaluator(ctx, createdValues, argumentScopes, construct
62940
63752
  };
62941
63753
  return (member, init, sourceValueId) => {
62942
63754
  const evaluate = (argumentValues = []) => {
62943
- if (!isMemberClassBase(member) || Object.keys(member.classArguments ?? {}).length === 0) {
62944
- return evaluateInitializerInContext(
62945
- init,
62946
- member,
62947
- ctx,
62948
- createdValues,
62949
- argumentValues,
62950
- sourceValueId ?? null
62951
- );
62952
- }
62953
- const previousSlots = ctx.__constructionGenericSlots;
62954
- ctx.__constructionGenericSlots = [
62955
- ...previousSlots ?? [],
62956
- {
62957
- classId: member.classId,
62958
- classArguments: member.classArguments ?? {}
62959
- }
62960
- ];
62961
- try {
62962
- return evaluateInitializerInContext(
63755
+ return withConstructionGenericSlots(
63756
+ member,
63757
+ ctx,
63758
+ instanceEnv,
63759
+ () => evaluateInitializerInContext(
62963
63760
  init,
62964
63761
  member,
62965
63762
  ctx,
62966
63763
  createdValues,
62967
63764
  argumentValues,
62968
63765
  sourceValueId ?? null
62969
- );
62970
- } finally {
62971
- ctx.__constructionGenericSlots = previousSlots;
62972
- }
63766
+ )
63767
+ );
62973
63768
  };
62974
63769
  if (init.compiled === void 0) {
62975
63770
  return evaluate();
@@ -63172,6 +63967,7 @@ function evaluateInitializerInContext(init, member, ctx, createdValues, argument
63172
63967
  return {
63173
63968
  value: result.value,
63174
63969
  classId: rootRow.classId ?? null,
63970
+ ...rootRow.genericBindings === void 0 ? {} : { genericBindings: { ...rootRow.genericBindings } },
63175
63971
  provisionalRootId: rootRow.id,
63176
63972
  ...constructorArgs === void 0 ? {} : { constructorArgs: structuredClone(constructorArgs) }
63177
63973
  };
@@ -63281,7 +64077,7 @@ function encodeRequiredConstructorArgument(args) {
63281
64077
  return adoptTracked(trackedId, "root");
63282
64078
  }
63283
64079
  if (args.typeInfo.type !== 6 /* List */ && args.typeInfo.type !== 5 /* Dictionary */) {
63284
- return args.value;
64080
+ return cloneConstructorLiteralValue(args.value);
63285
64081
  }
63286
64082
  if (args.value === null || args.value === void 0) return null;
63287
64083
  if (trackedId !== null) return adoptTracked(trackedId, "root");
@@ -63325,7 +64121,7 @@ function encodeRequiredConstructorArgument(args) {
63325
64121
  }
63326
64122
  return nested;
63327
64123
  }
63328
- return registerRow(value).id;
64124
+ return registerRow(cloneConstructorLiteralValue(value)).id;
63329
64125
  };
63330
64126
  if (collectionType.type === 6 /* List */) {
63331
64127
  if (!Array.isArray(args.value)) {
@@ -63407,7 +64203,8 @@ function constructDeclaredClassValueWithinFrame(descriptor, record3, info, scope
63407
64203
  ctx,
63408
64204
  createdValues,
63409
64205
  preparedArgumentScopes,
63410
- classId
64206
+ classId,
64207
+ descriptor.instanceEnv
63411
64208
  ),
63412
64209
  constructorRoot: {
63413
64210
  providedSchemaKeys: new Set(
@@ -63417,6 +64214,7 @@ function constructDeclaredClassValueWithinFrame(descriptor, record3, info, scope
63417
64214
  }
63418
64215
  });
63419
64216
  root.classId = classId;
64217
+ stampConstructedRootGenericBindings(root, descriptor);
63420
64218
  } catch (error) {
63421
64219
  if (error instanceof NSGetterRuntimeError) throw error;
63422
64220
  throw new NSGetterRuntimeError(
@@ -63917,10 +64715,10 @@ function cloneConstructorArgumentGraph(args) {
63917
64715
  return cloneRow(args.source, args.member, args.genericEnv);
63918
64716
  }
63919
64717
  function cloneConstructorLiteralValue(value) {
64718
+ if (typeof value !== "object" || value === null) return value;
63920
64719
  if (Array.isArray(value)) {
63921
64720
  return value.map((entry) => cloneConstructorLiteralValue(entry));
63922
64721
  }
63923
- if (typeof value !== "object" || value === null) return value;
63924
64722
  return Object.fromEntries(
63925
64723
  Object.entries(value).map(([key, entry]) => [
63926
64724
  key,
@@ -64752,6 +65550,7 @@ function evaluateMemberInitializer(args) {
64752
65550
  return {
64753
65551
  value: result.value,
64754
65552
  classId: rootRow.classId ?? null,
65553
+ ...rootRow.genericBindings === void 0 ? {} : { genericBindings: { ...rootRow.genericBindings } },
64755
65554
  provisionalRootId: rootRow.id,
64756
65555
  ...constructorArgs === void 0 ? {} : { constructorArgs: structuredClone(constructorArgs) }
64757
65556
  };
@@ -64811,6 +65610,7 @@ function materializeInitializerValue(args) {
64811
65610
  ...envelope,
64812
65611
  value: evaluated.value,
64813
65612
  ...evaluated.classId === null ? {} : { classId: evaluated.classId },
65613
+ ...evaluated.genericBindings === void 0 ? {} : { genericBindings: { ...evaluated.genericBindings } },
64814
65614
  ...evaluated.constructorArgs === void 0 ? {} : { constructorArgs: structuredClone(evaluated.constructorArgs) }
64815
65615
  };
64816
65616
  if (evaluated.provisionalRootId !== void 0) {
@@ -66456,6 +67256,7 @@ var init_project_document_read = __esm({
66456
67256
  });
66457
67257
 
66458
67258
  // src/workspace.ts
67259
+ import { createHash as createHash2 } from "node:crypto";
66459
67260
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
66460
67261
  import { dirname, join as join2, resolve } from "node:path";
66461
67262
  function recordStateKey(recordKind, recordId) {
@@ -66688,9 +67489,12 @@ function matchUnityScalarField(content, field) {
66688
67489
  function readWorkspaceState(root, options = {}) {
66689
67490
  const statePath = join2(root, NEO_STATE_DIR, NEO_STATE_FILE);
66690
67491
  if (!existsSync2(statePath)) {
67492
+ options.onSourceRead?.(null);
66691
67493
  return { records: {} };
66692
67494
  }
66693
- const parsed = JSON.parse(readFileSync2(statePath, "utf8"));
67495
+ const source = readFileSync2(statePath, "utf8");
67496
+ options.onSourceRead?.(source);
67497
+ const parsed = JSON.parse(source);
66694
67498
  if (typeof parsed !== "object" || parsed === null) {
66695
67499
  throw new Error(`"${statePath}" must contain a JSON object.`);
66696
67500
  }
@@ -66733,10 +67537,20 @@ function loadWorkspace(startDir, options = {}) {
66733
67537
  `No "${NEO_CONFIG_FILE}" found in "${startDir}" or any parent directory. Run "neo init" first.`
66734
67538
  );
66735
67539
  }
67540
+ let stateSourceSha256;
67541
+ const state = readWorkspaceState(root, {
67542
+ discardLegacyFormat2State: options.discardLegacyFormat2State,
67543
+ ...options.fingerprintStateSource === true ? {
67544
+ onSourceRead: (source) => {
67545
+ stateSourceSha256 = createHash2("sha256").update(source ?? "<missing>").digest("hex");
67546
+ }
67547
+ } : {}
67548
+ });
66736
67549
  return {
66737
67550
  root,
66738
67551
  config: readWorkspaceConfig(root),
66739
- state: readWorkspaceState(root, options)
67552
+ state,
67553
+ ...stateSourceSha256 === void 0 ? {} : { stateSourceSha256 }
66740
67554
  };
66741
67555
  }
66742
67556
  var NEO_CONFIG_FILE, NEO_STATE_DIR, NEO_STATE_FILE, CURRENT_FORMAT_VERSION;
@@ -72095,7 +72909,7 @@ var init_animation_clips = __esm({
72095
72909
 
72096
72910
  // src/project-source/materialized-construction-cache.ts
72097
72911
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
72098
- import { createHash as createHash2 } from "node:crypto";
72912
+ import { createHash as createHash3 } from "node:crypto";
72099
72913
  import { dirname as dirname2, join as join3 } from "node:path";
72100
72914
  function createMaterializedConstructionExpressionsV1(args) {
72101
72915
  const warm = args.useBuildCaches ? readMaterializedConstructionBuildCacheV1(args.root, args.state) : null;
@@ -72187,7 +73001,7 @@ function writeMaterializedConstructionBuildCacheV1(root, state, expressions) {
72187
73001
  renameSync(temporary, file);
72188
73002
  }
72189
73003
  function materializedConstructionStateFingerprint(state) {
72190
- const hash = createHash2("sha256");
73004
+ const hash = createHash3("sha256");
72191
73005
  hash.update(String(MATERIALIZED_CONSTRUCTION_BUILD_CACHE_REVISION));
72192
73006
  for (const [key, record3] of Object.entries(state.records).sort(
72193
73007
  ([left], [right]) => left.localeCompare(right)
@@ -73013,10 +73827,10 @@ var init_neo_script_recompile_scope = __esm({
73013
73827
  });
73014
73828
 
73015
73829
  // ../src/database/project-content-hash.ts
73016
- import { createHash as createHash3 } from "node:crypto";
73830
+ import { createHash as createHash4 } from "node:crypto";
73017
73831
  function hashCanonicalJson(value) {
73018
73832
  const canonicalJson = canonicalJsonStringify(value);
73019
- return createHash3("sha256").update(canonicalJson).digest("hex");
73833
+ return createHash4("sha256").update(canonicalJson).digest("hex");
73020
73834
  }
73021
73835
  var init_project_content_hash = __esm({
73022
73836
  "../src/database/project-content-hash.ts"() {
@@ -73083,7 +73897,7 @@ var init_project_fingerprint = __esm({
73083
73897
 
73084
73898
  // src/project-source/neoscript-build-cache.ts
73085
73899
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
73086
- import { createHash as createHash4 } from "node:crypto";
73900
+ import { createHash as createHash5 } from "node:crypto";
73087
73901
  import { dirname as dirname3, join as join4 } from "node:path";
73088
73902
  function loadOrBuildNeoScriptProjectV1(root, document) {
73089
73903
  const fingerprint = neoScriptProjectFingerprint(document);
@@ -73110,7 +73924,7 @@ function loadOrBuildNeoScriptProjectV1(root, document) {
73110
73924
  return project;
73111
73925
  }
73112
73926
  function neoScriptProjectFingerprint(document) {
73113
- return createHash4("sha256").update(
73927
+ return createHash5("sha256").update(
73114
73928
  canonicalJsonStringify(neoScriptCompilationProjectContract(document))
73115
73929
  ).digest("hex");
73116
73930
  }
@@ -74550,6 +75364,13 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
74550
75364
  const classesById = new Map(
74551
75365
  classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
74552
75366
  );
75367
+ const genericParamConstraintsById = new Map(
75368
+ classes.flatMap(
75369
+ (schemaClass2) => (schemaClass2.genericParams ?? []).map(
75370
+ (param) => [param.id, param.constraint]
75371
+ )
75372
+ )
75373
+ );
74553
75374
  const interfacesById = new Map(
74554
75375
  interfaces.map((neoInterface) => [neoInterface.id, neoInterface])
74555
75376
  );
@@ -75439,6 +76260,33 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
75439
76260
  }
75440
76261
  return { type: resolved.kind, required: required2 };
75441
76262
  };
76263
+ const constrainedGenericTypeInfo = (typeInfo) => {
76264
+ if (!isRecord7(typeInfo) || typeInfo.type !== 21 || typeof typeInfo.genericParamId !== "string") {
76265
+ return null;
76266
+ }
76267
+ const constraint = genericParamConstraintsById.get(typeInfo.genericParamId);
76268
+ if (!constraint) return null;
76269
+ return constraint.kind === "class" ? {
76270
+ type: 7,
76271
+ required: typeInfo.required === true,
76272
+ classId: constraint.classId
76273
+ } : {
76274
+ type: 8,
76275
+ required: typeInfo.required === true,
76276
+ enumId: constraint.enumId
76277
+ };
76278
+ };
76279
+ const constrainedGenericMemberTypeInfo = (member, schemaClass2) => {
76280
+ const resolved = substituteMemberForClass(member, schemaClass2);
76281
+ if (resolved.kind !== 21 || typeof resolved.genericParamId !== "string") {
76282
+ return null;
76283
+ }
76284
+ return constrainedGenericTypeInfo({
76285
+ type: 21,
76286
+ required: true,
76287
+ genericParamId: resolved.genericParamId
76288
+ });
76289
+ };
75442
76290
  const substituteTypeInfoForClass = (typeInfo, schemaClass2) => {
75443
76291
  if (!isRecord7(typeInfo)) return typeInfo;
75444
76292
  if (typeInfo.type === 21 && typeof typeInfo.genericParamId === "string") {
@@ -75509,7 +76357,8 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
75509
76357
  if (resolved.kind === 13 || resolved.kind === 23) {
75510
76358
  return "Schema member is a Function but the interface declares a property.";
75511
76359
  }
75512
- const implementationType = memberTypeInfo(member, schemaClass2);
76360
+ const storedType = memberTypeInfo(member, schemaClass2);
76361
+ const implementationType = declaration.settable === true ? storedType : storedType ? constrainedGenericTypeInfo(storedType) ?? storedType : constrainedGenericMemberTypeInfo(member, schemaClass2);
75513
76362
  if (!implementationType) return "Schema member type could not be resolved.";
75514
76363
  const matches = declaration.settable === true ? invariantTypeInfo(implementationType, declaration.typeInfo) : typeInfoAssignable(implementationType, declaration.typeInfo);
75515
76364
  if (!matches) return "Property type does not conform.";
@@ -92198,7 +93047,7 @@ var init_project_documents = __esm({
92198
93047
 
92199
93048
  // src/project-source/project-document-cache.ts
92200
93049
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
92201
- import { createHash as createHash5 } from "node:crypto";
93050
+ import { createHash as createHash6 } from "node:crypto";
92202
93051
  import { dirname as dirname4, join as join5 } from "node:path";
92203
93052
  function readProjectSourceAnalysisBuildCacheV4(root, sources) {
92204
93053
  try {
@@ -92322,7 +93171,7 @@ function writeProjectSourceDocumentBuildCacheV1(root, sources, documents) {
92322
93171
  renameSync3(temporary, file);
92323
93172
  }
92324
93173
  function projectSourceFileFingerprint(source) {
92325
- return createHash5("sha256").update(String(PROJECT_SOURCE_DOCUMENT_BUILD_CACHE_REVISION)).update("\0").update(String(NEOSCRIPT_COMPILER_REVISION)).update("\0").update(source.kind).update("\0").update(source.text).digest("hex");
93174
+ return createHash6("sha256").update(String(PROJECT_SOURCE_DOCUMENT_BUILD_CACHE_REVISION)).update("\0").update(String(NEOSCRIPT_COMPILER_REVISION)).update("\0").update(source.kind).update("\0").update(source.text).digest("hex");
92326
93175
  }
92327
93176
  function isCachedProjectSourceDocument(value, source) {
92328
93177
  if (value === null || typeof value !== "object") return false;
@@ -92330,7 +93179,7 @@ function isCachedProjectSourceDocument(value, source) {
92330
93179
  return document.kind === source.kind && document.sourceText === source.text && Array.isArray(document.declarations) && Array.isArray(document.diagnostics);
92331
93180
  }
92332
93181
  function projectSourceFingerprint(sources) {
92333
- const hash = createHash5("sha256");
93182
+ const hash = createHash6("sha256");
92334
93183
  hash.update(String(PROJECT_SOURCE_BUILD_CACHE_REVISION));
92335
93184
  hash.update("\0");
92336
93185
  hash.update(String(NEOSCRIPT_COMPILER_REVISION));
@@ -92358,7 +93207,7 @@ var init_project_document_cache = __esm({
92358
93207
  });
92359
93208
 
92360
93209
  // src/project-source/project-files.ts
92361
- import { createHash as createHash6 } from "node:crypto";
93210
+ import { createHash as createHash7 } from "node:crypto";
92362
93211
  import {
92363
93212
  existsSync as existsSync3,
92364
93213
  mkdirSync as mkdirSync6,
@@ -92605,7 +93454,7 @@ function discoverProjectBinariesV4(root, explicit, ignoredPaths = []) {
92605
93454
  }).sort((left, right) => compareCodePoints(left.path, right.path));
92606
93455
  }
92607
93456
  function sha256Bytes(bytes) {
92608
- return createHash6("sha256").update(bytes).digest("hex");
93457
+ return createHash7("sha256").update(bytes).digest("hex");
92609
93458
  }
92610
93459
  function sha256File(path) {
92611
93460
  return sha256Bytes(readFileSync6(path));
@@ -98058,7 +98907,7 @@ var init_script = __esm({
98058
98907
  });
98059
98908
 
98060
98909
  // ../src/database/project-source-identity.ts
98061
- import { createHash as createHash7 } from "node:crypto";
98910
+ import { createHash as createHash8 } from "node:crypto";
98062
98911
  function hashProjectSourceFiles(inputFiles) {
98063
98912
  const files = normalizeSourceFiles(inputFiles);
98064
98913
  const bytes = Buffer.from(JSON.stringify({ version: 1, files }), "utf8");
@@ -98067,7 +98916,7 @@ function hashProjectSourceFiles(inputFiles) {
98067
98916
  `Project source identity is ${bytes.byteLength} bytes; the limit is ${MAX_SOURCE_BYTES} bytes.`
98068
98917
  );
98069
98918
  }
98070
- return createHash7("sha256").update(bytes).digest("hex");
98919
+ return createHash8("sha256").update(bytes).digest("hex");
98071
98920
  }
98072
98921
  function normalizeSourceFiles(inputFiles) {
98073
98922
  if (inputFiles.length > MAX_SOURCE_FILES) {
@@ -98159,14 +99008,14 @@ var init_project_source_identity = __esm({
98159
99008
 
98160
99009
  // src/push-hook.ts
98161
99010
  import { spawn } from "node:child_process";
98162
- import { createHash as createHash8 } from "node:crypto";
99011
+ import { createHash as createHash9 } from "node:crypto";
98163
99012
  import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync as readdirSync4 } from "node:fs";
98164
99013
  import { join as join13, relative as relative3, sep as sep3 } from "node:path";
98165
- function fingerprintPushInputs(workspace) {
99014
+ function fingerprintPushInputs(workspace, options = {}) {
98166
99015
  const paths = /* @__PURE__ */ new Set([
98167
99016
  join13(workspace.root, "neo.json"),
98168
99017
  ...listProjectSourceFilesV4(workspace.root),
98169
- ...listProjectTestFilesV1(workspace.root),
99018
+ ...options.includeTests === false ? [] : listProjectTestFilesV1(workspace.root),
98170
99019
  ...workspace.config.unityConfigPath === void 0 ? [] : [join13(workspace.root, workspace.config.unityConfigPath)]
98171
99020
  ]);
98172
99021
  const visitManaged = (directory) => {
@@ -98180,7 +99029,7 @@ function fingerprintPushInputs(workspace) {
98180
99029
  };
98181
99030
  visitManaged(join13(workspace.root, "Files", "Images"));
98182
99031
  visitManaged(join13(workspace.root, "Files", "AudioClips"));
98183
- const hash = createHash8("sha256");
99032
+ const hash = createHash9("sha256");
98184
99033
  for (const path of [...paths].sort()) {
98185
99034
  const name = relative3(workspace.root, path).split(sep3).join("/");
98186
99035
  hash.update(name);
@@ -98837,7 +99686,7 @@ __export(push_exports, {
98837
99686
  runPush: () => runPush,
98838
99687
  stripServerDerivedNeoScript: () => stripServerDerivedNeoScript
98839
99688
  });
98840
- import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
99689
+ import { createHash as createHash10, randomUUID as randomUUID2 } from "node:crypto";
98841
99690
  import {
98842
99691
  mkdirSync as mkdirSync10,
98843
99692
  writeFileSync as writeFileSync10,
@@ -98877,8 +99726,13 @@ function assignPendingIds(changes, authoredValueSeeds, reconstructed3, localInit
98877
99726
  return fresh;
98878
99727
  };
98879
99728
  const collect = (value) => {
98880
- if (typeof value === "string" && isPendingId(value)) {
98881
- assign(value);
99729
+ if (typeof value === "string") {
99730
+ const interfaceOwner = pendingInterfaceMemberSymbolOwner(value);
99731
+ if (interfaceOwner !== null) {
99732
+ assign(interfaceOwner);
99733
+ return;
99734
+ }
99735
+ if (isPendingId(value)) assign(value);
98882
99736
  return;
98883
99737
  }
98884
99738
  if (Array.isArray(value)) {
@@ -99032,6 +99886,18 @@ function assignPendingIds(changes, authoredValueSeeds, reconstructed3, localInit
99032
99886
  }
99033
99887
  };
99034
99888
  }
99889
+ function pendingInterfaceMemberSymbolOwner(value) {
99890
+ const marker = ":member:";
99891
+ const markerIndex = value.indexOf(marker);
99892
+ if (markerIndex < 0) return null;
99893
+ const owner = value.slice(0, markerIndex);
99894
+ const parts = owner.split(":");
99895
+ if (parts.length !== 6) return null;
99896
+ if (parts[0] !== "__pending__" || parts[1] !== "interface") return null;
99897
+ if (!/^\d+$/u.test(parts[3] ?? "")) return null;
99898
+ if (!/^\d+$/u.test(parts[4] ?? "")) return null;
99899
+ return owner;
99900
+ }
99035
99901
  function readAcceptedProjectVersionCommitResponse(value) {
99036
99902
  if (!isObjectRecord2(value) || value.kind !== "accepted") return null;
99037
99903
  if (typeof value.transactionId !== "string") {
@@ -99603,7 +100469,7 @@ async function runPush(workspace, options, preparationOverride) {
99603
100469
  version: 1,
99604
100470
  projectFingerprint: `sha256:${preparedLocal.source.sourceHash}`,
99605
100471
  inputFingerprint: preparedInputFingerprint,
99606
- documentSha256: createHash9("sha256").update(documentJson).digest("hex"),
100472
+ documentSha256: createHash10("sha256").update(documentJson).digest("hex"),
99607
100473
  document: candidate.document
99608
100474
  })}
99609
100475
  `,
@@ -101612,7 +102478,7 @@ var init_registry2 = __esm({
101612
102478
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
101613
102479
  formatVersion: 3,
101614
102480
  contractVersion: "3.9",
101615
- cliVersion: "0.26.0",
102481
+ cliVersion: "0.26.2",
101616
102482
  projectFileUploadBatchSize: 32,
101617
102483
  documentRecords: {
101618
102484
  member: {
@@ -102905,7 +103771,7 @@ __export(test_exports, {
102905
103771
  maintainNeoTestBuildCache: () => maintainNeoTestBuildCache,
102906
103772
  runTest: () => runTest
102907
103773
  });
102908
- import { createHash as createHash10, randomUUID as randomUUID3 } from "node:crypto";
103774
+ import { createHash as createHash11, randomUUID as randomUUID3 } from "node:crypto";
102909
103775
  import {
102910
103776
  existsSync as existsSync12,
102911
103777
  mkdirSync as mkdirSync11,
@@ -103019,10 +103885,11 @@ function errorMessage2(error) {
103019
103885
  function findDelegateTargetMemberId(value) {
103020
103886
  return isRecord10(value) && typeof value.memberId === "string" ? value.memberId : null;
103021
103887
  }
103022
- function makeEvaluatorContext(rawDocument, interceptor, documentAlreadyIsolated = false) {
103023
- const isolatedDocument = documentAlreadyIsolated ? rawDocument : structuredClone(rawDocument);
103024
- const document = readDocumentArrays(isolatedDocument);
103025
- const constructors = Array.isArray(isolatedDocument.constructors) ? isolatedDocument.constructors.filter(isRecord10) : [];
103888
+ function sharedEvaluatorBase(rawDocument) {
103889
+ const cached = SHARED_EVALUATOR_BASES.get(rawDocument);
103890
+ if (cached !== void 0) return cached;
103891
+ const document = readDocumentArrays(rawDocument);
103892
+ const constructors = Array.isArray(rawDocument.constructors) ? rawDocument.constructors.filter(isRecord10) : [];
103026
103893
  const vm = {
103027
103894
  project: document.project,
103028
103895
  members: document.members,
@@ -103047,10 +103914,19 @@ function makeEvaluatorContext(rawDocument, interceptor, documentAlreadyIsolated
103047
103914
  }
103048
103915
  )
103049
103916
  };
103050
- return createNeoScriptEvaluationRuntime({
103917
+ const created = {
103051
103918
  vm,
103919
+ rootValue: buildRootValue(document)
103920
+ };
103921
+ SHARED_EVALUATOR_BASES.set(rawDocument, created);
103922
+ return created;
103923
+ }
103924
+ function makeEvaluatorContext(rawDocument, interceptor) {
103925
+ const base = sharedEvaluatorBase(rawDocument);
103926
+ return createNeoScriptEvaluationRuntime({
103927
+ vm: base.vm,
103052
103928
  thisValue: null,
103053
- rootValue: buildRootValue(document),
103929
+ rootValue: base.rootValue,
103054
103930
  dialogueContext: null,
103055
103931
  callInterceptor: interceptor,
103056
103932
  nativeFunctionErrorCheckBehavior: "strict"
@@ -103117,14 +103993,15 @@ function preparedHookCandidate(workspace) {
103117
103993
  );
103118
103994
  }
103119
103995
  const documentJson = JSON.stringify(parsed.document);
103120
- if (createHash10("sha256").update(documentJson).digest("hex") !== parsed.documentSha256) {
103996
+ if (createHash11("sha256").update(documentJson).digest("hex") !== parsed.documentSha256) {
103121
103997
  throw new NeoTestPreparedCandidateError(
103122
103998
  "Configured push hook candidate project document failed checksum verification."
103123
103999
  );
103124
104000
  }
103125
104001
  return {
103126
104002
  document: parsed.document,
103127
- sourceHash: configuredFingerprint.replace(/^sha256:/u, "")
104003
+ sourceHash: configuredFingerprint.replace(/^sha256:/u, ""),
104004
+ documentSha256: parsed.documentSha256
103128
104005
  };
103129
104006
  } catch (error) {
103130
104007
  if (error instanceof NeoTestPreparedCandidateError) throw error;
@@ -103133,11 +104010,79 @@ function preparedHookCandidate(workspace) {
103133
104010
  );
103134
104011
  }
103135
104012
  }
104013
+ function fingerprintTestCandidateInputs(workspace) {
104014
+ return createHash11("sha256").update("neo-test-candidate\0").update(String(TEST_CANDIDATE_CACHE_REVISION)).update("\0").update(PROJECT_SCHEMA_CONTRACT.cliVersion).update("\0").update(String(NEOSCRIPT_COMPILER_REVISION)).update("\0").update(fingerprintPushInputs(workspace, { includeTests: false })).update("\0").update(
104015
+ workspace.stateSourceSha256 === void 0 ? `object\0${JSON.stringify(workspace.state)}` : `source\0${workspace.stateSourceSha256}`
104016
+ ).digest("hex");
104017
+ }
104018
+ function testCandidateCachePath(workspace, inputFingerprint) {
104019
+ const cacheKey = createHash11("sha256").update(inputFingerprint).digest("hex");
104020
+ return join16(
104021
+ workspace.root,
104022
+ ".neo",
104023
+ "test-build",
104024
+ "v1",
104025
+ "project",
104026
+ cacheKey,
104027
+ "candidate.json"
104028
+ );
104029
+ }
104030
+ function cachedTestCandidate(workspace, inputFingerprint) {
104031
+ try {
104032
+ const candidatePath = testCandidateCachePath(workspace, inputFingerprint);
104033
+ const parsed = JSON.parse(readFileSync16(candidatePath, "utf8"));
104034
+ if (!isRecord10(parsed)) return null;
104035
+ if (parsed.version !== 1) return null;
104036
+ if (parsed.candidateRevision !== TEST_CANDIDATE_CACHE_REVISION) return null;
104037
+ if (parsed.cliVersion !== PROJECT_SCHEMA_CONTRACT.cliVersion) return null;
104038
+ if (parsed.compilerRevision !== NEOSCRIPT_COMPILER_REVISION) return null;
104039
+ if (parsed.inputFingerprint !== inputFingerprint) return null;
104040
+ if (typeof parsed.sourceHash !== "string" || parsed.sourceHash.length === 0) {
104041
+ return null;
104042
+ }
104043
+ if (typeof parsed.documentSha256 !== "string") return null;
104044
+ const documentJson = readFileSync16(
104045
+ join16(dirname9(candidatePath), "document.json"),
104046
+ "utf8"
104047
+ );
104048
+ if (createHash11("sha256").update(documentJson).digest("hex") !== parsed.documentSha256) {
104049
+ return null;
104050
+ }
104051
+ const document = JSON.parse(documentJson);
104052
+ if (!isRecord10(document)) return null;
104053
+ return {
104054
+ document,
104055
+ sourceHash: parsed.sourceHash,
104056
+ documentSha256: parsed.documentSha256
104057
+ };
104058
+ } catch {
104059
+ return null;
104060
+ }
104061
+ }
104062
+ function cacheTestCandidate(workspace, inputFingerprint, candidate) {
104063
+ const documentJson = JSON.stringify(candidate.document);
104064
+ const documentSha256 = createHash11("sha256").update(documentJson).digest("hex");
104065
+ const candidatePath = testCandidateCachePath(workspace, inputFingerprint);
104066
+ atomicWrite(join16(dirname9(candidatePath), "document.json"), documentJson);
104067
+ atomicWrite(
104068
+ candidatePath,
104069
+ JSON.stringify({
104070
+ version: 1,
104071
+ candidateRevision: TEST_CANDIDATE_CACHE_REVISION,
104072
+ cliVersion: PROJECT_SCHEMA_CONTRACT.cliVersion,
104073
+ compilerRevision: NEOSCRIPT_COMPILER_REVISION,
104074
+ inputFingerprint,
104075
+ sourceHash: candidate.sourceHash,
104076
+ documentSha256
104077
+ }) + "\n"
104078
+ );
104079
+ return { ...candidate, documentSha256 };
104080
+ }
103136
104081
  function compileSpec(workspace, document, absolutePath, projectCompilationHash2) {
103137
104082
  const scriptDocument = readDocumentArrays(document);
103138
104083
  const path = relative5(workspace.root, absolutePath).split(sep5).join("/");
103139
104084
  const source = readFileSync16(absolutePath, "utf8");
103140
- const sourceHash = createHash10("sha256").update(source).digest("hex");
104085
+ const sourceHash = createHash11("sha256").update(source).digest("hex");
103141
104086
  const artifactPath = join16(
103142
104087
  workspace.root,
103143
104088
  ".neo",
@@ -103148,7 +104093,7 @@ function compileSpec(workspace, document, absolutePath, projectCompilationHash2)
103148
104093
  );
103149
104094
  try {
103150
104095
  const cached = JSON.parse(readFileSync16(artifactPath, "utf8"));
103151
- if (isRecord10(cached) && cached.version === 1 && cached.compilerRevision === NEOSCRIPT_COMPILER_REVISION && cached.projectCompilationHash === projectCompilationHash2 && cached.sourceHash === sourceHash && isRecord10(cached.action) && typeof cached.artifactSha256 === "string" && createHash10("sha256").update(JSON.stringify(cached.action)).digest("hex") === cached.artifactSha256 && cached.action.compilerRevision === NEOSCRIPT_COMPILER_REVISION && Array.isArray(cached.action.parameters) && Array.isArray(cached.action.instructions) && isRecord10(cached.action.typeInfo)) {
104096
+ if (isRecord10(cached) && cached.version === 1 && cached.compilerRevision === NEOSCRIPT_COMPILER_REVISION && cached.projectCompilationHash === projectCompilationHash2 && cached.sourceHash === sourceHash && isRecord10(cached.action) && typeof cached.artifactSha256 === "string" && createHash11("sha256").update(JSON.stringify(cached.action)).digest("hex") === cached.artifactSha256 && cached.action.compilerRevision === NEOSCRIPT_COMPILER_REVISION && Array.isArray(cached.action.parameters) && Array.isArray(cached.action.instructions) && isRecord10(cached.action.typeInfo)) {
103152
104097
  return {
103153
104098
  path,
103154
104099
  source,
@@ -103180,15 +104125,16 @@ function compileSpec(workspace, document, absolutePath, projectCompilationHash2)
103180
104125
  compilerRevision: NEOSCRIPT_COMPILER_REVISION,
103181
104126
  projectCompilationHash: projectCompilationHash2,
103182
104127
  sourceHash,
103183
- artifactSha256: createHash10("sha256").update(JSON.stringify(compiled.action)).digest("hex"),
104128
+ artifactSha256: createHash11("sha256").update(JSON.stringify(compiled.action)).digest("hex"),
103184
104129
  action: compiled.action
103185
104130
  })}
103186
104131
  `
103187
104132
  );
103188
104133
  return compiled;
103189
104134
  }
103190
- function projectCompilationHash(projectSourceHash, document) {
103191
- return createHash10("sha256").update(projectSourceHash).update("\0").update(JSON.stringify(document)).digest("hex");
104135
+ function projectCompilationHash(projectSourceHash, document, knownDocumentSha256) {
104136
+ const documentSha256 = knownDocumentSha256 ?? createHash11("sha256").update(JSON.stringify(document)).digest("hex");
104137
+ return createHash11("sha256").update(projectSourceHash).update("\0").update(documentSha256).digest("hex");
103192
104138
  }
103193
104139
  function selectedSpecPaths(workspace, selectors) {
103194
104140
  const all = listProjectTestFilesV1(workspace.root);
@@ -103421,7 +104367,7 @@ function registerSpec(spec, document) {
103421
104367
  }
103422
104368
  names.add(fullName);
103423
104369
  const test = {
103424
- id: `sha256:${createHash10("sha256").update(`${spec.path}\0${namePath}`).digest("hex")}`,
104370
+ id: `sha256:${createHash11("sha256").update(`${spec.path}\0${namePath}`).digest("hex")}`,
103425
104371
  name: namePath,
103426
104372
  fullName,
103427
104373
  body: portableDelegate(call.args[1]),
@@ -103772,24 +104718,32 @@ function interceptTestCall(environment, call) {
103772
104718
  throw error;
103773
104719
  }
103774
104720
  }
103775
- function createTestEnvironment(document, documentAlreadyIsolated = false) {
104721
+ function createTestEnvironment(document) {
103776
104722
  const environment = {
103777
104723
  baseDocument: document,
103778
104724
  context: void 0,
103779
104725
  mocks: /* @__PURE__ */ new Map(),
103780
104726
  mocksByMember: /* @__PURE__ */ new Map(),
103781
- nextMockId: 1
104727
+ nextMockId: 1,
104728
+ hasStateChanges: false
103782
104729
  };
103783
104730
  environment.context = makeEvaluatorContext(
103784
104731
  document,
103785
- (call) => interceptTestCall(environment, call),
103786
- documentAlreadyIsolated
104732
+ (call) => interceptTestCall(environment, call)
103787
104733
  );
103788
104734
  return environment;
103789
104735
  }
103790
104736
  function snapshotEnvironmentDocument(environment) {
103791
- const snapshot = structuredClone(environment.baseDocument);
103792
- const values = Array.isArray(snapshot.values) ? snapshot.values : [];
104737
+ const overlay = environment.context.__valueOverlay;
104738
+ const runtime = environment.context.__runtimeSessionValues;
104739
+ const bindings = new Map([
104740
+ ...environment.context.__saveStaticBindings ?? [],
104741
+ ...environment.context.__sessionStaticBindings ?? []
104742
+ ]);
104743
+ if (!environment.hasStateChanges) return environment.baseDocument;
104744
+ const snapshot = { ...environment.baseDocument };
104745
+ const baseValues = Array.isArray(snapshot.values) ? snapshot.values : [];
104746
+ const values = [...baseValues];
103793
104747
  const byId = /* @__PURE__ */ new Map();
103794
104748
  values.forEach((value, index) => {
103795
104749
  if (isRecord10(value) && typeof value.id === "string")
@@ -103806,20 +104760,17 @@ function snapshotEnvironmentDocument(environment) {
103806
104760
  values[index] = cloned;
103807
104761
  }
103808
104762
  };
103809
- for (const row of environment.context.__valueOverlay?.values() ?? [])
103810
- mergeRow(row);
103811
- for (const row of environment.context.__runtimeSessionValues?.values() ?? [])
103812
- mergeRow(row);
104763
+ for (const row of overlay?.values() ?? []) mergeRow(row);
104764
+ for (const row of runtime?.values() ?? []) mergeRow(row);
103813
104765
  snapshot.values = values;
103814
- const members = Array.isArray(snapshot.members) ? snapshot.members : [];
103815
- const bindings = new Map([
103816
- ...environment.context.__saveStaticBindings ?? [],
103817
- ...environment.context.__sessionStaticBindings ?? []
103818
- ]);
103819
- for (const member of members) {
103820
- if (!isRecord10(member) || typeof member.id !== "string") continue;
103821
- if (bindings.has(member.id))
103822
- member.valueId = bindings.get(member.id) ?? null;
104766
+ if (bindings.size > 0) {
104767
+ const baseMembers = Array.isArray(snapshot.members) ? snapshot.members : [];
104768
+ snapshot.members = baseMembers.map((member) => {
104769
+ if (!isRecord10(member) || typeof member.id !== "string" || !bindings.has(member.id)) {
104770
+ return member;
104771
+ }
104772
+ return { ...member, valueId: bindings.get(member.id) ?? null };
104773
+ });
103823
104774
  }
103824
104775
  return snapshot;
103825
104776
  }
@@ -103840,10 +104791,7 @@ function copyMocks(source, target) {
103840
104791
  }
103841
104792
  }
103842
104793
  function cloneTestEnvironment(source) {
103843
- const cloned = createTestEnvironment(
103844
- snapshotEnvironmentDocument(source),
103845
- true
103846
- );
104794
+ const cloned = createTestEnvironment(snapshotEnvironmentDocument(source));
103847
104795
  copyMocks(source, cloned);
103848
104796
  return cloned;
103849
104797
  }
@@ -103854,10 +104802,14 @@ function runCallback(environment, delegate, deadlineMs) {
103854
104802
  __indexes: void 0,
103855
104803
  wallClockDeadlineMs: deadlineMs
103856
104804
  });
103857
- evaluateNSDelegate(
103858
- bindNeoScriptDelegateToContext(delegate, environment.context),
103859
- environment.context
103860
- );
104805
+ try {
104806
+ evaluateNSDelegate(
104807
+ bindNeoScriptDelegateToContext(delegate, environment.context),
104808
+ environment.context
104809
+ );
104810
+ } finally {
104811
+ environment.hasStateChanges ||= (environment.context.__executionState?.writes.length ?? 0) > 0 || (environment.context.__runtimeSessionValues?.size ?? 0) > 0 || (environment.context.__saveStaticBindings?.size ?? 0) > 0 || (environment.context.__sessionStaticBindings?.size ?? 0) > 0;
104812
+ }
103861
104813
  }
103862
104814
  function failureFor(error, file, position, member = null) {
103863
104815
  const timeout = error instanceof NeoScriptWallClockTimeoutError;
@@ -103883,18 +104835,22 @@ function suiteHasSelectedTests(suite, selected2) {
103883
104835
  async function executeRegisteredSpec(registered, document, selected2, timeoutMs, interrupted) {
103884
104836
  const results = [];
103885
104837
  const fileFailures = [];
104838
+ let testDurationMs = 0;
103886
104839
  const executeSuite = async (suite, parentEnvironment, inheritedFailures) => {
103887
104840
  if (!suiteHasSelectedTests(suite, selected2)) return;
103888
104841
  const fixture = cloneTestEnvironment(parentEnvironment);
103889
104842
  const suiteFailures = [...inheritedFailures];
103890
104843
  const suiteDeadline = Date.now() + timeoutMs;
103891
104844
  for (const hook of suite.beforeAll) {
104845
+ const hookStarted = performance.now();
103892
104846
  try {
103893
104847
  runCallback(fixture, hook, suiteDeadline);
103894
104848
  } catch (error) {
103895
104849
  suiteFailures.push(
103896
104850
  failureFor(error, registered.spec.path, suite, "beforeAll")
103897
104851
  );
104852
+ } finally {
104853
+ testDurationMs += performance.now() - hookStarted;
103898
104854
  }
103899
104855
  }
103900
104856
  for (const test of suite.tests) {
@@ -103902,6 +104858,8 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
103902
104858
  const started = performance.now();
103903
104859
  const deadline = Date.now() + timeoutMs;
103904
104860
  const environment = cloneTestEnvironment(fixture);
104861
+ const startupBeforeTestMs = performance.now() - started;
104862
+ const testStarted = performance.now();
103905
104863
  const failures = [...suiteFailures];
103906
104864
  let setupFailed = failures.length > 0;
103907
104865
  if (!setupFailed) {
@@ -103938,19 +104896,24 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
103938
104896
  }
103939
104897
  }
103940
104898
  }
103941
- const durationMs = performance.now() - started;
104899
+ const currentTestDurationMs = performance.now() - testStarted;
104900
+ const schedulerStarted = performance.now();
104901
+ await new Promise((resolvePromise) => {
104902
+ setImmediate(resolvePromise);
104903
+ });
104904
+ const startupDurationMs = startupBeforeTestMs + performance.now() - schedulerStarted;
104905
+ testDurationMs += currentTestDurationMs;
103942
104906
  results.push({
103943
104907
  id: test.id,
103944
104908
  file: registered.spec.path,
103945
104909
  name: test.name,
103946
104910
  fullName: test.fullName,
103947
104911
  status: failures.length === 0 ? "passed" : "failed",
103948
- durationMs,
104912
+ durationMs: startupDurationMs + currentTestDurationMs,
104913
+ startupDurationMs,
104914
+ testDurationMs: currentTestDurationMs,
103949
104915
  failures
103950
104916
  });
103951
- await new Promise((resolvePromise) => {
103952
- setImmediate(resolvePromise);
103953
- });
103954
104917
  if (interrupted()) break;
103955
104918
  }
103956
104919
  for (const child of suite.suites) {
@@ -103958,12 +104921,15 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
103958
104921
  await executeSuite(child, fixture, suiteFailures);
103959
104922
  }
103960
104923
  for (const hook of suite.afterAll) {
104924
+ const hookStarted = performance.now();
103961
104925
  try {
103962
104926
  runCallback(fixture, hook, Date.now() + timeoutMs);
103963
104927
  } catch (error) {
103964
104928
  fileFailures.push(
103965
104929
  failureFor(error, registered.spec.path, suite, "afterAll")
103966
104930
  );
104931
+ } finally {
104932
+ testDurationMs += performance.now() - hookStarted;
103967
104933
  }
103968
104934
  }
103969
104935
  };
@@ -103974,7 +104940,7 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
103974
104940
  results.sort(
103975
104941
  (left, right) => (registrationOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (registrationOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)
103976
104942
  );
103977
- return { tests: results, failures: fileFailures };
104943
+ return { tests: results, failures: fileFailures, testDurationMs };
103978
104944
  }
103979
104945
  function atomicWrite(path, content) {
103980
104946
  mkdirSync11(dirname9(path), { recursive: true });
@@ -104027,8 +104993,12 @@ function maintainNeoTestBuildCache(root, maxBytes = TEST_BUILD_CACHE_LIMIT_BYTES
104027
104993
  total -= file.size;
104028
104994
  }
104029
104995
  }
104996
+ function formatTestDuration(durationMs) {
104997
+ return `${durationMs < 10 ? durationMs.toFixed(2) : durationMs.toFixed(1)}ms`;
104998
+ }
104030
104999
  async function runTest(workspace, options, dependencies = {}) {
104031
105000
  const started = Date.now();
105001
+ const performanceStarted = performance.now();
104032
105002
  const testBuildRoot = join16(workspace.root, ".neo", "test-build");
104033
105003
  maintainNeoTestBuildCache(testBuildRoot);
104034
105004
  const startedAt = new Date(started).toISOString();
@@ -104041,6 +105011,7 @@ async function runTest(workspace, options, dependencies = {}) {
104041
105011
  const selectedTestCountByFile = /* @__PURE__ */ new Map();
104042
105012
  const fileFailuresByPath = /* @__PURE__ */ new Map();
104043
105013
  const diagnostics = [];
105014
+ let testDurationMs = 0;
104044
105015
  let exitCode = 0;
104045
105016
  let interrupted = false;
104046
105017
  const handleSigint = () => {
@@ -104056,18 +105027,47 @@ async function runTest(workspace, options, dependencies = {}) {
104056
105027
  process.on("SIGINT", handleSigint);
104057
105028
  let phase = "prepare";
104058
105029
  try {
104059
- const candidate = dependencies.prepareLocalCandidate === void 0 ? preparedHookCandidate(workspace) ?? await prepareLocalCandidateV4(workspace) : await dependencies.prepareLocalCandidate(workspace);
105030
+ let candidate = dependencies.prepareLocalCandidate === void 0 ? preparedHookCandidate(workspace) : null;
105031
+ let candidateInputFingerprint = null;
105032
+ let shouldCacheCandidate = false;
105033
+ if (candidate === null) {
105034
+ const canUseCandidateCache = dependencies.prepareLocalCandidate === void 0 || dependencies.fingerprintCandidateInputs !== void 0;
105035
+ if (canUseCandidateCache) {
105036
+ candidateInputFingerprint = dependencies.fingerprintCandidateInputs?.(workspace) ?? fingerprintTestCandidateInputs(workspace);
105037
+ candidate = cachedTestCandidate(workspace, candidateInputFingerprint);
105038
+ }
105039
+ if (candidate === null) {
105040
+ const prepared = dependencies.prepareLocalCandidate === void 0 ? await prepareLocalCandidateV4(workspace) : await dependencies.prepareLocalCandidate(workspace);
105041
+ if (prepared.document === null) {
105042
+ throw new NeoTestReportWriteError(
105043
+ "Local test preparation did not produce a project document."
105044
+ );
105045
+ }
105046
+ candidate = {
105047
+ document: prepared.document,
105048
+ sourceHash: prepared.sourceHash
105049
+ };
105050
+ shouldCacheCandidate = canUseCandidateCache;
105051
+ }
105052
+ }
104060
105053
  assertNotInterrupted();
104061
- if (candidate.document === null) {
105054
+ if (candidate === null) {
104062
105055
  throw new NeoTestReportWriteError(
104063
- "Local test preparation did not produce a project document."
105056
+ "Local test preparation produced no reusable candidate."
104064
105057
  );
104065
105058
  }
105059
+ if (shouldCacheCandidate && candidateInputFingerprint !== null) {
105060
+ candidate = cacheTestCandidate(workspace, candidateInputFingerprint, {
105061
+ document: candidate.document,
105062
+ sourceHash: candidate.sourceHash
105063
+ });
105064
+ }
104066
105065
  projectFingerprint = `sha256:${candidate.sourceHash}`;
104067
105066
  const rawDocument = documentRecord(candidate.document);
104068
105067
  const compilationHash = projectCompilationHash(
104069
105068
  candidate.sourceHash,
104070
- rawDocument
105069
+ rawDocument,
105070
+ candidate.documentSha256
104071
105071
  );
104072
105072
  phase = "select";
104073
105073
  let selectedPaths;
@@ -104126,6 +105126,7 @@ async function runTest(workspace, options, dependencies = {}) {
104126
105126
  () => interrupted
104127
105127
  );
104128
105128
  results.push(...executed.tests);
105129
+ testDurationMs += executed.testDurationMs;
104129
105130
  fileFailuresByPath.set(entry.spec.path, executed.failures);
104130
105131
  assertNotInterrupted();
104131
105132
  }
@@ -104139,14 +105140,14 @@ async function runTest(workspace, options, dependencies = {}) {
104139
105140
  compilerRevision: NEOSCRIPT_COMPILER_REVISION,
104140
105141
  evaluatorRevision: 4,
104141
105142
  projectFingerprint,
104142
- configurationSha256: createHash10("sha256").update(JSON.stringify(workspace.config.test ?? {})).digest("hex"),
105143
+ configurationSha256: createHash11("sha256").update(JSON.stringify(workspace.config.test ?? {})).digest("hex"),
104143
105144
  dependencyGraph: Object.fromEntries(
104144
105145
  specs.map((spec) => [spec.path, [projectFingerprint]])
104145
105146
  ),
104146
105147
  artifacts: specs.map((spec) => ({
104147
105148
  path: spec.path,
104148
- sourceSha256: createHash10("sha256").update(spec.source).digest("hex"),
104149
- artifactSha256: createHash10("sha256").update(JSON.stringify(spec.action)).digest("hex")
105149
+ sourceSha256: createHash11("sha256").update(spec.source).digest("hex"),
105150
+ artifactSha256: createHash11("sha256").update(JSON.stringify(spec.action)).digest("hex")
104150
105151
  }))
104151
105152
  };
104152
105153
  atomicWrite(
@@ -104174,7 +105175,16 @@ async function runTest(workspace, options, dependencies = {}) {
104174
105175
  0
104175
105176
  );
104176
105177
  const skipped = Math.max(0, selectedTestTotal - results.length);
104177
- const durationMs = Date.now() - started;
105178
+ const durationMs = performance.now() - performanceStarted;
105179
+ const startupDurationMs = Math.max(0, durationMs - testDurationMs);
105180
+ const perTestStartupDurationMs = results.reduce(
105181
+ (sum, result) => sum + result.startupDurationMs,
105182
+ 0
105183
+ );
105184
+ const sharedStartupDurationMs = Math.max(
105185
+ 0,
105186
+ startupDurationMs - perTestStartupDurationMs
105187
+ );
104178
105188
  const testFiles = specs.map((spec) => {
104179
105189
  const tests = results.filter((result) => result.file === spec.path);
104180
105190
  const failures = fileFailuresByPath.get(spec.path) ?? [];
@@ -104191,7 +105201,7 @@ async function runTest(workspace, options, dependencies = {}) {
104191
105201
  success: exitCode === 0,
104192
105202
  projectFingerprint,
104193
105203
  seed: Number.parseInt(
104194
- createHash10("sha256").update(projectFingerprint ?? "neo-test:no-project").digest("hex").slice(0, 8),
105204
+ createHash11("sha256").update(projectFingerprint ?? "neo-test:no-project").digest("hex").slice(0, 8),
104195
105205
  16
104196
105206
  ),
104197
105207
  selection: {
@@ -104206,7 +105216,10 @@ async function runTest(workspace, options, dependencies = {}) {
104206
105216
  failed,
104207
105217
  skipped,
104208
105218
  todo: 0,
104209
- durationMs
105219
+ durationMs,
105220
+ startupDurationMs,
105221
+ sharedStartupDurationMs,
105222
+ testDurationMs
104210
105223
  },
104211
105224
  testFiles,
104212
105225
  diagnostics,
@@ -104231,9 +105244,12 @@ async function runTest(workspace, options, dependencies = {}) {
104231
105244
  if (options.reporter === "json") {
104232
105245
  if (options.outputFile === null) process.stdout.write(serialized);
104233
105246
  } else {
105247
+ console.log(
105248
+ `\u21BB shared startup (${formatTestDuration(report.summary.sharedStartupDurationMs)})`
105249
+ );
104234
105250
  for (const result of results) {
104235
105251
  console.log(
104236
- `${result.status === "passed" ? "\u2713" : "\u2717"} ${result.file} > ${result.name} (${result.durationMs.toFixed(1)}ms)`
105252
+ `${result.status === "passed" ? "\u2713" : "\u2717"} ${result.file} > ${result.name} (total ${formatTestDuration(result.durationMs)}; test ${formatTestDuration(result.testDurationMs)}; startup ${formatTestDuration(result.startupDurationMs)})`
104237
105253
  );
104238
105254
  for (const failure of result.failures)
104239
105255
  console.log(` ${failure.message}`);
@@ -104254,7 +105270,7 @@ async function runTest(workspace, options, dependencies = {}) {
104254
105270
  for (const diagnostic of diagnostics)
104255
105271
  console.log(`\u2717 ${diagnostic.message}`);
104256
105272
  console.log(
104257
- `${report.summary.passed} passed, ${report.summary.failed} failed (${report.summary.durationMs}ms)`
105273
+ `${report.summary.passed} passed, ${report.summary.failed} failed (total ${formatTestDuration(report.summary.durationMs)}; test ${formatTestDuration(report.summary.testDurationMs)}; startup ${formatTestDuration(report.summary.startupDurationMs)})`
104258
105274
  );
104259
105275
  }
104260
105276
  if (!report.success) process.exitCode = exitCode;
@@ -104301,7 +105317,7 @@ async function inspectNeoTestCompilation(workspace, dependencies = {}) {
104301
105317
  errors
104302
105318
  };
104303
105319
  }
104304
- var TEST_BUILD_CACHE_LIMIT_BYTES, ABANDONED_TEMP_MAX_AGE_MS, NeoTestUsageError, NeoTestNoTestsError, NeoTestRegistrationError, NeoTestReportWriteError, NeoTestPreparedCandidateError, NeoTestInterruptedError, NeoTestAssertionError, ERROR_SOURCE_POSITIONS, REGISTRATION_IDS;
105320
+ var TEST_BUILD_CACHE_LIMIT_BYTES, ABANDONED_TEMP_MAX_AGE_MS, TEST_CANDIDATE_CACHE_REVISION, NeoTestUsageError, NeoTestNoTestsError, NeoTestRegistrationError, NeoTestReportWriteError, NeoTestPreparedCandidateError, NeoTestInterruptedError, NeoTestAssertionError, ERROR_SOURCE_POSITIONS, SHARED_EVALUATOR_BASES, REGISTRATION_IDS;
104305
105321
  var init_test = __esm({
104306
105322
  "src/commands/test.ts"() {
104307
105323
  "use strict";
@@ -104316,6 +105332,7 @@ var init_test = __esm({
104316
105332
  init_push_hook();
104317
105333
  TEST_BUILD_CACHE_LIMIT_BYTES = 512 * 1024 * 1024;
104318
105334
  ABANDONED_TEMP_MAX_AGE_MS = 60 * 60 * 1e3;
105335
+ TEST_CANDIDATE_CACHE_REVISION = 1;
104319
105336
  NeoTestUsageError = class extends Error {
104320
105337
  name = "NeoTestUsageError";
104321
105338
  };
@@ -104347,6 +105364,7 @@ var init_test = __esm({
104347
105364
  received;
104348
105365
  };
104349
105366
  ERROR_SOURCE_POSITIONS = /* @__PURE__ */ new WeakMap();
105367
+ SHARED_EVALUATOR_BASES = /* @__PURE__ */ new WeakMap();
104350
105368
  REGISTRATION_IDS = /* @__PURE__ */ new Set([
104351
105369
  NEO_TEST_IDS.describe,
104352
105370
  NEO_TEST_IDS.test,
@@ -107742,7 +108760,8 @@ function loadWorkspaceForCommand(args) {
107742
108760
  // A reset reconstructs the working copy from the server and may therefore
107743
108761
  // discard an unsupported pre-cutover cache. No other command gets this
107744
108762
  // exception, so format 2 never becomes an active compatibility read path.
107745
- discardLegacyFormat2State: args.command === "pull" && boolFlag(args, "reset")
108763
+ discardLegacyFormat2State: args.command === "pull" && boolFlag(args, "reset"),
108764
+ fingerprintStateSource: args.command === "test"
107746
108765
  });
107747
108766
  const apiOverride = stringFlag(args, "api");
107748
108767
  if (apiOverride !== null) {
@@ -107920,7 +108939,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
107920
108939
  async function main() {
107921
108940
  const args = parseArgs(process.argv.slice(2));
107922
108941
  if (args.command === "--version") {
107923
- console.log("0.26.0");
108942
+ console.log("0.26.2");
107924
108943
  return;
107925
108944
  }
107926
108945
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {