@neocompose/cli 0.26.1 → 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"() {
@@ -21592,6 +21921,7 @@ function compileProjectSourceBodies(documents) {
21592
21921
  const graph = buildProjectGraph(documents);
21593
21922
  const projectIndex = createProjectIndex(graph.project);
21594
21923
  const bodies = [];
21924
+ const contexts = [];
21595
21925
  const diagnostics = [];
21596
21926
  for (const [uri, document] of documents) {
21597
21927
  if (document.kind !== "definition") continue;
@@ -21636,6 +21966,7 @@ function compileProjectSourceBodies(documents) {
21636
21966
  },
21637
21967
  projectIndex,
21638
21968
  bodies,
21969
+ contexts,
21639
21970
  diagnostics
21640
21971
  });
21641
21972
  }
@@ -21671,6 +22002,7 @@ function compileProjectSourceBodies(documents) {
21671
22002
  },
21672
22003
  projectIndex,
21673
22004
  bodies,
22005
+ contexts,
21674
22006
  diagnostics
21675
22007
  });
21676
22008
  }
@@ -21711,6 +22043,7 @@ function compileProjectSourceBodies(documents) {
21711
22043
  },
21712
22044
  projectIndex,
21713
22045
  bodies,
22046
+ contexts,
21714
22047
  diagnostics
21715
22048
  });
21716
22049
  continue;
@@ -21735,15 +22068,24 @@ function compileProjectSourceBodies(documents) {
21735
22068
  context: { ...baseContext2, kind: unit },
21736
22069
  projectIndex,
21737
22070
  bodies,
22071
+ contexts,
21738
22072
  diagnostics
21739
22073
  });
21740
22074
  }
21741
22075
  }
21742
22076
  }
21743
22077
  }
21744
- return { project: graph.project, bodies, diagnostics };
22078
+ return { project: graph.project, bodies, contexts, diagnostics };
21745
22079
  }
21746
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
+ });
21747
22089
  const source = new SourceText(args.document.sourceText);
21748
22090
  const start = source.offsetAt(args.range.start);
21749
22091
  const end = source.offsetAt(args.range.end);
@@ -24063,6 +24405,7 @@ function analyzeNeoProjectSources(inputs, parsedDocuments = /* @__PURE__ */ new
24063
24405
  symbols,
24064
24406
  project: bodyCompilation.project,
24065
24407
  compiledBodies: bodyCompilation.bodies,
24408
+ bodyContexts: bodyCompilation.contexts,
24066
24409
  diagnostics
24067
24410
  };
24068
24411
  }
@@ -24890,9 +25233,25 @@ var init_quick_fixes = __esm({
24890
25233
 
24891
25234
  // ../packages/neoscript-language/src/project-source-language-features.ts
24892
25235
  function projectDiagnostics(analysis, uri) {
24893
- 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
+ }));
24894
25245
  }
24895
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 };
24896
25255
  const members = projectMemberCompletions(analysis, document, position);
24897
25256
  if (members) {
24898
25257
  return {
@@ -24977,13 +25336,126 @@ function projectCompletions(analysis, document, position) {
24977
25336
  if (initializerMembers) {
24978
25337
  return { isIncomplete: false, items: initializerMembers };
24979
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) {
24980
25448
  const items = /* @__PURE__ */ new Map();
24981
- for (const keyword of [
24982
- "@id",
24983
- "@settings",
24984
- "@storage",
24985
- "@relations",
24986
- "@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 ? [] : [
24987
25459
  "class",
24988
25460
  "interface",
24989
25461
  "enum",
@@ -24993,16 +25465,20 @@ function projectCompletions(analysis, document, position) {
24993
25465
  "override",
24994
25466
  "readonly",
24995
25467
  "virtual",
24996
- "static",
24997
- "new"
24998
- ]) {
25468
+ "static"
25469
+ ];
25470
+ for (const keyword of keywords) {
24999
25471
  items.set(keyword, {
25000
25472
  label: keyword,
25001
- kind: keyword.startsWith("@") ? "snippet" : "keyword",
25473
+ kind: "keyword",
25002
25474
  insertText: keyword
25003
25475
  });
25004
25476
  }
25005
- 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;
25006
25482
  items.set(typeName, {
25007
25483
  label: typeName,
25008
25484
  kind: "class",
@@ -25010,10 +25486,31 @@ function projectCompletions(analysis, document, position) {
25010
25486
  insertText: typeName
25011
25487
  });
25012
25488
  }
25489
+ const source = analysis.documents.get(document.uri);
25490
+ const owner = source ? declarationContaining(source, position)?.name : void 0;
25013
25491
  for (const symbol of analysis.symbols) {
25014
25492
  if (symbol.scopeRange && (symbol.location.uri !== document.uri || !rangeContains(symbol.scopeRange, position))) {
25015
25493
  continue;
25016
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
+ }
25017
25514
  const kind = projectCompletionKind(symbol);
25018
25515
  items.set(symbol.name, {
25019
25516
  label: symbol.name,
@@ -25025,6 +25522,168 @@ function projectCompletions(analysis, document, position) {
25025
25522
  }
25026
25523
  return { isIncomplete: false, items: [...items.values()] };
25027
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
+ }
25028
25687
  function projectMemberCompletions(analysis, document, position) {
25029
25688
  const tokens = projectTokens(document).filter(
25030
25689
  (token) => token.kind !== "eof" && token.kind !== "comment" && positionCompare(token.range.start, position) <= 0
@@ -25262,6 +25921,8 @@ function activeNamedArgument(text, position) {
25262
25921
  return /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*\.[A-Za-z0-9_]*$/.exec(prefix)?.[1] ?? null;
25263
25922
  }
25264
25923
  function projectHover(analysis, document, position) {
25924
+ const body = projectBodySnapshotAt(analysis, document, position);
25925
+ if (body) return hover(body, position);
25265
25926
  const resolved = projectSymbolAt(analysis, document, position);
25266
25927
  if (!resolved) return null;
25267
25928
  const owner = resolved.symbol.ownerName ? ` on \`${resolved.symbol.ownerName}\`` : "";
@@ -25309,6 +25970,8 @@ Construct with \`new(${parameters})\` \u2014 this class declares a required cons
25309
25970
  ${lines.join("\n\n")}`;
25310
25971
  }
25311
25972
  function projectDefinition(analysis, document, position) {
25973
+ const body = projectBodySnapshotAt(analysis, document, position);
25974
+ if (body) return definition(body, position);
25312
25975
  const resolved = projectSymbolAt(analysis, document, position);
25313
25976
  return resolved ? [resolved.symbol.location] : [];
25314
25977
  }
@@ -25375,6 +26038,8 @@ function requiredDestinationEdit(document, initializer, destination) {
25375
26038
  return open && close ? trailingBlockReturnEdit(document.text, open, close, destination) : null;
25376
26039
  }
25377
26040
  function projectSignatureHelp(analysis, document, position) {
26041
+ const body = projectBodySnapshotAt(analysis, document, position);
26042
+ if (body) return signatureHelp(body, position);
25378
26043
  const tokens = tokensBeforePosition(document.text, position);
25379
26044
  const openIndex = activeCallOpenIndex(tokens, position);
25380
26045
  if (openIndex < 0) return null;
@@ -25436,6 +26101,9 @@ function projectSignatureHelp(analysis, document, position) {
25436
26101
  };
25437
26102
  }
25438
26103
  function projectInlayHints(analysis, document, range2) {
26104
+ const bodyContexts = analysis.bodyContexts.filter(
26105
+ (body) => body.uri === document.uri
26106
+ );
25439
26107
  const tokens = lex(document.text).tokens.filter(
25440
26108
  (token) => token.kind !== "comment" && token.kind !== "eof"
25441
26109
  );
@@ -25450,6 +26118,9 @@ function projectInlayHints(analysis, document, range2) {
25450
26118
  if (!callee || callee.kind !== "identifier" && callee.kind !== "type") {
25451
26119
  continue;
25452
26120
  }
26121
+ if (bodyContexts.some((body) => rangeContains(body.range, callee.range.start))) {
26122
+ continue;
26123
+ }
25453
26124
  if (!open || open.text !== "(") continue;
25454
26125
  const beforeCallee = tokens[index - 1];
25455
26126
  if (beforeCallee?.text === "@") continue;
@@ -25476,6 +26147,14 @@ function projectInlayHints(analysis, document, range2) {
25476
26147
  });
25477
26148
  }
25478
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
+ }
25479
26158
  return hints;
25480
26159
  }
25481
26160
  function projectCallParameterNames(analysis, document, callee, beforeCallee) {
@@ -25677,8 +26356,14 @@ function symbolsDeclaredAt(index, uri, range2) {
25677
26356
  function projectSemanticTokens(document, analysis) {
25678
26357
  const index = buildProjectSymbolIndex(analysis.symbols);
25679
26358
  const tokens = projectTokens(document);
25680
- 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) => {
25681
26363
  if (token.kind === "eof" || token.kind === "error") return [];
26364
+ if (bodyContexts.some((body) => rangeContains(body.range, token.range.start))) {
26365
+ return [];
26366
+ }
25682
26367
  const declaration = symbolsDeclaredAt(index, document.uri, token.range)[0];
25683
26368
  const resolved = token.kind === "identifier" ? indexedProjectSymbolAt(analysis, document, token.range.start, index)?.symbol : void 0;
25684
26369
  let type;
@@ -25705,6 +26390,17 @@ function projectSemanticTokens(document, analysis) {
25705
26390
  }
25706
26391
  ];
25707
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
+ );
25708
26404
  }
25709
26405
  function isProjectContextualKeyword(tokens, tokenIndex) {
25710
26406
  const token = tokens[tokenIndex];
@@ -25729,6 +26425,17 @@ function indexedProjectSymbolAt(analysis, document, position, index) {
25729
26425
  if (exact.length === 1) return { token, symbol: exact[0] };
25730
26426
  const candidates = index.byName.get(token.text) ?? [];
25731
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
+ }
25732
26439
  const scoped = candidates.filter(
25733
26440
  (symbol) => symbol.scopeRange && symbol.location.uri === document.uri && rangeContains(symbol.scopeRange, token.range.start)
25734
26441
  ).sort(
@@ -25740,10 +26447,6 @@ function indexedProjectSymbolAt(analysis, document, position, index) {
25740
26447
  return { token, symbol: visibleCandidates[0] };
25741
26448
  }
25742
26449
  if (visibleCandidates.length === 0) return null;
25743
- const sourceTokens = projectTokens(document);
25744
- const tokenIndex = sourceTokens.findIndex(
25745
- (candidate) => candidate.start === token.start && candidate.end === token.end
25746
- );
25747
26450
  if (sourceTokens[tokenIndex + 1]?.text === "=" && sourceTokens[tokenIndex + 2]?.text !== "=") {
25748
26451
  const constructed = constructionMemberSymbol(
25749
26452
  analysis,
@@ -25753,7 +26456,6 @@ function indexedProjectSymbolAt(analysis, document, position, index) {
25753
26456
  );
25754
26457
  if (constructed) return { token, symbol: constructed };
25755
26458
  }
25756
- const source = analysis.documents.get(document.uri);
25757
26459
  const owner = source ? declarationContaining(source, token.range.start)?.name : void 0;
25758
26460
  if (owner) {
25759
26461
  const owned = visibleCandidates.filter(
@@ -26272,10 +26974,11 @@ function projectFunctionsNamed(analysis, name) {
26272
26974
  }
26273
26975
  return result;
26274
26976
  }
26275
- var CONSTRUCTION_INDEX_CACHE;
26977
+ var PROJECT_BODY_SNAPSHOT_CACHE, CONSTRUCTION_INDEX_CACHE;
26276
26978
  var init_project_source_language_features = __esm({
26277
26979
  "../packages/neoscript-language/src/project-source-language-features.ts"() {
26278
26980
  "use strict";
26981
+ init_analyzer();
26279
26982
  init_lexer();
26280
26983
  init_language_spec();
26281
26984
  init_project_source_semantics();
@@ -26283,6 +26986,8 @@ var init_project_source_language_features = __esm({
26283
26986
  init_project_source_tokens();
26284
26987
  init_quick_fixes();
26285
26988
  init_source_text();
26989
+ init_syntax();
26990
+ PROJECT_BODY_SNAPSHOT_CACHE = /* @__PURE__ */ new WeakMap();
26286
26991
  CONSTRUCTION_INDEX_CACHE = /* @__PURE__ */ new WeakMap();
26287
26992
  }
26288
26993
  });
@@ -50456,15 +51161,14 @@ function commentEndIndex(source, index) {
50456
51161
  return null;
50457
51162
  }
50458
51163
  function stripInitializerComments(source) {
50459
- const lines = [{ text: "", removedComment: false }];
51164
+ const lines = [{ parts: [], removedComment: false }];
50460
51165
  let quote6 = null;
50461
51166
  let escaped = false;
50462
51167
  let index = 0;
51168
+ let segmentStart = 0;
50463
51169
  while (index < source.length) {
50464
51170
  const character = source[index];
50465
- const line = lines[lines.length - 1];
50466
51171
  if (quote6 !== null) {
50467
- line.text += character;
50468
51172
  if (escaped) escaped = false;
50469
51173
  else if (character === "\\") escaped = true;
50470
51174
  else if (character === quote6) quote6 = null;
@@ -50473,20 +51177,34 @@ function stripInitializerComments(source) {
50473
51177
  }
50474
51178
  const afterComment = commentEndIndex(source, index);
50475
51179
  if (afterComment !== null) {
51180
+ const line = lines[lines.length - 1];
51181
+ if (segmentStart < index) {
51182
+ line.parts.push(source.slice(segmentStart, index));
51183
+ }
50476
51184
  line.removedComment = true;
50477
51185
  index = afterComment;
51186
+ segmentStart = index;
50478
51187
  continue;
50479
51188
  }
50480
51189
  if (character === "\n") {
50481
- 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 });
50482
51194
  index += 1;
51195
+ segmentStart = index;
50483
51196
  continue;
50484
51197
  }
50485
51198
  if (character === '"' || character === "'") quote6 = character;
50486
- line.text += character;
50487
51199
  index += 1;
50488
51200
  }
50489
- 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();
50490
51208
  }
50491
51209
  function normalizeBodySource(source) {
50492
51210
  const trimmed = source.trim();
@@ -56658,6 +57376,12 @@ var init_NeoScriptScope = __esm({
56658
57376
  setLocal(bindingId, value) {
56659
57377
  this.#bindings.set(bindingId, value);
56660
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
+ }
56661
57385
  *keys() {
56662
57386
  const inherited = /* @__PURE__ */ new Set();
56663
57387
  if (this.parent !== null) {
@@ -61887,23 +62611,30 @@ function evalFunction(fn, scope, ctx) {
61887
62611
  const innerFn = fn.info.function;
61888
62612
  const isList = Array.isArray(c);
61889
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;
61890
62618
  iterateCollection(c, ctx, (entry, key, valueId) => {
61891
62619
  consumeBudget(ctx, "workUnits", 1, "work unit");
61892
- const innerScope = pushParams(
61893
- scope,
61894
- innerFn.parameters,
61895
- [
61896
- isList ? key : key,
61897
- // first param: index for list, key for dict
61898
- entry
61899
- ],
61900
- isList
61901
- );
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
+ }
61902
62633
  const result = evalInstructions(
61903
62634
  innerFn.instructions,
61904
62635
  innerScope,
61905
62636
  ctx,
61906
- evaluationOptions(ctx, false)
62637
+ callbackOptions ?? evaluationOptions(ctx, false)
61907
62638
  );
61908
62639
  if (result.kind === "return" && result.value === true) {
61909
62640
  consumeBudget(
@@ -61928,24 +62659,35 @@ function evalFunction(fn, scope, ctx) {
61928
62659
  const innerFn = fn.info.function ?? null;
61929
62660
  const isList = Array.isArray(c);
61930
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;
61931
62666
  let found = sentinel;
61932
62667
  iterateCollection(c, ctx, (entry, key) => {
61933
- if (!innerFn) {
62668
+ if (innerFn === null) {
61934
62669
  found = entry;
61935
62670
  return 1 /* Break */;
61936
62671
  }
61937
62672
  consumeBudget(ctx, "workUnits", 1, "work unit");
61938
- const innerScope = pushParams(
61939
- scope,
61940
- innerFn.parameters,
61941
- [key, entry],
61942
- isList
61943
- );
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
+ }
61944
62686
  const result = evalInstructions(
61945
62687
  innerFn.instructions,
61946
62688
  innerScope,
61947
62689
  ctx,
61948
- evaluationOptions(ctx, false)
62690
+ callbackOptions ?? evaluationOptions(ctx, false)
61949
62691
  );
61950
62692
  if (result.kind === "return" && result.value === true) {
61951
62693
  found = entry;
@@ -61963,22 +62705,33 @@ function evalFunction(fn, scope, ctx) {
61963
62705
  }
61964
62706
  case "select" /* select */: {
61965
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);
61966
62711
  const innerFn = fn.info.function;
62712
+ const callbackParameterCount = innerFn.parameters.length === 1 || innerFn.parameters.length === 2 ? innerFn.parameters.length : 0;
61967
62713
  const isList = Array.isArray(c);
61968
62714
  const out = [];
61969
62715
  iterateCollection(c, ctx, (entry, key) => {
61970
62716
  consumeBudget(ctx, "workUnits", 1, "work unit");
61971
- const innerScope = pushParams(
61972
- scope,
61973
- innerFn.parameters,
61974
- [key, entry],
61975
- isList
61976
- );
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
+ }
61977
62730
  const result = evalInstructions(
61978
62731
  innerFn.instructions,
61979
62732
  innerScope,
61980
62733
  ctx,
61981
- evaluationOptions(ctx, false)
62734
+ callbackOptions ?? evaluationOptions(ctx, false)
61982
62735
  );
61983
62736
  if (result.kind === "return") {
61984
62737
  consumeBudget(
@@ -63324,7 +64077,7 @@ function encodeRequiredConstructorArgument(args) {
63324
64077
  return adoptTracked(trackedId, "root");
63325
64078
  }
63326
64079
  if (args.typeInfo.type !== 6 /* List */ && args.typeInfo.type !== 5 /* Dictionary */) {
63327
- return args.value;
64080
+ return cloneConstructorLiteralValue(args.value);
63328
64081
  }
63329
64082
  if (args.value === null || args.value === void 0) return null;
63330
64083
  if (trackedId !== null) return adoptTracked(trackedId, "root");
@@ -63368,7 +64121,7 @@ function encodeRequiredConstructorArgument(args) {
63368
64121
  }
63369
64122
  return nested;
63370
64123
  }
63371
- return registerRow(value).id;
64124
+ return registerRow(cloneConstructorLiteralValue(value)).id;
63372
64125
  };
63373
64126
  if (collectionType.type === 6 /* List */) {
63374
64127
  if (!Array.isArray(args.value)) {
@@ -63962,10 +64715,10 @@ function cloneConstructorArgumentGraph(args) {
63962
64715
  return cloneRow(args.source, args.member, args.genericEnv);
63963
64716
  }
63964
64717
  function cloneConstructorLiteralValue(value) {
64718
+ if (typeof value !== "object" || value === null) return value;
63965
64719
  if (Array.isArray(value)) {
63966
64720
  return value.map((entry) => cloneConstructorLiteralValue(entry));
63967
64721
  }
63968
- if (typeof value !== "object" || value === null) return value;
63969
64722
  return Object.fromEntries(
63970
64723
  Object.entries(value).map(([key, entry]) => [
63971
64724
  key,
@@ -66503,6 +67256,7 @@ var init_project_document_read = __esm({
66503
67256
  });
66504
67257
 
66505
67258
  // src/workspace.ts
67259
+ import { createHash as createHash2 } from "node:crypto";
66506
67260
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
66507
67261
  import { dirname, join as join2, resolve } from "node:path";
66508
67262
  function recordStateKey(recordKind, recordId) {
@@ -66735,9 +67489,12 @@ function matchUnityScalarField(content, field) {
66735
67489
  function readWorkspaceState(root, options = {}) {
66736
67490
  const statePath = join2(root, NEO_STATE_DIR, NEO_STATE_FILE);
66737
67491
  if (!existsSync2(statePath)) {
67492
+ options.onSourceRead?.(null);
66738
67493
  return { records: {} };
66739
67494
  }
66740
- const parsed = JSON.parse(readFileSync2(statePath, "utf8"));
67495
+ const source = readFileSync2(statePath, "utf8");
67496
+ options.onSourceRead?.(source);
67497
+ const parsed = JSON.parse(source);
66741
67498
  if (typeof parsed !== "object" || parsed === null) {
66742
67499
  throw new Error(`"${statePath}" must contain a JSON object.`);
66743
67500
  }
@@ -66780,10 +67537,20 @@ function loadWorkspace(startDir, options = {}) {
66780
67537
  `No "${NEO_CONFIG_FILE}" found in "${startDir}" or any parent directory. Run "neo init" first.`
66781
67538
  );
66782
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
+ });
66783
67549
  return {
66784
67550
  root,
66785
67551
  config: readWorkspaceConfig(root),
66786
- state: readWorkspaceState(root, options)
67552
+ state,
67553
+ ...stateSourceSha256 === void 0 ? {} : { stateSourceSha256 }
66787
67554
  };
66788
67555
  }
66789
67556
  var NEO_CONFIG_FILE, NEO_STATE_DIR, NEO_STATE_FILE, CURRENT_FORMAT_VERSION;
@@ -72142,7 +72909,7 @@ var init_animation_clips = __esm({
72142
72909
 
72143
72910
  // src/project-source/materialized-construction-cache.ts
72144
72911
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
72145
- import { createHash as createHash2 } from "node:crypto";
72912
+ import { createHash as createHash3 } from "node:crypto";
72146
72913
  import { dirname as dirname2, join as join3 } from "node:path";
72147
72914
  function createMaterializedConstructionExpressionsV1(args) {
72148
72915
  const warm = args.useBuildCaches ? readMaterializedConstructionBuildCacheV1(args.root, args.state) : null;
@@ -72234,7 +73001,7 @@ function writeMaterializedConstructionBuildCacheV1(root, state, expressions) {
72234
73001
  renameSync(temporary, file);
72235
73002
  }
72236
73003
  function materializedConstructionStateFingerprint(state) {
72237
- const hash = createHash2("sha256");
73004
+ const hash = createHash3("sha256");
72238
73005
  hash.update(String(MATERIALIZED_CONSTRUCTION_BUILD_CACHE_REVISION));
72239
73006
  for (const [key, record3] of Object.entries(state.records).sort(
72240
73007
  ([left], [right]) => left.localeCompare(right)
@@ -73060,10 +73827,10 @@ var init_neo_script_recompile_scope = __esm({
73060
73827
  });
73061
73828
 
73062
73829
  // ../src/database/project-content-hash.ts
73063
- import { createHash as createHash3 } from "node:crypto";
73830
+ import { createHash as createHash4 } from "node:crypto";
73064
73831
  function hashCanonicalJson(value) {
73065
73832
  const canonicalJson = canonicalJsonStringify(value);
73066
- return createHash3("sha256").update(canonicalJson).digest("hex");
73833
+ return createHash4("sha256").update(canonicalJson).digest("hex");
73067
73834
  }
73068
73835
  var init_project_content_hash = __esm({
73069
73836
  "../src/database/project-content-hash.ts"() {
@@ -73130,7 +73897,7 @@ var init_project_fingerprint = __esm({
73130
73897
 
73131
73898
  // src/project-source/neoscript-build-cache.ts
73132
73899
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
73133
- import { createHash as createHash4 } from "node:crypto";
73900
+ import { createHash as createHash5 } from "node:crypto";
73134
73901
  import { dirname as dirname3, join as join4 } from "node:path";
73135
73902
  function loadOrBuildNeoScriptProjectV1(root, document) {
73136
73903
  const fingerprint = neoScriptProjectFingerprint(document);
@@ -73157,7 +73924,7 @@ function loadOrBuildNeoScriptProjectV1(root, document) {
73157
73924
  return project;
73158
73925
  }
73159
73926
  function neoScriptProjectFingerprint(document) {
73160
- return createHash4("sha256").update(
73927
+ return createHash5("sha256").update(
73161
73928
  canonicalJsonStringify(neoScriptCompilationProjectContract(document))
73162
73929
  ).digest("hex");
73163
73930
  }
@@ -92280,7 +93047,7 @@ var init_project_documents = __esm({
92280
93047
 
92281
93048
  // src/project-source/project-document-cache.ts
92282
93049
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
92283
- import { createHash as createHash5 } from "node:crypto";
93050
+ import { createHash as createHash6 } from "node:crypto";
92284
93051
  import { dirname as dirname4, join as join5 } from "node:path";
92285
93052
  function readProjectSourceAnalysisBuildCacheV4(root, sources) {
92286
93053
  try {
@@ -92404,7 +93171,7 @@ function writeProjectSourceDocumentBuildCacheV1(root, sources, documents) {
92404
93171
  renameSync3(temporary, file);
92405
93172
  }
92406
93173
  function projectSourceFileFingerprint(source) {
92407
- 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");
92408
93175
  }
92409
93176
  function isCachedProjectSourceDocument(value, source) {
92410
93177
  if (value === null || typeof value !== "object") return false;
@@ -92412,7 +93179,7 @@ function isCachedProjectSourceDocument(value, source) {
92412
93179
  return document.kind === source.kind && document.sourceText === source.text && Array.isArray(document.declarations) && Array.isArray(document.diagnostics);
92413
93180
  }
92414
93181
  function projectSourceFingerprint(sources) {
92415
- const hash = createHash5("sha256");
93182
+ const hash = createHash6("sha256");
92416
93183
  hash.update(String(PROJECT_SOURCE_BUILD_CACHE_REVISION));
92417
93184
  hash.update("\0");
92418
93185
  hash.update(String(NEOSCRIPT_COMPILER_REVISION));
@@ -92440,7 +93207,7 @@ var init_project_document_cache = __esm({
92440
93207
  });
92441
93208
 
92442
93209
  // src/project-source/project-files.ts
92443
- import { createHash as createHash6 } from "node:crypto";
93210
+ import { createHash as createHash7 } from "node:crypto";
92444
93211
  import {
92445
93212
  existsSync as existsSync3,
92446
93213
  mkdirSync as mkdirSync6,
@@ -92687,7 +93454,7 @@ function discoverProjectBinariesV4(root, explicit, ignoredPaths = []) {
92687
93454
  }).sort((left, right) => compareCodePoints(left.path, right.path));
92688
93455
  }
92689
93456
  function sha256Bytes(bytes) {
92690
- return createHash6("sha256").update(bytes).digest("hex");
93457
+ return createHash7("sha256").update(bytes).digest("hex");
92691
93458
  }
92692
93459
  function sha256File(path) {
92693
93460
  return sha256Bytes(readFileSync6(path));
@@ -98140,7 +98907,7 @@ var init_script = __esm({
98140
98907
  });
98141
98908
 
98142
98909
  // ../src/database/project-source-identity.ts
98143
- import { createHash as createHash7 } from "node:crypto";
98910
+ import { createHash as createHash8 } from "node:crypto";
98144
98911
  function hashProjectSourceFiles(inputFiles) {
98145
98912
  const files = normalizeSourceFiles(inputFiles);
98146
98913
  const bytes = Buffer.from(JSON.stringify({ version: 1, files }), "utf8");
@@ -98149,7 +98916,7 @@ function hashProjectSourceFiles(inputFiles) {
98149
98916
  `Project source identity is ${bytes.byteLength} bytes; the limit is ${MAX_SOURCE_BYTES} bytes.`
98150
98917
  );
98151
98918
  }
98152
- return createHash7("sha256").update(bytes).digest("hex");
98919
+ return createHash8("sha256").update(bytes).digest("hex");
98153
98920
  }
98154
98921
  function normalizeSourceFiles(inputFiles) {
98155
98922
  if (inputFiles.length > MAX_SOURCE_FILES) {
@@ -98241,14 +99008,14 @@ var init_project_source_identity = __esm({
98241
99008
 
98242
99009
  // src/push-hook.ts
98243
99010
  import { spawn } from "node:child_process";
98244
- import { createHash as createHash8 } from "node:crypto";
99011
+ import { createHash as createHash9 } from "node:crypto";
98245
99012
  import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync as readdirSync4 } from "node:fs";
98246
99013
  import { join as join13, relative as relative3, sep as sep3 } from "node:path";
98247
- function fingerprintPushInputs(workspace) {
99014
+ function fingerprintPushInputs(workspace, options = {}) {
98248
99015
  const paths = /* @__PURE__ */ new Set([
98249
99016
  join13(workspace.root, "neo.json"),
98250
99017
  ...listProjectSourceFilesV4(workspace.root),
98251
- ...listProjectTestFilesV1(workspace.root),
99018
+ ...options.includeTests === false ? [] : listProjectTestFilesV1(workspace.root),
98252
99019
  ...workspace.config.unityConfigPath === void 0 ? [] : [join13(workspace.root, workspace.config.unityConfigPath)]
98253
99020
  ]);
98254
99021
  const visitManaged = (directory) => {
@@ -98262,7 +99029,7 @@ function fingerprintPushInputs(workspace) {
98262
99029
  };
98263
99030
  visitManaged(join13(workspace.root, "Files", "Images"));
98264
99031
  visitManaged(join13(workspace.root, "Files", "AudioClips"));
98265
- const hash = createHash8("sha256");
99032
+ const hash = createHash9("sha256");
98266
99033
  for (const path of [...paths].sort()) {
98267
99034
  const name = relative3(workspace.root, path).split(sep3).join("/");
98268
99035
  hash.update(name);
@@ -98919,7 +99686,7 @@ __export(push_exports, {
98919
99686
  runPush: () => runPush,
98920
99687
  stripServerDerivedNeoScript: () => stripServerDerivedNeoScript
98921
99688
  });
98922
- import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
99689
+ import { createHash as createHash10, randomUUID as randomUUID2 } from "node:crypto";
98923
99690
  import {
98924
99691
  mkdirSync as mkdirSync10,
98925
99692
  writeFileSync as writeFileSync10,
@@ -99702,7 +100469,7 @@ async function runPush(workspace, options, preparationOverride) {
99702
100469
  version: 1,
99703
100470
  projectFingerprint: `sha256:${preparedLocal.source.sourceHash}`,
99704
100471
  inputFingerprint: preparedInputFingerprint,
99705
- documentSha256: createHash9("sha256").update(documentJson).digest("hex"),
100472
+ documentSha256: createHash10("sha256").update(documentJson).digest("hex"),
99706
100473
  document: candidate.document
99707
100474
  })}
99708
100475
  `,
@@ -101711,7 +102478,7 @@ var init_registry2 = __esm({
101711
102478
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
101712
102479
  formatVersion: 3,
101713
102480
  contractVersion: "3.9",
101714
- cliVersion: "0.26.1",
102481
+ cliVersion: "0.26.2",
101715
102482
  projectFileUploadBatchSize: 32,
101716
102483
  documentRecords: {
101717
102484
  member: {
@@ -103004,7 +103771,7 @@ __export(test_exports, {
103004
103771
  maintainNeoTestBuildCache: () => maintainNeoTestBuildCache,
103005
103772
  runTest: () => runTest
103006
103773
  });
103007
- import { createHash as createHash10, randomUUID as randomUUID3 } from "node:crypto";
103774
+ import { createHash as createHash11, randomUUID as randomUUID3 } from "node:crypto";
103008
103775
  import {
103009
103776
  existsSync as existsSync12,
103010
103777
  mkdirSync as mkdirSync11,
@@ -103118,10 +103885,11 @@ function errorMessage2(error) {
103118
103885
  function findDelegateTargetMemberId(value) {
103119
103886
  return isRecord10(value) && typeof value.memberId === "string" ? value.memberId : null;
103120
103887
  }
103121
- function makeEvaluatorContext(rawDocument, interceptor, documentAlreadyIsolated = false) {
103122
- const isolatedDocument = documentAlreadyIsolated ? rawDocument : structuredClone(rawDocument);
103123
- const document = readDocumentArrays(isolatedDocument);
103124
- 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) : [];
103125
103893
  const vm = {
103126
103894
  project: document.project,
103127
103895
  members: document.members,
@@ -103146,10 +103914,19 @@ function makeEvaluatorContext(rawDocument, interceptor, documentAlreadyIsolated
103146
103914
  }
103147
103915
  )
103148
103916
  };
103149
- return createNeoScriptEvaluationRuntime({
103917
+ const created = {
103150
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,
103151
103928
  thisValue: null,
103152
- rootValue: buildRootValue(document),
103929
+ rootValue: base.rootValue,
103153
103930
  dialogueContext: null,
103154
103931
  callInterceptor: interceptor,
103155
103932
  nativeFunctionErrorCheckBehavior: "strict"
@@ -103216,14 +103993,15 @@ function preparedHookCandidate(workspace) {
103216
103993
  );
103217
103994
  }
103218
103995
  const documentJson = JSON.stringify(parsed.document);
103219
- if (createHash10("sha256").update(documentJson).digest("hex") !== parsed.documentSha256) {
103996
+ if (createHash11("sha256").update(documentJson).digest("hex") !== parsed.documentSha256) {
103220
103997
  throw new NeoTestPreparedCandidateError(
103221
103998
  "Configured push hook candidate project document failed checksum verification."
103222
103999
  );
103223
104000
  }
103224
104001
  return {
103225
104002
  document: parsed.document,
103226
- sourceHash: configuredFingerprint.replace(/^sha256:/u, "")
104003
+ sourceHash: configuredFingerprint.replace(/^sha256:/u, ""),
104004
+ documentSha256: parsed.documentSha256
103227
104005
  };
103228
104006
  } catch (error) {
103229
104007
  if (error instanceof NeoTestPreparedCandidateError) throw error;
@@ -103232,11 +104010,79 @@ function preparedHookCandidate(workspace) {
103232
104010
  );
103233
104011
  }
103234
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
+ }
103235
104081
  function compileSpec(workspace, document, absolutePath, projectCompilationHash2) {
103236
104082
  const scriptDocument = readDocumentArrays(document);
103237
104083
  const path = relative5(workspace.root, absolutePath).split(sep5).join("/");
103238
104084
  const source = readFileSync16(absolutePath, "utf8");
103239
- const sourceHash = createHash10("sha256").update(source).digest("hex");
104085
+ const sourceHash = createHash11("sha256").update(source).digest("hex");
103240
104086
  const artifactPath = join16(
103241
104087
  workspace.root,
103242
104088
  ".neo",
@@ -103247,7 +104093,7 @@ function compileSpec(workspace, document, absolutePath, projectCompilationHash2)
103247
104093
  );
103248
104094
  try {
103249
104095
  const cached = JSON.parse(readFileSync16(artifactPath, "utf8"));
103250
- 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)) {
103251
104097
  return {
103252
104098
  path,
103253
104099
  source,
@@ -103279,15 +104125,16 @@ function compileSpec(workspace, document, absolutePath, projectCompilationHash2)
103279
104125
  compilerRevision: NEOSCRIPT_COMPILER_REVISION,
103280
104126
  projectCompilationHash: projectCompilationHash2,
103281
104127
  sourceHash,
103282
- artifactSha256: createHash10("sha256").update(JSON.stringify(compiled.action)).digest("hex"),
104128
+ artifactSha256: createHash11("sha256").update(JSON.stringify(compiled.action)).digest("hex"),
103283
104129
  action: compiled.action
103284
104130
  })}
103285
104131
  `
103286
104132
  );
103287
104133
  return compiled;
103288
104134
  }
103289
- function projectCompilationHash(projectSourceHash, document) {
103290
- 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");
103291
104138
  }
103292
104139
  function selectedSpecPaths(workspace, selectors) {
103293
104140
  const all = listProjectTestFilesV1(workspace.root);
@@ -103520,7 +104367,7 @@ function registerSpec(spec, document) {
103520
104367
  }
103521
104368
  names.add(fullName);
103522
104369
  const test = {
103523
- id: `sha256:${createHash10("sha256").update(`${spec.path}\0${namePath}`).digest("hex")}`,
104370
+ id: `sha256:${createHash11("sha256").update(`${spec.path}\0${namePath}`).digest("hex")}`,
103524
104371
  name: namePath,
103525
104372
  fullName,
103526
104373
  body: portableDelegate(call.args[1]),
@@ -103871,24 +104718,32 @@ function interceptTestCall(environment, call) {
103871
104718
  throw error;
103872
104719
  }
103873
104720
  }
103874
- function createTestEnvironment(document, documentAlreadyIsolated = false) {
104721
+ function createTestEnvironment(document) {
103875
104722
  const environment = {
103876
104723
  baseDocument: document,
103877
104724
  context: void 0,
103878
104725
  mocks: /* @__PURE__ */ new Map(),
103879
104726
  mocksByMember: /* @__PURE__ */ new Map(),
103880
- nextMockId: 1
104727
+ nextMockId: 1,
104728
+ hasStateChanges: false
103881
104729
  };
103882
104730
  environment.context = makeEvaluatorContext(
103883
104731
  document,
103884
- (call) => interceptTestCall(environment, call),
103885
- documentAlreadyIsolated
104732
+ (call) => interceptTestCall(environment, call)
103886
104733
  );
103887
104734
  return environment;
103888
104735
  }
103889
104736
  function snapshotEnvironmentDocument(environment) {
103890
- const snapshot = structuredClone(environment.baseDocument);
103891
- 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];
103892
104747
  const byId = /* @__PURE__ */ new Map();
103893
104748
  values.forEach((value, index) => {
103894
104749
  if (isRecord10(value) && typeof value.id === "string")
@@ -103905,20 +104760,17 @@ function snapshotEnvironmentDocument(environment) {
103905
104760
  values[index] = cloned;
103906
104761
  }
103907
104762
  };
103908
- for (const row of environment.context.__valueOverlay?.values() ?? [])
103909
- mergeRow(row);
103910
- for (const row of environment.context.__runtimeSessionValues?.values() ?? [])
103911
- mergeRow(row);
104763
+ for (const row of overlay?.values() ?? []) mergeRow(row);
104764
+ for (const row of runtime?.values() ?? []) mergeRow(row);
103912
104765
  snapshot.values = values;
103913
- const members = Array.isArray(snapshot.members) ? snapshot.members : [];
103914
- const bindings = new Map([
103915
- ...environment.context.__saveStaticBindings ?? [],
103916
- ...environment.context.__sessionStaticBindings ?? []
103917
- ]);
103918
- for (const member of members) {
103919
- if (!isRecord10(member) || typeof member.id !== "string") continue;
103920
- if (bindings.has(member.id))
103921
- 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
+ });
103922
104774
  }
103923
104775
  return snapshot;
103924
104776
  }
@@ -103939,10 +104791,7 @@ function copyMocks(source, target) {
103939
104791
  }
103940
104792
  }
103941
104793
  function cloneTestEnvironment(source) {
103942
- const cloned = createTestEnvironment(
103943
- snapshotEnvironmentDocument(source),
103944
- true
103945
- );
104794
+ const cloned = createTestEnvironment(snapshotEnvironmentDocument(source));
103946
104795
  copyMocks(source, cloned);
103947
104796
  return cloned;
103948
104797
  }
@@ -103953,10 +104802,14 @@ function runCallback(environment, delegate, deadlineMs) {
103953
104802
  __indexes: void 0,
103954
104803
  wallClockDeadlineMs: deadlineMs
103955
104804
  });
103956
- evaluateNSDelegate(
103957
- bindNeoScriptDelegateToContext(delegate, environment.context),
103958
- environment.context
103959
- );
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
+ }
103960
104813
  }
103961
104814
  function failureFor(error, file, position, member = null) {
103962
104815
  const timeout = error instanceof NeoScriptWallClockTimeoutError;
@@ -103982,18 +104835,22 @@ function suiteHasSelectedTests(suite, selected2) {
103982
104835
  async function executeRegisteredSpec(registered, document, selected2, timeoutMs, interrupted) {
103983
104836
  const results = [];
103984
104837
  const fileFailures = [];
104838
+ let testDurationMs = 0;
103985
104839
  const executeSuite = async (suite, parentEnvironment, inheritedFailures) => {
103986
104840
  if (!suiteHasSelectedTests(suite, selected2)) return;
103987
104841
  const fixture = cloneTestEnvironment(parentEnvironment);
103988
104842
  const suiteFailures = [...inheritedFailures];
103989
104843
  const suiteDeadline = Date.now() + timeoutMs;
103990
104844
  for (const hook of suite.beforeAll) {
104845
+ const hookStarted = performance.now();
103991
104846
  try {
103992
104847
  runCallback(fixture, hook, suiteDeadline);
103993
104848
  } catch (error) {
103994
104849
  suiteFailures.push(
103995
104850
  failureFor(error, registered.spec.path, suite, "beforeAll")
103996
104851
  );
104852
+ } finally {
104853
+ testDurationMs += performance.now() - hookStarted;
103997
104854
  }
103998
104855
  }
103999
104856
  for (const test of suite.tests) {
@@ -104001,6 +104858,8 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
104001
104858
  const started = performance.now();
104002
104859
  const deadline = Date.now() + timeoutMs;
104003
104860
  const environment = cloneTestEnvironment(fixture);
104861
+ const startupBeforeTestMs = performance.now() - started;
104862
+ const testStarted = performance.now();
104004
104863
  const failures = [...suiteFailures];
104005
104864
  let setupFailed = failures.length > 0;
104006
104865
  if (!setupFailed) {
@@ -104037,19 +104896,24 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
104037
104896
  }
104038
104897
  }
104039
104898
  }
104040
- 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;
104041
104906
  results.push({
104042
104907
  id: test.id,
104043
104908
  file: registered.spec.path,
104044
104909
  name: test.name,
104045
104910
  fullName: test.fullName,
104046
104911
  status: failures.length === 0 ? "passed" : "failed",
104047
- durationMs,
104912
+ durationMs: startupDurationMs + currentTestDurationMs,
104913
+ startupDurationMs,
104914
+ testDurationMs: currentTestDurationMs,
104048
104915
  failures
104049
104916
  });
104050
- await new Promise((resolvePromise) => {
104051
- setImmediate(resolvePromise);
104052
- });
104053
104917
  if (interrupted()) break;
104054
104918
  }
104055
104919
  for (const child of suite.suites) {
@@ -104057,12 +104921,15 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
104057
104921
  await executeSuite(child, fixture, suiteFailures);
104058
104922
  }
104059
104923
  for (const hook of suite.afterAll) {
104924
+ const hookStarted = performance.now();
104060
104925
  try {
104061
104926
  runCallback(fixture, hook, Date.now() + timeoutMs);
104062
104927
  } catch (error) {
104063
104928
  fileFailures.push(
104064
104929
  failureFor(error, registered.spec.path, suite, "afterAll")
104065
104930
  );
104931
+ } finally {
104932
+ testDurationMs += performance.now() - hookStarted;
104066
104933
  }
104067
104934
  }
104068
104935
  };
@@ -104073,7 +104940,7 @@ async function executeRegisteredSpec(registered, document, selected2, timeoutMs,
104073
104940
  results.sort(
104074
104941
  (left, right) => (registrationOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (registrationOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)
104075
104942
  );
104076
- return { tests: results, failures: fileFailures };
104943
+ return { tests: results, failures: fileFailures, testDurationMs };
104077
104944
  }
104078
104945
  function atomicWrite(path, content) {
104079
104946
  mkdirSync11(dirname9(path), { recursive: true });
@@ -104126,8 +104993,12 @@ function maintainNeoTestBuildCache(root, maxBytes = TEST_BUILD_CACHE_LIMIT_BYTES
104126
104993
  total -= file.size;
104127
104994
  }
104128
104995
  }
104996
+ function formatTestDuration(durationMs) {
104997
+ return `${durationMs < 10 ? durationMs.toFixed(2) : durationMs.toFixed(1)}ms`;
104998
+ }
104129
104999
  async function runTest(workspace, options, dependencies = {}) {
104130
105000
  const started = Date.now();
105001
+ const performanceStarted = performance.now();
104131
105002
  const testBuildRoot = join16(workspace.root, ".neo", "test-build");
104132
105003
  maintainNeoTestBuildCache(testBuildRoot);
104133
105004
  const startedAt = new Date(started).toISOString();
@@ -104140,6 +105011,7 @@ async function runTest(workspace, options, dependencies = {}) {
104140
105011
  const selectedTestCountByFile = /* @__PURE__ */ new Map();
104141
105012
  const fileFailuresByPath = /* @__PURE__ */ new Map();
104142
105013
  const diagnostics = [];
105014
+ let testDurationMs = 0;
104143
105015
  let exitCode = 0;
104144
105016
  let interrupted = false;
104145
105017
  const handleSigint = () => {
@@ -104155,18 +105027,47 @@ async function runTest(workspace, options, dependencies = {}) {
104155
105027
  process.on("SIGINT", handleSigint);
104156
105028
  let phase = "prepare";
104157
105029
  try {
104158
- 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
+ }
104159
105053
  assertNotInterrupted();
104160
- if (candidate.document === null) {
105054
+ if (candidate === null) {
104161
105055
  throw new NeoTestReportWriteError(
104162
- "Local test preparation did not produce a project document."
105056
+ "Local test preparation produced no reusable candidate."
104163
105057
  );
104164
105058
  }
105059
+ if (shouldCacheCandidate && candidateInputFingerprint !== null) {
105060
+ candidate = cacheTestCandidate(workspace, candidateInputFingerprint, {
105061
+ document: candidate.document,
105062
+ sourceHash: candidate.sourceHash
105063
+ });
105064
+ }
104165
105065
  projectFingerprint = `sha256:${candidate.sourceHash}`;
104166
105066
  const rawDocument = documentRecord(candidate.document);
104167
105067
  const compilationHash = projectCompilationHash(
104168
105068
  candidate.sourceHash,
104169
- rawDocument
105069
+ rawDocument,
105070
+ candidate.documentSha256
104170
105071
  );
104171
105072
  phase = "select";
104172
105073
  let selectedPaths;
@@ -104225,6 +105126,7 @@ async function runTest(workspace, options, dependencies = {}) {
104225
105126
  () => interrupted
104226
105127
  );
104227
105128
  results.push(...executed.tests);
105129
+ testDurationMs += executed.testDurationMs;
104228
105130
  fileFailuresByPath.set(entry.spec.path, executed.failures);
104229
105131
  assertNotInterrupted();
104230
105132
  }
@@ -104238,14 +105140,14 @@ async function runTest(workspace, options, dependencies = {}) {
104238
105140
  compilerRevision: NEOSCRIPT_COMPILER_REVISION,
104239
105141
  evaluatorRevision: 4,
104240
105142
  projectFingerprint,
104241
- configurationSha256: createHash10("sha256").update(JSON.stringify(workspace.config.test ?? {})).digest("hex"),
105143
+ configurationSha256: createHash11("sha256").update(JSON.stringify(workspace.config.test ?? {})).digest("hex"),
104242
105144
  dependencyGraph: Object.fromEntries(
104243
105145
  specs.map((spec) => [spec.path, [projectFingerprint]])
104244
105146
  ),
104245
105147
  artifacts: specs.map((spec) => ({
104246
105148
  path: spec.path,
104247
- sourceSha256: createHash10("sha256").update(spec.source).digest("hex"),
104248
- 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")
104249
105151
  }))
104250
105152
  };
104251
105153
  atomicWrite(
@@ -104273,7 +105175,16 @@ async function runTest(workspace, options, dependencies = {}) {
104273
105175
  0
104274
105176
  );
104275
105177
  const skipped = Math.max(0, selectedTestTotal - results.length);
104276
- 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
+ );
104277
105188
  const testFiles = specs.map((spec) => {
104278
105189
  const tests = results.filter((result) => result.file === spec.path);
104279
105190
  const failures = fileFailuresByPath.get(spec.path) ?? [];
@@ -104290,7 +105201,7 @@ async function runTest(workspace, options, dependencies = {}) {
104290
105201
  success: exitCode === 0,
104291
105202
  projectFingerprint,
104292
105203
  seed: Number.parseInt(
104293
- 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),
104294
105205
  16
104295
105206
  ),
104296
105207
  selection: {
@@ -104305,7 +105216,10 @@ async function runTest(workspace, options, dependencies = {}) {
104305
105216
  failed,
104306
105217
  skipped,
104307
105218
  todo: 0,
104308
- durationMs
105219
+ durationMs,
105220
+ startupDurationMs,
105221
+ sharedStartupDurationMs,
105222
+ testDurationMs
104309
105223
  },
104310
105224
  testFiles,
104311
105225
  diagnostics,
@@ -104330,9 +105244,12 @@ async function runTest(workspace, options, dependencies = {}) {
104330
105244
  if (options.reporter === "json") {
104331
105245
  if (options.outputFile === null) process.stdout.write(serialized);
104332
105246
  } else {
105247
+ console.log(
105248
+ `\u21BB shared startup (${formatTestDuration(report.summary.sharedStartupDurationMs)})`
105249
+ );
104333
105250
  for (const result of results) {
104334
105251
  console.log(
104335
- `${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)})`
104336
105253
  );
104337
105254
  for (const failure of result.failures)
104338
105255
  console.log(` ${failure.message}`);
@@ -104353,7 +105270,7 @@ async function runTest(workspace, options, dependencies = {}) {
104353
105270
  for (const diagnostic of diagnostics)
104354
105271
  console.log(`\u2717 ${diagnostic.message}`);
104355
105272
  console.log(
104356
- `${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)})`
104357
105274
  );
104358
105275
  }
104359
105276
  if (!report.success) process.exitCode = exitCode;
@@ -104400,7 +105317,7 @@ async function inspectNeoTestCompilation(workspace, dependencies = {}) {
104400
105317
  errors
104401
105318
  };
104402
105319
  }
104403
- 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;
104404
105321
  var init_test = __esm({
104405
105322
  "src/commands/test.ts"() {
104406
105323
  "use strict";
@@ -104415,6 +105332,7 @@ var init_test = __esm({
104415
105332
  init_push_hook();
104416
105333
  TEST_BUILD_CACHE_LIMIT_BYTES = 512 * 1024 * 1024;
104417
105334
  ABANDONED_TEMP_MAX_AGE_MS = 60 * 60 * 1e3;
105335
+ TEST_CANDIDATE_CACHE_REVISION = 1;
104418
105336
  NeoTestUsageError = class extends Error {
104419
105337
  name = "NeoTestUsageError";
104420
105338
  };
@@ -104446,6 +105364,7 @@ var init_test = __esm({
104446
105364
  received;
104447
105365
  };
104448
105366
  ERROR_SOURCE_POSITIONS = /* @__PURE__ */ new WeakMap();
105367
+ SHARED_EVALUATOR_BASES = /* @__PURE__ */ new WeakMap();
104449
105368
  REGISTRATION_IDS = /* @__PURE__ */ new Set([
104450
105369
  NEO_TEST_IDS.describe,
104451
105370
  NEO_TEST_IDS.test,
@@ -107841,7 +108760,8 @@ function loadWorkspaceForCommand(args) {
107841
108760
  // A reset reconstructs the working copy from the server and may therefore
107842
108761
  // discard an unsupported pre-cutover cache. No other command gets this
107843
108762
  // exception, so format 2 never becomes an active compatibility read path.
107844
- discardLegacyFormat2State: args.command === "pull" && boolFlag(args, "reset")
108763
+ discardLegacyFormat2State: args.command === "pull" && boolFlag(args, "reset"),
108764
+ fingerprintStateSource: args.command === "test"
107845
108765
  });
107846
108766
  const apiOverride = stringFlag(args, "api");
107847
108767
  if (apiOverride !== null) {
@@ -108019,7 +108939,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
108019
108939
  async function main() {
108020
108940
  const args = parseArgs(process.argv.slice(2));
108021
108941
  if (args.command === "--version") {
108022
- console.log("0.26.1");
108942
+ console.log("0.26.2");
108023
108943
  return;
108024
108944
  }
108025
108945
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {