com.wallstop-studios.dxmessaging 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +211 -2
  2. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +69 -62
  3. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +3 -3
  4. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +3 -3
  5. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +3 -3
  6. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +3 -3
  7. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +3 -3
  8. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  9. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +15 -2
  10. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  11. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +2 -2
  12. package/Editor/AssemblyInfo.cs.meta +9 -1
  13. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +24 -15
  14. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +9 -1
  15. package/Editor/DxMessagingEditorIdle.cs +62 -0
  16. package/{Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta → Editor/DxMessagingEditorIdle.cs.meta} +1 -1
  17. package/Editor/DxMessagingEditorInitializer.cs +112 -15
  18. package/Editor/DxMessagingEditorInitializer.cs.meta +9 -1
  19. package/Editor/DxMessagingEditorLog.cs +32 -0
  20. package/Editor/DxMessagingEditorLog.cs.meta +11 -0
  21. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +135 -12
  22. package/Editor/Settings/DxMessagingSettings.cs +92 -31
  23. package/Editor/Settings/DxMessagingSettings.cs.meta +9 -1
  24. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +9 -1
  25. package/Editor/SetupCscRsp.cs +339 -173
  26. package/Editor/SetupCscRsp.cs.meta +9 -1
  27. package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
  28. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +9 -1
  29. package/README.md +17 -18
  30. package/Runtime/AssemblyInfo.cs.meta +9 -1
  31. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +9 -1
  32. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +9 -1
  33. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs +2 -4
  34. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +9 -1
  35. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +9 -1
  36. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +9 -1
  37. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs +56 -0
  38. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs.meta +11 -0
  39. package/Runtime/Core/DataStructure/CyclicBuffer.cs +44 -26
  40. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +9 -1
  41. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +9 -1
  42. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +9 -1
  43. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +9 -1
  44. package/Runtime/Core/Extensions/EnumExtensions.cs +6 -5
  45. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +9 -1
  46. package/Runtime/Core/Extensions/IListExtensions.cs.meta +9 -1
  47. package/Runtime/Core/Extensions/MessageExtensions.cs +0 -60
  48. package/Runtime/Core/Helper/MessageCache.cs.meta +9 -1
  49. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +9 -1
  50. package/Runtime/Core/InstanceId.cs +25 -1
  51. package/Runtime/Core/Internal/DxUnsafe.cs +60 -0
  52. package/Runtime/Core/Internal/DxUnsafe.cs.meta +11 -0
  53. package/Runtime/Core/Internal/FlatDispatch.cs +198 -0
  54. package/Runtime/Core/Internal/FlatDispatch.cs.meta +11 -0
  55. package/Runtime/Core/Internal/TypedSlots.cs +5 -21
  56. package/Runtime/Core/MessageBus/IMessageBus.cs +12 -12
  57. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs +1 -0
  58. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +7 -6
  59. package/Runtime/Core/MessageBus/MessageBus.cs +2313 -2936
  60. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +187 -14
  61. package/Runtime/Core/MessageHandler.cs +1023 -1143
  62. package/Runtime/Core/MessageRegistrationToken.cs +425 -47
  63. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +9 -1
  64. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +9 -1
  65. package/Runtime/Core/Messages/StringMessage.cs.meta +9 -1
  66. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +9 -1
  67. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +9 -1
  68. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +9 -1
  69. package/Runtime/Unity/MessageAwareComponent.cs +46 -1
  70. package/Runtime/Unity/MessagingComponent.cs +43 -10
  71. package/Runtime/WallstopStudios.DxMessaging.asmdef +1 -1
  72. package/Samples~/DI/README.md +7 -7
  73. package/SourceGenerators/Directory.Build.props +50 -3
  74. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +96 -63
  75. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +745 -87
  76. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +9 -1
  77. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +39 -46
  78. package/SourceGenerators/global.json +7 -0
  79. package/SourceGenerators/global.json.meta +7 -0
  80. package/package.json +27 -40
  81. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +0 -51
@@ -31,10 +31,9 @@ namespace DxMessaging.Editor
31
31
  private static readonly string SourceGeneratorDllName =
32
32
  "WallstopStudios.DxMessaging.SourceGenerators.dll";
33
33
 
34
- // The analyzer DLL is a SEPARATE assembly compiled against Roslyn 3.8.0 because Unity 2021's
35
- // analyzer loader silently rejects analyzer DLLs built against Roslyn 4.x. The source-
36
- // generator DLL above stays at Roslyn 4.x because it uses IIncrementalGenerator (4.0+).
37
- // Both DLLs ship side-by-side and both need the RoslynAnalyzer label.
34
+ // The analyzer DLL is a SEPARATE assembly, but both compiler-host DLLs are pinned to
35
+ // Unity 2021-compatible Roslyn 3.8.0. They ship side-by-side and both need the
36
+ // RoslynAnalyzer label.
38
37
  private static readonly string AnalyzerDllName = "WallstopStudios.DxMessaging.Analyzer.dll";
39
38
 
40
39
  // The analyzer DLLs and shared Roslyn surface ship unconditionally; they're light enough
@@ -73,9 +72,36 @@ namespace DxMessaging.Editor
73
72
 
74
73
  static SetupCscRsp()
75
74
  {
76
- EditorApplication.delayCall += EnsureDLLsExistInAssets;
77
- EditorApplication.delayCall += EnsureCscRsp;
78
- EditorApplication.delayCall += EnsureAdditionalFileForIgnoreList;
75
+ ScheduleSetupStep(EnsureDLLsExistInAssets, "copy analyzer DLLs into Assets");
76
+ ScheduleSetupStep(EnsureCscRsp, "clean csc.rsp analyzer entries");
77
+ ScheduleAdditionalFileForIgnoreListSync();
78
+ }
79
+
80
+ internal static void ScheduleAdditionalFileForIgnoreListSync()
81
+ {
82
+ ScheduleSetupStep(
83
+ EnsureAdditionalFileForIgnoreList,
84
+ "sync csc.rsp base-call ignore additionalfile entry"
85
+ );
86
+ }
87
+
88
+ private static void ScheduleSetupStep(Action work, string description)
89
+ {
90
+ DxMessagingEditorIdle.ScheduleAssetDatabaseMutation(() =>
91
+ RunSetupStep(work, description)
92
+ );
93
+ }
94
+
95
+ private static void RunSetupStep(Action work, string description)
96
+ {
97
+ try
98
+ {
99
+ work();
100
+ }
101
+ catch (Exception ex)
102
+ {
103
+ DxMessagingEditorLog.LogError($"SetupCscRsp failed to {description}.", ex);
104
+ }
79
105
  }
80
106
 
81
107
  private static void EnsureDLLsExistInAssets()
@@ -149,21 +175,28 @@ namespace DxMessaging.Editor
149
175
  {
150
176
  bool importerDirty = false;
151
177
 
178
+ // A RoslynAnalyzer-labeled DLL must be EXCLUDED from every
179
+ // build platform, including the Editor, so Unity treats it as a
180
+ // C# compiler analyzer rather than a managed precompiled
181
+ // assembly. The same-named DLL is importable from two locations
182
+ // (the package's own Editor/Analyzers copy and this Assets
183
+ // copy); if either is an Editor-enabled precompiled assembly,
184
+ // Unity 2021 aborts with "Multiple precompiled assemblies with
185
+ // the same name". The Roslyn runtime dependencies in the same
186
+ // folder already ship Editor-disabled and never collide -- this
187
+ // converges the analyzer DLLs onto that proven-safe shape.
188
+ // NOTE: SetExcludeFromAnyPlatform is a no-op once
189
+ // CompatibleWithAnyPlatform is false, so the Editor platform is
190
+ // disabled through the effective SetCompatibleWithEditor API.
152
191
  if (importer.GetCompatibleWithAnyPlatform())
153
192
  {
154
193
  importer.SetCompatibleWithAnyPlatform(false);
155
194
  importerDirty = true;
156
195
  }
157
196
 
158
- if (importer.GetExcludeFromAnyPlatform("Editor"))
159
- {
160
- importer.SetExcludeFromAnyPlatform("Editor", false);
161
- importerDirty = true;
162
- }
163
-
164
- if (!importer.GetExcludeFromAnyPlatform("Standalone"))
197
+ if (importer.GetCompatibleWithEditor())
165
198
  {
166
- importer.SetExcludeFromAnyPlatform("Standalone", true);
199
+ importer.SetCompatibleWithEditor(false);
167
200
  importerDirty = true;
168
201
  }
169
202
 
@@ -176,10 +209,11 @@ namespace DxMessaging.Editor
176
209
  DllNames.Add(requiredDllName);
177
210
  break;
178
211
  }
179
- catch (Exception e)
212
+ catch (Exception ex)
180
213
  {
181
- Debug.LogError(
182
- $"Failed to copy {requiredDllName} to Assets, failed with {e}."
214
+ DxMessagingEditorLog.LogError(
215
+ $"Failed to copy {requiredDllName} to Assets.",
216
+ ex
183
217
  );
184
218
  }
185
219
  }
@@ -214,9 +248,14 @@ namespace DxMessaging.Editor
214
248
  }
215
249
 
216
250
  /// <summary>
217
- /// Synchronizes <c>csc.rsp</c> with the current set of analyzer arguments derived from the
218
- /// on-disk DLL roster.
251
+ /// Removes stale DxMessaging analyzer <c>-a:</c> entries from <c>csc.rsp</c>.
219
252
  /// </summary>
253
+ /// <remarks>
254
+ /// DxMessaging analyzers are activated solely through the RoslynAnalyzer-labeled
255
+ /// <c>Assets/Plugins/Editor/WallstopStudios.DxMessaging</c> copy. A second
256
+ /// <c>-a:</c> registration here double-loads the analyzer/source-generator, and
257
+ /// registering dependency DLLs as analyzers makes Unity's compiler path fragile.
258
+ /// </remarks>
220
259
  private static void EnsureCscRsp()
221
260
  {
222
261
  try
@@ -229,72 +268,12 @@ namespace DxMessaging.Editor
229
268
 
230
269
  string rspContent = File.ReadAllText(RspFilePath);
231
270
 
232
- // Get current valid analyzer arguments
233
- HashSet<string> currentAnalyzerArgs = new(
234
- GetAnalyzerArguments(),
235
- StringComparer.OrdinalIgnoreCase
271
+ string[] newLines = CleanDxMessagingAnalyzerLines(
272
+ rspContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries),
273
+ out bool foundStaleEntries
236
274
  );
237
275
 
238
- // Parse existing lines and filter out stale DxMessaging analyzer entries
239
- List<string> newLines = new();
240
- bool foundStaleEntries = false;
241
-
242
- foreach (
243
- string line in rspContent.Split(
244
- new[] { '\r', '\n' },
245
- StringSplitOptions.RemoveEmptyEntries
246
- )
247
- )
248
- {
249
- string trimmedLine = line.Trim();
250
-
251
- // Check if this is a DxMessaging analyzer line
252
- bool isDxMessagingAnalyzer =
253
- trimmedLine.StartsWith("-a:", StringComparison.OrdinalIgnoreCase)
254
- && (
255
- trimmedLine.Contains(
256
- "com.wallstop-studios.dxmessaging",
257
- StringComparison.OrdinalIgnoreCase
258
- )
259
- || trimmedLine.Contains(
260
- "WallstopStudios.DxMessaging",
261
- StringComparison.OrdinalIgnoreCase
262
- )
263
- );
264
-
265
- if (isDxMessagingAnalyzer)
266
- {
267
- // Only keep if it's in the current valid set
268
- if (currentAnalyzerArgs.Contains(trimmedLine))
269
- {
270
- newLines.Add(trimmedLine);
271
- }
272
- else
273
- {
274
- foundStaleEntries = true;
275
- }
276
- }
277
- else
278
- {
279
- // Keep all non-DxMessaging lines as-is
280
- newLines.Add(trimmedLine);
281
- }
282
- }
283
-
284
- // Add any new analyzer arguments that aren't already present
285
- bool foundNewEntries = false;
286
- foreach (string analyzerArgument in currentAnalyzerArgs)
287
- {
288
- if (!newLines.Contains(analyzerArgument, StringComparer.OrdinalIgnoreCase))
289
- {
290
- newLines.Add(analyzerArgument);
291
- foundNewEntries = true;
292
- }
293
- }
294
-
295
- bool modified = foundStaleEntries || foundNewEntries;
296
-
297
- if (modified)
276
+ if (foundStaleEntries)
298
277
  {
299
278
  // Write the cleaned up content
300
279
  string newContent = string.Join(Environment.NewLine, newLines);
@@ -309,18 +288,88 @@ namespace DxMessaging.Editor
309
288
  }
310
289
  catch (IOException ex)
311
290
  {
312
- Debug.LogError($"Failed to modify csc.rsp: {ex}");
291
+ DxMessagingEditorLog.LogError("Failed to modify csc.rsp.", ex);
313
292
  }
314
293
  }
315
294
 
295
+ internal static string[] CleanDxMessagingAnalyzerLines(
296
+ IEnumerable<string> lines,
297
+ out bool foundStaleEntries
298
+ )
299
+ {
300
+ List<string> newLines = new();
301
+ foundStaleEntries = false;
302
+
303
+ foreach (string line in lines ?? Array.Empty<string>())
304
+ {
305
+ string trimmedLine = line?.Trim();
306
+ if (string.IsNullOrEmpty(trimmedLine))
307
+ {
308
+ continue;
309
+ }
310
+
311
+ if (IsResponseFileComment(trimmedLine))
312
+ {
313
+ newLines.Add(trimmedLine);
314
+ continue;
315
+ }
316
+
317
+ List<string> retainedArguments = new();
318
+ bool removedFromLine = false;
319
+ foreach (string argument in SplitResponseFileArguments(trimmedLine))
320
+ {
321
+ if (IsDxMessagingAnalyzerArgument(argument))
322
+ {
323
+ foundStaleEntries = true;
324
+ removedFromLine = true;
325
+ continue;
326
+ }
327
+
328
+ retainedArguments.Add(argument);
329
+ }
330
+
331
+ if (removedFromLine)
332
+ {
333
+ if (retainedArguments.Count > 0)
334
+ {
335
+ newLines.Add(string.Join(" ", retainedArguments));
336
+ }
337
+ continue;
338
+ }
339
+
340
+ newLines.Add(trimmedLine);
341
+ }
342
+
343
+ return newLines.ToArray();
344
+ }
345
+
346
+ private static bool IsDxMessagingAnalyzerArgument(string trimmedLine)
347
+ {
348
+ return (
349
+ TryGetCompilerOptionValue(trimmedLine, "a", out string _)
350
+ || TryGetCompilerOptionValue(trimmedLine, "analyzer", out string _)
351
+ )
352
+ && (
353
+ trimmedLine.Contains(
354
+ "com.wallstop-studios.dxmessaging",
355
+ StringComparison.OrdinalIgnoreCase
356
+ )
357
+ || trimmedLine.Contains(
358
+ "WallstopStudios.DxMessaging",
359
+ StringComparison.OrdinalIgnoreCase
360
+ )
361
+ );
362
+ }
363
+
316
364
  /// <summary>
317
365
  /// Ensures <c>csc.rsp</c> contains a single <c>-additionalfile:</c> line pointing at the
318
366
  /// base-call ignore sidecar, when (and only when) that sidecar physically exists. Stale
319
367
  /// entries pointing at moved or deleted sidecar paths are removed.
320
368
  /// </summary>
321
369
  /// <remarks>
322
- /// The sidecar is generated by <see cref="DxMessagingBaseCallIgnoreSync"/> only when there
323
- /// is content to write. csc happily runs without it, so this method does NOT auto-create.
370
+ /// The sidecar is generated by <see cref="DxMessagingBaseCallIgnoreSync"/>. csc happily
371
+ /// runs without it, so this method does NOT auto-create; sidecar writes schedule this sync
372
+ /// again after deferred regeneration completes.
324
373
  /// </remarks>
325
374
  private static void EnsureAdditionalFileForIgnoreList()
326
375
  {
@@ -339,74 +388,12 @@ namespace DxMessaging.Editor
339
388
  .Replace("\\", "/");
340
389
 
341
390
  bool sidecarExists = File.Exists(sidecarAbsolutePath);
342
- string desiredLine = $"-additionalfile:\"{sidecarRelativePath}\"";
343
-
344
- string rspContent = File.ReadAllText(RspFilePath);
345
- List<string> newLines = new();
346
- bool foundDesired = false;
347
- bool foundStale = false;
348
-
349
- foreach (
350
- string line in rspContent.Split(
351
- new[] { '\r', '\n' },
352
- StringSplitOptions.RemoveEmptyEntries
353
- )
354
- )
355
- {
356
- string trimmedLine = line.Trim();
357
-
358
- bool isDxMessagingAdditionalFile =
359
- trimmedLine.StartsWith(
360
- "-additionalfile:",
361
- StringComparison.OrdinalIgnoreCase
362
- )
363
- && trimmedLine.Contains("DxMessaging.", StringComparison.OrdinalIgnoreCase)
364
- && trimmedLine.Contains(
365
- "BaseCallIgnore",
366
- StringComparison.OrdinalIgnoreCase
367
- );
368
-
369
- if (isDxMessagingAdditionalFile)
370
- {
371
- if (
372
- sidecarExists
373
- && string.Equals(
374
- trimmedLine,
375
- desiredLine,
376
- StringComparison.OrdinalIgnoreCase
377
- )
378
- )
379
- {
380
- if (!foundDesired)
381
- {
382
- newLines.Add(trimmedLine);
383
- foundDesired = true;
384
- }
385
- else
386
- {
387
- // Drop duplicate.
388
- foundStale = true;
389
- }
390
- }
391
- else
392
- {
393
- // Stale entry pointing at a moved/renamed/deleted sidecar; drop it.
394
- foundStale = true;
395
- }
396
- }
397
- else
398
- {
399
- newLines.Add(trimmedLine);
400
- }
401
- }
402
-
403
- bool needsAppend = sidecarExists && !foundDesired;
404
- if (needsAppend)
405
- {
406
- newLines.Add(desiredLine);
407
- }
408
-
409
- bool modified = foundStale || needsAppend;
391
+ string[] newLines = SynchronizeAdditionalFileForIgnoreListLines(
392
+ File.ReadAllLines(RspFilePath),
393
+ sidecarRelativePath,
394
+ sidecarExists,
395
+ out bool modified
396
+ );
410
397
 
411
398
  if (modified)
412
399
  {
@@ -422,46 +409,225 @@ namespace DxMessaging.Editor
422
409
  }
423
410
  catch (IOException ex)
424
411
  {
425
- Debug.LogError($"Failed to update csc.rsp additionalfile entry: {ex}");
412
+ DxMessagingEditorLog.LogError("Failed to update csc.rsp additionalfile entry.", ex);
426
413
  }
427
414
  }
428
415
 
429
- private static IEnumerable<string> GetAnalyzerArguments()
416
+ internal static string[] SynchronizeAdditionalFileForIgnoreListLines(
417
+ IEnumerable<string> lines,
418
+ string sidecarRelativePath,
419
+ bool sidecarExists,
420
+ out bool modified
421
+ )
430
422
  {
431
- HashSet<string> yielded = new(StringComparer.OrdinalIgnoreCase);
432
- string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
423
+ string desiredLine = FormatAdditionalFileArgument(sidecarRelativePath);
424
+ List<string> newLines = new();
425
+ bool foundDesired = false;
426
+ bool foundStale = false;
433
427
 
434
- foreach (string directory in AnalyzerDirectories)
428
+ foreach (string line in lines ?? Array.Empty<string>())
435
429
  {
436
- foreach (string dllName in RequiredDllNames)
430
+ string trimmedLine = line?.Trim();
431
+ if (string.IsNullOrEmpty(trimmedLine))
432
+ {
433
+ continue;
434
+ }
435
+
436
+ if (IsResponseFileComment(trimmedLine))
437
437
  {
438
- string absoluteDirectory = Path.IsPathRooted(directory)
439
- ? directory
440
- : Path.GetFullPath(Path.Combine(projectRoot, directory));
438
+ newLines.Add(trimmedLine);
439
+ continue;
440
+ }
441
441
 
442
- string absoluteAnalyzerPath = Path.Combine(absoluteDirectory, dllName);
443
- if (!File.Exists(absoluteAnalyzerPath))
442
+ List<string> retainedArguments = new();
443
+ bool removedFromLine = false;
444
+ foreach (string argument in SplitResponseFileArguments(trimmedLine))
445
+ {
446
+ if (!IsDxMessagingBaseCallIgnoreAdditionalFile(argument))
444
447
  {
448
+ retainedArguments.Add(argument);
445
449
  continue;
446
450
  }
447
451
 
448
- string projectRelativePath = FileUtil.GetProjectRelativePath(
449
- absoluteAnalyzerPath
450
- );
451
- if (string.IsNullOrEmpty(projectRelativePath))
452
+ bool isDesired =
453
+ sidecarExists
454
+ && IsAdditionalFileArgumentForPath(argument, sidecarRelativePath);
455
+ if (!isDesired)
452
456
  {
457
+ // Stale entry pointing at a moved/renamed/deleted sidecar; drop it.
458
+ foundStale = true;
459
+ removedFromLine = true;
453
460
  continue;
454
461
  }
455
462
 
456
- string normalizedRelativePath = projectRelativePath.Replace("\\", "/");
457
- if (!yielded.Add(normalizedRelativePath))
463
+ if (foundDesired)
458
464
  {
465
+ // Drop duplicate.
466
+ foundStale = true;
467
+ removedFromLine = true;
459
468
  continue;
460
469
  }
461
470
 
462
- yield return $"-a:\"{normalizedRelativePath}\"";
471
+ retainedArguments.Add(desiredLine);
472
+ foundDesired = true;
473
+ if (!string.Equals(argument, desiredLine, StringComparison.Ordinal))
474
+ {
475
+ foundStale = true;
476
+ removedFromLine = true;
477
+ }
478
+ }
479
+
480
+ if (retainedArguments.Count > 0)
481
+ {
482
+ newLines.Add(string.Join(" ", retainedArguments));
483
+ continue;
484
+ }
485
+
486
+ if (!removedFromLine)
487
+ {
488
+ newLines.Add(trimmedLine);
489
+ }
490
+ }
491
+
492
+ bool needsAppend = sidecarExists && !foundDesired;
493
+ if (needsAppend)
494
+ {
495
+ newLines.Add(desiredLine);
496
+ }
497
+
498
+ modified = foundStale || needsAppend;
499
+ return newLines.ToArray();
500
+ }
501
+
502
+ private static string FormatAdditionalFileArgument(string sidecarRelativePath)
503
+ {
504
+ return $"-additionalfile:\"{sidecarRelativePath}\"";
505
+ }
506
+
507
+ private static bool IsResponseFileComment(string trimmedLine)
508
+ {
509
+ return trimmedLine.StartsWith("#", StringComparison.Ordinal);
510
+ }
511
+
512
+ private static bool IsDxMessagingBaseCallIgnoreAdditionalFile(string trimmedLine)
513
+ {
514
+ return TryGetCompilerOptionValue(trimmedLine, "additionalfile", out string value)
515
+ && value.Contains("DxMessaging.", StringComparison.OrdinalIgnoreCase)
516
+ && value.Contains("BaseCallIgnore", StringComparison.OrdinalIgnoreCase);
517
+ }
518
+
519
+ private static bool IsAdditionalFileArgumentForPath(
520
+ string argument,
521
+ string sidecarRelativePath
522
+ )
523
+ {
524
+ if (!TryGetCompilerOptionValue(argument, "additionalfile", out string value))
525
+ {
526
+ return false;
527
+ }
528
+
529
+ string unquotedValue = UnquoteWholeArgument(value.Trim());
530
+ return string.Equals(
531
+ unquotedValue.Replace("\\", "/"),
532
+ sidecarRelativePath,
533
+ StringComparison.OrdinalIgnoreCase
534
+ );
535
+ }
536
+
537
+ private static bool TryGetCompilerOptionValue(
538
+ string argument,
539
+ string optionName,
540
+ out string value
541
+ )
542
+ {
543
+ value = string.Empty;
544
+ if (string.IsNullOrEmpty(argument) || string.IsNullOrEmpty(optionName))
545
+ {
546
+ return false;
547
+ }
548
+
549
+ string unquotedArgument = UnquoteWholeArgument(argument.Trim());
550
+ if (unquotedArgument.Length <= optionName.Length + 1)
551
+ {
552
+ return false;
553
+ }
554
+
555
+ if (unquotedArgument[0] != '-' && unquotedArgument[0] != '/')
556
+ {
557
+ return false;
558
+ }
559
+
560
+ if (unquotedArgument[optionName.Length + 1] != ':')
561
+ {
562
+ return false;
563
+ }
564
+
565
+ if (
566
+ string.Compare(
567
+ unquotedArgument,
568
+ 1,
569
+ optionName,
570
+ 0,
571
+ optionName.Length,
572
+ StringComparison.OrdinalIgnoreCase
573
+ ) != 0
574
+ )
575
+ {
576
+ return false;
577
+ }
578
+
579
+ value = unquotedArgument.Substring(optionName.Length + 2);
580
+ return true;
581
+ }
582
+
583
+ private static string UnquoteWholeArgument(string argument)
584
+ {
585
+ if (argument.Length >= 2 && argument[0] == '"' && argument[argument.Length - 1] == '"')
586
+ {
587
+ return argument.Substring(1, argument.Length - 2).Replace("\"\"", "\"");
588
+ }
589
+
590
+ return argument;
591
+ }
592
+
593
+ private static List<string> SplitResponseFileArguments(string line)
594
+ {
595
+ List<string> arguments = new();
596
+ if (string.IsNullOrWhiteSpace(line))
597
+ {
598
+ return arguments;
599
+ }
600
+
601
+ System.Text.StringBuilder current = new();
602
+ bool inQuotes = false;
603
+ foreach (char character in line)
604
+ {
605
+ if (character == '"')
606
+ {
607
+ inQuotes = !inQuotes;
608
+ current.Append(character);
609
+ continue;
463
610
  }
611
+
612
+ if (char.IsWhiteSpace(character) && !inQuotes)
613
+ {
614
+ if (current.Length > 0)
615
+ {
616
+ arguments.Add(current.ToString());
617
+ current.Clear();
618
+ }
619
+ continue;
620
+ }
621
+
622
+ current.Append(character);
464
623
  }
624
+
625
+ if (current.Length > 0)
626
+ {
627
+ arguments.Add(current.ToString());
628
+ }
629
+
630
+ return arguments;
465
631
  }
466
632
  }
467
633
  }
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: 54bd01ff5fd34bbc9950a1c7230a15d8
3
- timeCreated: 1730841461
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -39,7 +39,7 @@ namespace DxMessaging.Editor.Testing
39
39
  }
40
40
 
41
41
  List<ListenerDiagnosticsView> listenerViews = component
42
- ._registeredListeners.OrderBy(pair => pair.Key.GetInstanceID())
42
+ ._registeredListeners.OrderBy(pair => InstanceId.StableId(pair.Key))
43
43
  .Select(pair => CreateListenerView(pair.Key, pair.Value))
44
44
  .ToList();
45
45
 
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: 2624c9e8eb5243ebb71545774f411c2c
3
- timeCreated: 1761880591
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: