From c3ac556f2f4c800fbc6070791d7d4f6747d716e2 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 13 Aug 2026 07:51:52 -0400 Subject: [PATCH 1/8] feat: add a diff-scoped comment-hygiene ratchet and compat gate Enforces the fieldworks-code-commenting standard's banned-content rules and a non-ascii-punctuation check against lines a diff adds, wired into build.ps1, test.ps1, and CI. Includes a triage tool that attributes existing violations to their introducing commit, and a stray-docs CI gate blocking brainstorming/planning markdown from surviving into a merged PR. The comment-length rule is a 200-character budget rather than a hard 1-line cap, since the cap forced every explanation onto one line that then broke .editorconfig's line-length limit once genuinely non-trivial. This retroactively surfaced 127 pre-existing over-long comments, all tracing via git blame to the already-merged Avalonia foundation commit (#964); all 127 are reformatted into multi-line prose here. Adds a powershell-compat gate (regex scan plus best-effort PSScriptAnalyzer) and dual-shell CI test runs, after an independent review surfaced three functional bugs specific to Windows PowerShell 5.1 vs 7: a Set-StrictMode crash in local base-ref resolution, a CI checkout missing fetch-depth that left origin/main unresolvable, and a PowerShell 6+-only escape sequence that silently disabled ASCII detection under 5.1 -- the exact engine CI's build.ps1/test.ps1 steps run under. All three are fixed and verified by direct reproduction under both engines, not just reasoned about. Co-Authored-By: Claude Sonnet 5 --- .claude/skills/commit-messages/SKILL.md | 66 ++++ .../fieldworks-code-commenting/SKILL.md | 22 +- .claude/skills/pr-pitch/SKILL.md | 2 +- .claude/skills/pr-preflight/SKILL.md | 11 +- .github/workflows/CI.yml | 26 ++ .github/workflows/stray-docs.yml | 31 ++ .gitignore | 2 + AGENTS.md | 6 + Build/Agent/CommentHygiene.Tests.ps1 | 211 +++++++++++ Build/Agent/CommentHygiene.psm1 | 342 ++++++++++++++++++ Build/Agent/comment-hygiene-blame.ps1 | 98 +++++ Build/Agent/comment-hygiene-repair.ps1 | 72 ++++ Build/Agent/comment-hygiene.ps1 | 182 ++++++++++ Build/Agent/powershell-compat.ps1 | 144 ++++++++ .../LocalizeFieldWorksTests.cs | 4 +- .../FwBuildTasksTests/RegFreeCreatorTests.cs | 1 - Build/Src/FwBuildTasks/RegFreeCreator.cs | 2 - Docs/CONTRIBUTING.md | 2 + ...31-retired-avalonia-lesson-cards-design.md | 75 ---- Lib/src/Converter/Convertlib/Convert.cs | 2 +- .../FormLanguageSwitch/FormLanguageSwitch.cs | 2 +- Src/CacheLight/MetaDataCache.cs | 2 +- .../Controls/DetailControls/DataTree.cs | 15 +- .../DataTreeRenderTests.cs | 32 +- .../DataTreeReshowTimingTests.cs | 2 +- .../MorphTypeAtomicLauncherTests.cs | 8 +- .../Controls/DetailControls/ObjSeqHashMap.cs | 2 +- Src/Common/Controls/DetailControls/Slice.cs | 2 +- .../XMLViewsTests/TestXmlBrowseView.cs | 2 +- .../Controls/XMLViews/XmlBrowseViewBaseVc.cs | 2 +- Src/Common/Framework/MainWindowDelegate.cs | 2 +- Src/Common/FwAvalonia/AvaloniaDialogHost.cs | 18 +- Src/Common/FwAvalonia/CompactDialogStyles.cs | 6 +- Src/Common/FwAvalonia/Detail/DataTree.cs | 69 ++-- .../FwAvalonia/Detail/DetailFocusMemory.cs | 10 +- .../FwAvalonia/Detail/DetailMenuFlyout.cs | 4 +- Src/Common/FwAvalonia/Detail/DetailModel.cs | 62 ++-- .../FwAvalonia/Detail/DetailModelProjector.cs | 4 +- .../FwAvalonia/Detail/DetailRichTextChrome.cs | 8 +- .../FwAvalonia/Detail/DetailStructureRules.cs | 8 +- .../Detail/DetailViewingServices.cs | 2 +- .../FwAvalonia/Detail/FwFieldControls.cs | 108 +++--- .../FwAvalonia/Detail/FwOptionChooser.cs | 48 +-- .../Detail/FwStructuredTextField.cs | 16 +- Src/Common/FwAvalonia/Detail/HoverReveal.cs | 30 +- .../FwAvalonia/Detail/IDetailEditContext.cs | 6 +- .../Detail/IStructuredTextEditing.cs | 10 +- .../FwAvalonia/Detail/LexiconFirstSlice.cs | 4 +- .../FwAvalonia/Detail/MorphTypeSwapLogic.cs | 4 +- Src/Common/FwAvalonia/Detail/SliceFactory.cs | 24 +- Src/Common/FwAvalonia/DetailHostControl.cs | 4 +- .../FwAvalonia/FilterableDropdownSupport.cs | 4 +- .../FinalizerSafeSynchronizationContext.cs | 16 +- Src/Common/FwAvalonia/FwAvaloniaDensity.cs | 14 +- Src/Common/FwAvalonia/FwAvaloniaPlatform.cs | 6 +- Src/Common/FwAvalonia/FwAvaloniaRuntime.cs | 8 +- Src/Common/FwAvalonia/FwAvaloniaStrings.cs | 40 +- .../AvaloniaDialogHostTests.cs | 6 +- .../FwAvaloniaTests/CanonicalJsonTests.cs | 4 +- .../DetailCustomFieldRenderingTests.cs | 2 +- .../FwAvaloniaTests/DetailEditingTests.cs | 33 +- .../DetailEditorParityTests.cs | 4 +- .../FwAvaloniaTests/DetailFocusMemoryTests.cs | 8 +- .../FwAvaloniaTests/DetailMenuTests.cs | 24 +- .../FwAvaloniaTests/DetailModelTests.cs | 6 +- .../FwAvaloniaTests/DetailOrcTests.cs | 2 +- .../DetailOverrideRenderingTests.cs | 8 +- .../DetailViewingParityTests.cs | 19 +- .../DetailViewingServiceReplacementTests.cs | 14 +- .../DialogLauncherFieldTests.cs | 2 +- .../EditorKindMapClassifyTests.cs | 4 +- .../EngineIsolationAuditTests.cs | 12 +- .../FwFieldSelectionInputTests.cs | 6 +- .../FwMultiWsTextFieldTests.cs | 2 +- .../FwAvaloniaTests/FwOptionChooserTests.cs | 7 +- .../FwAvaloniaTests/FwPosChooserTests.cs | 2 +- .../FwAvaloniaTests/HoverRevealTests.cs | 10 +- .../LayoutImportCoverageTests.cs | 34 +- .../FwAvalonia/FwAvaloniaTests/SeamTests.cs | 4 +- .../FwAvaloniaTests/SliceFactoryTests.cs | 10 +- .../StructuredTextEdgeCaseTests.cs | 17 +- .../StructuredTextRichDepthTests.cs | 2 +- .../TreeNodeTemplateAndRtlTests.cs | 2 +- .../UIFrameworkAndHostContractTests.cs | 6 +- .../ViewDefinitionOverrideApplierTests.cs | 4 +- .../ViewDefinitionOverrideDifferTests.cs | 2 +- .../ViewDefinitionOverrideEdgeCaseTests.cs | 2 +- .../ViewDefinitionOverrideEditorTests.cs | 2 +- ...ViewDefinitionOverrideFileMigratorTests.cs | 2 +- ...ewDefinitionOverrideJsonSerializerTests.cs | 2 +- .../ViewDefinitionOverrideMigratorTests.cs | 2 +- .../FwAvaloniaTests/Visual/DialogSnapshot.cs | 8 +- .../Visual/VisualSnapshotTests.cs | 23 +- .../Workflows/HeadlessWorkflowHarness.cs | 8 +- Src/Common/FwAvalonia/FwCheckBoxStyle.cs | 44 +-- Src/Common/FwAvalonia/FwPosChooser.cs | 14 +- Src/Common/FwAvalonia/FwRadioButtonStyle.cs | 32 +- Src/Common/FwAvalonia/FwSurfaceStyles.cs | 18 +- Src/Common/FwAvalonia/IDialogViewModel.cs | 2 +- .../InputKeyClaimingAvaloniaHost.cs | 4 +- .../FwAvalonia/LexiconFeatureCatalog.cs | 6 +- Src/Common/FwAvalonia/Seams/FwDragDrop.cs | 2 +- Src/Common/FwAvalonia/Seams/IFwClipboard.cs | 8 +- Src/Common/FwAvalonia/UIFrameworkRegistry.cs | 15 +- Src/Common/FwAvalonia/UIFrameworkResolver.cs | 2 +- .../FwAvalonia/UIFrameworkSelectionService.cs | 2 +- .../ViewDefinition/DictionaryPartResolver.cs | 6 +- .../ViewDefinition/EditorKindMap.cs | 8 +- .../ViewDefinition/IViewDefinitionImporter.cs | 2 +- .../ViewDefinition/LayoutImportCoverage.cs | 6 +- .../ViewDefinition/LayoutSourceLoader.cs | 4 +- .../ViewDefinition/ViewDefinitionCompiler.cs | 2 +- .../ViewDefinitionJsonSerializer.cs | 4 +- .../ViewDefinition/ViewDefinitionModel.cs | 12 +- .../ViewDefinitionOverrideApplier.cs | 2 +- .../ViewDefinitionOverrideDiffer.cs | 2 +- .../ViewDefinitionOverrideEditor.cs | 10 +- .../ViewDefinitionOverrideFileMigrator.cs | 4 +- .../ViewDefinitionOverrideMigrator.cs | 2 +- .../ViewDefinitionOverrideStore.cs | 8 +- .../ViewDefinition/XmlLayoutImporter.cs | 44 ++- .../FwAvaloniaDialogs/AddNewSenseDlgInput.cs | 8 +- .../AddNewSenseDlgPayload.cs | 6 +- .../AddNewSenseDlgView.axaml.cs | 2 +- .../AddNewSenseDlgViewModel.cs | 8 +- .../AvaloniaDialogLauncher.cs | 4 +- .../FwAvaloniaDialogs/ChooserDialogInput.cs | 4 +- .../FwAvaloniaDialogs/ChooserDialogResult.cs | 2 +- .../ChooserDialogView.axaml.cs | 7 +- .../ChooserDialogViewModel.cs | 24 +- .../FwAvaloniaDialogs/ChooserTreeNode.cs | 4 +- .../CreateFeatureDialogViewModel.cs | 2 +- .../FwAvaloniaDialogs/DialogThemeBootstrap.cs | 29 +- .../EntryGoAuxiliaryOption.cs | 4 +- .../FwAvaloniaDialogs/EntryGoDialogInput.cs | 8 +- .../FwAvaloniaDialogs/EntryGoDialogResult.cs | 2 +- .../EntryGoDialogView.axaml.cs | 10 +- .../EntryGoDialogViewModel.cs | 20 +- .../FwAvaloniaDialogs/EntryGoResultColumn.cs | 12 +- .../EntryGoSearchFieldSpec.cs | 4 +- .../FwAvaloniaDialogs/EntryGoSearchResult.cs | 10 +- .../FeatureChooserDialogInput.cs | 4 +- .../FeatureChooserDialogView.axaml.cs | 2 +- .../FeatureChooserDialogViewModel.cs | 6 +- .../FeatureChooserPayload.cs | 4 +- .../FwAvaloniaDialogsStrings.cs | 48 +-- .../AvaloniaDialogTestHarness.cs | 4 +- .../ChooserDialogTests.cs | 7 +- .../ChooserDialogTreeTests.cs | 12 +- .../CreatePosCatalogTests.cs | 4 +- .../DialogKeyboardA11yTests.cs | 19 +- .../DialogLayoutAssert.cs | 6 +- .../DialogLayoutAssertTests.cs | 10 +- .../DialogViewModelBaseTests.cs | 2 +- .../EntryGoDialogTests.cs | 15 +- .../FwFeatureStructureEditorTests.cs | 6 +- .../FwMsaGroupBoxTests.cs | 2 +- .../InsertEntryDialogTests.cs | 28 +- .../OptionsDialogTests.cs | 2 +- .../FwFeatureStructureEditor.cs | 34 +- Src/Common/FwAvaloniaDialogs/FwMessageBox.cs | 2 +- Src/Common/FwAvaloniaDialogs/FwSandboxMsa.cs | 26 +- .../InMemoryDetailEditContext.cs | 2 +- .../FwAvaloniaDialogs/InsertEntryDlgInput.cs | 36 +- .../InsertEntryDlgPayload.cs | 8 +- .../InsertEntryDlgView.axaml.cs | 2 +- .../InsertEntryDlgViewModel.cs | 64 ++-- .../FwAvaloniaDialogs/LexOptionsDlgState.cs | 8 +- .../LexOptionsDlgView.axaml.cs | 2 +- .../LexOptionsDlgViewModel.cs | 4 +- Src/Common/FwAvaloniaDialogs/MSAGroupBox.cs | 34 +- .../FwAvaloniaDialogs/MsaCreatorDlgInput.cs | 14 +- .../FwAvaloniaDialogs/MsaCreatorDlgPayload.cs | 6 +- .../MsaCreatorDlgView.axaml.cs | 2 +- .../MsaCreatorDlgViewModel.cs | 2 +- .../FwAvaloniaPreviewHost/DialogPreviews.cs | 12 +- .../PreviewHostUiaTests.cs | 2 +- Src/Common/FwUtils/FwUpdater.cs | 2 +- .../FwUtils/FwUtilsTests/AssemblyInfo.cs | 2 +- .../FwUtilsTests/FwRegistryHelperTests.cs | 2 +- Src/Common/FwUtils/ISubscriber.cs | 2 +- Src/Common/FwUtils/StringTable.cs | 2 +- Src/Common/FwUtils/Subscriber.cs | 2 +- .../CompositeViewCapture.cs | 29 +- .../DataTreeRenderHarness.cs | 23 +- .../RootSiteTests/GenericLexEntryView.cs | 14 +- .../RootSiteTests/RenderBenchmarkHarness.cs | 2 +- .../RootSiteTests/RenderBenchmarkTestsBase.cs | 6 +- .../ExtraComInterfacesTests.cs | 2 +- Src/FwCoreDlgs/FwWritingSystemSetupModel.cs | 2 +- .../FlexInterlinModel/FlexInterlinear.cs | 2 +- Src/LexText/Interlinear/InterlinVc.cs | 2 +- .../InterlinearTextsRecordClerk.cs | 2 +- .../Avalonia/AvaloniaOptionsDialogLauncher.cs | 26 +- .../Avalonia/EntryGoLauncherShared.cs | 14 +- .../Avalonia/FwFeatureStructureAdapter.cs | 22 +- .../Avalonia/LcmAddAllomorphDialogLauncher.cs | 6 +- .../Avalonia/LcmAddNewSenseDialogLauncher.cs | 13 +- .../Avalonia/LcmCreateFeatureLauncher.cs | 12 +- .../Avalonia/LcmCreatePartOfSpeechLauncher.cs | 18 +- .../Avalonia/LcmGoToEntryDialogLauncher.cs | 4 +- .../LcmInflectionFeatureChooserLauncher.cs | 12 +- .../LcmInflectionFeatureCreateWiring.cs | 2 +- .../Avalonia/LcmInsertEntryDialogLauncher.cs | 86 ++--- .../LcmLinkAllomorphDialogLauncher.cs | 8 +- .../LcmLinkEntryOrSenseDialogLauncher.cs | 2 +- .../Avalonia/LcmLinkMsaDialogLauncher.cs | 2 +- .../Avalonia/LcmMergeEntryDialogLauncher.cs | 14 +- .../Avalonia/LcmMsaCreatorDialogLauncher.cs | 10 +- .../LcmPhonologicalFeatureChooserLauncher.cs | 8 +- .../AvaloniaOptionsDialogLauncherTests.cs | 2 +- .../FwFeatureStructureAdapterTests.cs | 4 +- .../LcmAddAllomorphDialogLauncherTests.cs | 4 +- .../LcmAddNewSenseDialogLauncherTests.cs | 10 +- .../Avalonia/LcmCreateFeatureLauncherTests.cs | 2 +- .../LcmCreatePartOfSpeechLauncherTests.cs | 6 +- .../LcmFeatureChooserLauncherTests.cs | 2 +- .../LcmGoToEntryDialogLauncherTests.cs | 2 +- .../LcmInflectionFeatureCreateWiringTests.cs | 4 +- .../LcmInsertEntryDialogLauncherTests.cs | 7 +- .../LcmLinkAllomorphDialogLauncherTests.cs | 2 +- .../Avalonia/LcmLinkMsaDialogLauncherTests.cs | 2 +- .../LcmMergeEntryDialogLauncherTests.cs | 10 +- .../LcmMsaCreatorDialogLauncherTests.cs | 13 +- .../Avalonia/ScreenshotHarnessTests.cs | 32 +- Src/LexText/Lexicon/GhostLexRefSlice.cs | 6 +- .../MsaInflectionFeatureListDlgLauncher.cs | 9 +- .../RespellingTests.cs | 2 - .../XAmpleManagedWrapper/AssemblyInfo.cs | 2 +- Src/ParatextImport/ImportedBooks.cs | 2 +- .../ImportTests/ParatextImportManagerTests.cs | 2 +- .../SilSidePaneTests/AssemblyInfo.cs | 2 +- Src/XCore/XCoreSample/AssemblyInfo.cs | 2 +- Src/XCore/xCoreInterfaces/PropertyTable.cs | 2 +- Src/XCore/xWindow.cs | 2 +- .../Composer/ComposedDetailEditContext.cs | 2 +- .../Avalonia/Composer/DetailComposer.cs | 249 +++++-------- .../Avalonia/Composer/DetailValueFactory.cs | 12 +- .../Avalonia/Composer/LcmDetailEditSession.cs | 15 +- .../Composer/LexiconEditErrorFallback.cs | 12 +- .../Composer/LexiconFirstSliceEditContext.cs | 2 +- Src/xWorks/Avalonia/DetailEditContextBase.cs | 12 +- .../Avalonia/DetailEditContextHolder.cs | 12 +- .../Avalonia/DetailOverrideMigration.cs | 4 +- .../AvaloniaDetailRefreshController.cs | 18 +- .../Avalonia/Hosting/FwTsStringClipboard.cs | 2 +- .../Hosting/RecordClerkNavigationContext.cs | 2 +- .../Hosting/RecordEditView.Avalonia.cs | 75 ++-- .../Hosting/WritingSystemKeyboards.cs | 2 +- .../Avalonia/Hosting/XCoreMenuBridge.cs | 10 +- Src/xWorks/Avalonia/IPrepareToGoAway.cs | 2 +- .../Plugins/ReversalIndexEntryPlugin.cs | 14 +- Src/xWorks/Avalonia/Plugins/SlicePlugins.cs | 8 +- Src/xWorks/CustomListDlg.cs | 2 +- Src/xWorks/DTMenuHandler.cs | 2 +- .../FirstAlphaMigrator.cs | 2 +- .../PreHistoricMigrator.cs | 2 +- Src/xWorks/IWebonaryClient.cs | 2 +- Src/xWorks/LcmXhtmlGenerator.cs | 2 +- Src/xWorks/RecordEditView.cs | 12 +- Src/xWorks/WordStyleCollection.cs | 2 +- .../Composer/AdhocCoProhibComposeTests.cs | 4 +- .../Avalonia/Composer/BackRefVectorTests.cs | 2 +- .../Composer/CompoundRuleComposeTests.cs | 4 +- .../Composer/DetailComposerOverrideTests.cs | 2 +- .../DetailComposerReferenceChooserTests.cs | 28 +- .../Composer/DetailComposerVoiceWsTests.cs | 2 +- .../Composer/DetailConsolidationTests.cs | 8 +- .../Composer/DetailEditContextEditingTests.cs | 97 ++--- .../DetailEditSessionLifecycleTests.cs | 8 +- .../Composer/DetailOverrideMigrationTests.cs | 2 +- .../Composer/EntryReferenceVectorTests.cs | 13 +- ...xiconFirstSliceEditContextEdgeCaseTests.cs | 2 +- .../Composer/NaturalClassComposeTests.cs | 2 +- .../Composer/StructuredTextAdapterTests.cs | 8 +- .../StructuredTextIntegrationTests.cs | 16 +- .../Composer/StructuredTextWorkflowTests.cs | 17 +- .../Hosting/AvaloniaHeadlessPlatformTests.cs | 4 +- .../Hosting/DataTreeMoveReachabilityTests.cs | 2 +- .../DetailCommandAdapterHardeningTests.cs | 8 +- .../DetailEditGuardAndSchedulingTests.cs | 12 +- .../Hosting/DetailEditLinkDispatchTests.cs | 2 +- .../DetailObjectCommandExecutionTests.cs | 26 +- .../Hosting/FwTsStringClipboardTests.cs | 2 +- .../TestLocalizationManagerBootstrap.cs | 4 +- .../Plugins/LexemeEditorInventoryTests.cs | 12 +- .../xWorksTests/LcmWordGeneratorTests.cs | 10 +- .../BulkEditReplaceCharacterizationTests.cs | 162 +++++++++ build.ps1 | 12 + test.ps1 | 12 + 290 files changed, 2814 insertions(+), 1722 deletions(-) create mode 100644 .claude/skills/commit-messages/SKILL.md create mode 100644 .github/workflows/stray-docs.yml create mode 100644 Build/Agent/CommentHygiene.Tests.ps1 create mode 100644 Build/Agent/CommentHygiene.psm1 create mode 100644 Build/Agent/comment-hygiene-blame.ps1 create mode 100644 Build/Agent/comment-hygiene-repair.ps1 create mode 100644 Build/Agent/comment-hygiene.ps1 create mode 100644 Build/Agent/powershell-compat.ps1 delete mode 100644 Docs/superpowers/specs/2026-07-31-retired-avalonia-lesson-cards-design.md create mode 100644 Src/xWorks/xWorksTests/Search/BulkEditReplaceCharacterizationTests.cs diff --git a/.claude/skills/commit-messages/SKILL.md b/.claude/skills/commit-messages/SKILL.md new file mode 100644 index 0000000000..644a48dca9 --- /dev/null +++ b/.claude/skills/commit-messages/SKILL.md @@ -0,0 +1,66 @@ +--- +name: commit-messages +description: MUST use before writing any git commit message in this repository. Covers the gitlint rules CI enforces (title/body length, blank line, trailing punctuation) and how to write a compliant message on the first try. +--- + +# Commit Messages + +CI (`.github/workflows/CommitMessage.yml`) runs `gitlint` against every +commit on a PR and fails the build on any violation. `.gitlint` in the repo +root only exempts `Agent-Logs-Url:` and the Copilot Autofix co-author +trailer from the body-length rule -- every other line, in every commit, +is checked. There is no leniency for "just the summary" or "just this once." + +## The rules that actually fire in practice + +| Rule | Limit | Notes | +|---|---|---| +| Title length (T1) | 72 characters | Counts the whole subject line, including the `type:` prefix. | +| Body line length (B1) | 80 characters | Per line, not per paragraph. A heredoc does NOT auto-wrap -- you must break lines yourself. | +| Blank line after title (B4) | required | One empty line between the subject and the body. | +| Title trailing punctuation (T3) | none | No period, no colon, at the end of the subject line. | +| Trailing whitespace (T2, B2) | none | Watch for trailing spaces left by hand-wrapped lines. | +| Hard tabs (B3) | none | Use spaces in the body. | + +## Writing a compliant message the first time + +- Count the title before committing to it. "docs: remove doc-pointers and + provenance narration from PR #964" is 63 characters; adding a scope like + "and cleanup notes" on top of an already-full title is how T1 fails. +- Wrap body prose by hand at well under 80 characters per line -- a heredoc + passed to `git commit -F -`/`-m` reproduces exactly the line breaks you + typed, it does not reflow them. Aim for ~70 so a `Co-Authored-By:` trailer + or an indented list item added later doesn't push a line over. +- Prefer several short lines over one long one, and several short + paragraphs over one dense one -- a commit message is read in a `git log` + pane, not a text editor with wrapping. + +## Verify before considering a commit done + +Run the same check CI runs, scoped to the current branch: + +```powershell +gitlint --ignore body-is-missing --commits main..HEAD +``` + +(Substitute the actual base branch if not `main`.) A clean run prints +nothing and exits 0. `Build/Agent/commit-messages.ps1` (or `.sh`) wraps this +with the same base-ref auto-detection CI uses, if you want the base +resolved for you instead of naming it. + +## Fixing a violation after the fact + +If a commit already landed non-compliant and hasn't been pushed to a shared +branch, reword it without a full interactive rebase: + +```bash +git rebase ^ --exec "if [ \"\$(git rev-parse HEAD)\" = \"\" ]; then git commit --amend -F ; fi" +``` + +This replays history non-interactively (no editor, no `-i` prompt) and +amends only the one commit whose SHA matches, at the point in the replay +where it is HEAD. Never do this on a branch that has already been pushed +and could have a PR or other work based on it -- check +`git rev-parse --abbrev-ref --symbolic-full-name @{u}` and +`git ls-remote --heads origin ` first, and confirm with the user if +either shows the branch is shared. diff --git a/.claude/skills/fieldworks-code-commenting/SKILL.md b/.claude/skills/fieldworks-code-commenting/SKILL.md index a8bf4c7668..8ae45dbb39 100644 --- a/.claude/skills/fieldworks-code-commenting/SKILL.md +++ b/.claude/skills/fieldworks-code-commenting/SKILL.md @@ -1,6 +1,6 @@ --- name: fieldworks-code-commenting -description: The FieldWorks code-comment standard for C#. Use whenever writing or editing code comments in this repository -- new code, refactors, or comment audits. Covers doc-comment contracts, banned content categories, legacy references, XML doc tags, and XML-doc placement. +description: The FieldWorks code-comment standard for C# and PowerShell. Use whenever writing or editing a comment anywhere in this repository -- new code, refactors, or comment audits, in .cs or .ps1 alike. Covers doc-comment contracts, banned content categories, the 200-character cap on implementation comments, legacy references, XML doc tags, and XML-doc placement. --- # FieldWorks Code Commenting @@ -11,6 +11,16 @@ Apply it while authoring -- do not write loose comments and clean them later. The audience is the next reader of the code -- never the reviewer of the current diff, never a coverage gate. +## Scope + +Every comment in this repository, in any language: `//`/`///` in C#, `#`/ +`<# #>` in PowerShell. The mechanical subset of this standard (banned content, +ASCII-only, the 200-character implementation-comment budget) is enforced by +`Build/Agent/comment-hygiene.ps1` against `.cs` and `.ps1` files alike; the +judgment-based rules (accuracy, WHAT-not-HOW, standalone clarity) are not +mechanically checked in either language and still require applying this +standard while authoring. + ## The standard 1. **Accuracy first, then brevity.** A comment that misstates behavior is @@ -134,7 +144,15 @@ information of its own. ## Inline comments Sparingly: only when the reasoning is not clear from the code, or a bugfix is -non-obvious. Keep them short and located above the level of nesting that the code being described spans. +non-obvious. **200 characters, across as many lines as that takes to respect +the line-length limit.** If the WHY needs more than that, either it belongs +as a doc comment on a named symbol (which may run long-form), or the comment +is trying to explain too much at once -- cut it to the single sentence that +would confuse a reader most if it were missing, and drop the rest, even if +that loses nuance the original draft had. `Build/Agent/comment-hygiene.ps1` +enforces this mechanically (category `comment-too-long`) for `//` and `#` +alike -- see "Scope" above. +Located above the level of nesting that the code being described spans. In tests, two extra tells of noise: restating what the test method name already says, and justifying a test to the coverage gate ("covers the false diff --git a/.claude/skills/pr-pitch/SKILL.md b/.claude/skills/pr-pitch/SKILL.md index 599fa2a847..961e2e66f2 100644 --- a/.claude/skills/pr-pitch/SKILL.md +++ b/.claude/skills/pr-pitch/SKILL.md @@ -1,6 +1,6 @@ --- name: pr-pitch -description: Compose a PR body as a pitch that answers the unknowns a reviewer arrives with, with the branch's decisions, provenance, and paths-not-taken folded into collapsed accordions below it, while evicting those files from the repo. Use when writing or refreshing a PR description, when a branch carries working markdown that should not merge, or when pr-preflight reaches its PR step. +description: "NOT an entrypoint -- pr-preflight calls this for the write-up step; use pr-preflight for a fresh 'write/make/open a PR' request. Invoke this directly only to redo the write-up on a PR that already exists. Composes a PR body as a pitch that answers the unknowns a reviewer arrives with, with the branch's decisions, provenance, and paths-not-taken folded into collapsed accordions below it, while evicting those files from the repo." argument-hint: "Optional PR number (defaults to the PR for the current branch)" --- diff --git a/.claude/skills/pr-preflight/SKILL.md b/.claude/skills/pr-preflight/SKILL.md index bff19861b0..f032ea6f1d 100644 --- a/.claude/skills/pr-preflight/SKILL.md +++ b/.claude/skills/pr-preflight/SKILL.md @@ -1,6 +1,6 @@ --- name: pr-preflight -description: "Use when preparing a FieldWorks branch or pull request for review: pre-PR review, branch readiness, author interview, review summary generation, validation evidence, or PR description preparation." +description: "The required entrypoint whenever asked to write, make, open, create, update, or ship a PR for this repo -- do not post a PR body without running this first. Also use for pre-PR review, branch readiness, author interview, review summary generation, or validation evidence." argument-hint: "Optional branch purpose or PR goal" user-invocable: true --- @@ -236,7 +236,7 @@ After writing the summary, tell the author: > > Please review it, make changes where appropriate, and run `/pr-preflight` again until you are ready to post the PR. > -> If you do not want to make any changes and are ready for review, would you like me to commit any uncommitted changes, push, and post the PR? I will check whether one already exists for this branch and update it, or create a new one if not. The write-up runs through `pr-pitch`, which will also triage any research or working markdown on the branch into collapsed PR comments and out of the tree -- you approve that triage before anything is deleted." +> If you do not want to make any changes and are ready for review, would you like me to commit any uncommitted changes, push, and post the PR? I will check whether one already exists for this branch and update it, or create a new one if not. The write-up runs through `pr-pitch`, which will also triage any research or working markdown on the branch into collapsed sections in the PR body and out of the tree -- you approve that triage before anything is deleted." Only create or update a PR after the author confirms. @@ -244,9 +244,10 @@ Only create or update a PR after the author confirms. This skill is the single entrypoint for making a PR, but it does not compose the description itself. Once the author confirms readiness, invoke the -`pr-pitch` skill and let it own the write-up. It produces three artifacts -together: the PR body as a pitch, provenance in collapsed PR comments, and a -commit evicting the branch's research and working markdown from the tree. +`pr-pitch` skill and let it own the write-up. It produces two artifacts +together: the PR body (a pitch above the fold, provenance in collapsed +accordions below it) and a commit evicting the branch's research and working +markdown from the tree. Hand `pr-pitch` the branch purpose, the findings, and `.review/summary.md`. diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 730eb64502..c1b2d5c4e5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,6 +23,32 @@ jobs: - name: Checkout Files uses: actions/checkout@v7 id: checkout + with: + # comment-hygiene.ps1 diffs against origin/; a + # shallow, single-branch checkout leaves that ref unresolvable + # and the gate fails every build with "bad revision". + fetch-depth: 0 + + # Real-runtime check: this repo is authored on PowerShell 7 but + # build.ps1/test.ps1 run under Windows PowerShell 5.1 below. A script + # can parse identically on both and still resolve differently at + # runtime (see comment-hygiene's ASCII-replacement map, which used to + # do exactly that). windows-2022 ships both engines, so run the + # comment-hygiene fixture suite under each rather than assuming one + # implies the other. + - name: Comment hygiene fixture tests (PowerShell 7) + id: comment-hygiene-tests-pwsh + shell: pwsh + run: | + Build/Agent/CommentHygiene.Tests.ps1 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Comment hygiene fixture tests (Windows PowerShell 5.1) + id: comment-hygiene-tests-winps + shell: powershell + run: | + Build\Agent\CommentHygiene.Tests.ps1 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Build with tests id: build diff --git a/.github/workflows/stray-docs.yml b/.github/workflows/stray-docs.yml new file mode 100644 index 0000000000..b53e267f57 --- /dev/null +++ b/.github/workflows/stray-docs.yml @@ -0,0 +1,31 @@ +name: stray-docs + +on: + pull_request: + types: [opened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + stray-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Check for stray planning/spec docs + shell: bash + run: | + BASE_SHA="${{ github.event.pull_request.base.sha }}" + ADDED=$(git diff --name-only --diff-filter=A "${BASE_SHA}...HEAD" -- 'Docs/superpowers/plans/*.md' 'Docs/superpowers/specs/*.md') + if [[ -n "$ADDED" ]]; then + echo "Stray working docs found in this PR's diff:" + echo "$ADDED" + echo "" + echo "These are brainstorming/planning artifacts that should be evicted before merge (see the pr-pitch skill)." + exit 1 + fi + echo "No stray working docs found." diff --git a/.gitignore b/.gitignore index 2476e52091..5f8caa9db1 100644 --- a/.gitignore +++ b/.gitignore @@ -217,3 +217,5 @@ DataTreeTimingBaselines.json *[Ss]cratchPad* Docs/migration/working/ +Build/Agent/comment-hygiene-report.json +.review/ diff --git a/AGENTS.md b/AGENTS.md index 1b5afa39c5..eefd98985f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,10 @@ Minimal, high-signal guidance for coding agents in this repository. - Build with `.\build.ps1`. - Test with `.\test.ps1`. - Do not bypass repository scripts for normal build/test work. +- Commit messages must pass `gitlint` (CI: `.github/workflows/CommitMessage.yml`): + title <=72 characters, body lines <=80 characters, blank line between + them. A heredoc reproduces your line breaks exactly -- wrap body prose + by hand. See `.claude/skills/commit-messages/SKILL.md`. ## Critical constraints @@ -16,6 +20,8 @@ Minimal, high-signal guidance for coding agents in this repository. - Keep localization in `.resx`; do not hardcode translatable UI strings. - Follow the code-comment standard in `.claude/skills/fieldworks-code-commenting/SKILL.md`. +- Follow the commit-message rules in + `.claude/skills/commit-messages/SKILL.md` for every commit. ## Context model diff --git a/Build/Agent/CommentHygiene.Tests.ps1 b/Build/Agent/CommentHygiene.Tests.ps1 new file mode 100644 index 0000000000..fd50d7e685 --- /dev/null +++ b/Build/Agent/CommentHygiene.Tests.ps1 @@ -0,0 +1,211 @@ +<# +.SYNOPSIS + Fixture-based tests for CommentHygiene.psm1. + +.DESCRIPTION + One true-positive and one near-miss per category. Run directly: + pwsh -File Build/Agent/CommentHygiene.Tests.ps1 +#> + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Import-Module (Join-Path $PSScriptRoot 'CommentHygiene.psm1') -Force + +function Get-Codepoint { + <# + .SYNOPSIS + Builds a real Unicode character from a code point, portable across + PowerShell 7 and Windows PowerShell 5.1. + + .DESCRIPTION + Fixture strings must not use the backtick-u{} escape: it is + PowerShell 6+ only, and under 5.1 it silently degrades to the + literal text "u{2014}" instead of throwing -- exactly the bug this + suite exists to catch in the module, so the fixtures cannot carry + it themselves. + #> + param([int] $Codepoint) + if ($Codepoint -gt 0xFFFF) { return [char]::ConvertFromUtf32($Codepoint) } + return [string][char]$Codepoint +} + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("CommentHygieneTests_" + [System.Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $tempDir | Out-Null + +$failures = New-Object System.Collections.ArrayList + +function Assert-Category { + param([string] $Name, [string] $Line, [string] $ExpectedCategory, [string] $Extension = '.cs') + + $file = Join-Path $tempDir "$Name$Extension" + Set-Content -LiteralPath $file -Value $Line -Encoding UTF8 + $violations = Get-CommentHygieneViolations -Files @($file) + $hit = $violations | Where-Object { $_.Category -eq $ExpectedCategory } + if (-not $hit) { + [void]$script:failures.Add("FAIL [$Name]: expected category '$ExpectedCategory' for line: $Line") + } +} + +function Assert-Clean { + param([string] $Name, [string] $Line, [string] $Extension = '.cs') + + $file = Join-Path $tempDir "$Name$Extension" + Set-Content -LiteralPath $file -Value $Line -Encoding UTF8 + $violations = Get-CommentHygieneViolations -Files @($file) + if ($violations.Count -gt 0) { + $hitCategories = ($violations | ForEach-Object { $_.Category }) -join ',' + [void]$script:failures.Add("FAIL [$Name]: expected no violations for line: $Line -- got $hitCategories") + } +} + +function Assert-CategoryLines { + param([string] $Name, [string[]] $Lines, [string] $ExpectedCategory, [string] $Extension = '.cs') + + $file = Join-Path $tempDir "$Name$Extension" + Set-Content -LiteralPath $file -Value $Lines -Encoding UTF8 + $violations = Get-CommentHygieneViolations -Files @($file) + $hit = $violations | Where-Object { $_.Category -eq $ExpectedCategory } + if (-not $hit) { + [void]$script:failures.Add("FAIL [$Name]: expected category '$ExpectedCategory' for lines: $($Lines -join ' / ')") + } +} + +function Assert-CleanLines { + param([string] $Name, [string[]] $Lines, [string] $Extension = '.cs') + + $file = Join-Path $tempDir "$Name$Extension" + Set-Content -LiteralPath $file -Value $Lines -Encoding UTF8 + $violations = Get-CommentHygieneViolations -Files @($file) + if ($violations.Count -gt 0) { + $hitCategories = ($violations | ForEach-Object { $_.Category }) -join ',' + [void]$script:failures.Add("FAIL [$Name]: expected no violations for lines: $($Lines -join ' / ') -- got $hitCategories") + } +} + +function Assert-Repair { + param([string] $Name, [string] $Line, [string] $ExpectedFixedLine) + + $actual = Repair-CommentLine -Line $Line + if ($actual -ne $ExpectedFixedLine) { + [void]$script:failures.Add("FAIL [$Name]: expected repaired line '$ExpectedFixedLine', got '$actual' for input: $Line") + } +} + +function Assert-Unrepairable { + param([string] $Name, [string] $Line) + + $actual = Repair-CommentLine -Line $Line + if ($null -ne $actual) { + [void]$script:failures.Add("FAIL [$Name]: expected `$null (unrepairable) for line: $Line -- got '$actual'") + } +} + +# process-framing -- skill worked example +Assert-Category 'phase-framing' '// Phase 3 test (b): picking a style applies it to the selection' 'process-framing' +Assert-Clean 'phase-clean' '// Applies the selected style to the current selection' + +# process-framing must not fire on "stage"/"commit" here: this codebase has a real two-step stage-then-commit UI pattern, distinct from migration-phase framing. +Assert-Clean 'stage-domain-term' '// STAGE 1 -- the pick already populated the auxiliary picker.' +Assert-Clean 'commit-domain-term' '// Capture everything staged since the last boundary -- that is what this commit "writes".' + +# doc-pointer -- skill worked example +Assert-Category 'doc-pointer-md' '// winforms-free-lexeme-editor.md D1: a plugin-claimed custom slice renders its plugin''s own control' 'doc-pointer' +Assert-Clean 'doc-pointer-clean' '// LT-22351: a plugin-claimed custom slice renders its plugin''s own control' + +# doc-pointer finding-code must stay case-sensitive: "m3" is a real LCM field name (IMoStemMsa), not a design-doc finding code, despite the (?i) flag earlier in the pattern. +Assert-Clean 'doc-pointer-lowercase-field' '// Seed text matches the canonical field label (the m3 InflectionClass field label).' + +# doc-pointer finding-code must not fire on function keys or generic type-parameter names, which share the letter-plus-digit shape but are ordinary code vocabulary, not design-doc codes. +Assert-Clean 'doc-pointer-function-key' '// Whether it came from a legacy view, F5/RefreshAllViews-driven, or something else.' +Assert-Clean 'doc-pointer-generic-param' '// Factored to a Func seam so the caller can inject either path.' + +# absence-narration -- skill worked example +Assert-Category 'absence-no-longer' '// An ORC run no longer forces the whole value read-only.' 'absence-narration' +Assert-Clean 'absence-clean' '// An ORC run does not force the whole value read-only.' + +# cross-file-pointer +Assert-Category 'cross-file' "// See BulkEditBar's note about ownership checks." 'cross-file-pointer' +Assert-Clean 'cross-file-clean' '// Ownership checks run before every write in this method.' + +# provenance +Assert-Category 'provenance' '// This helper is shared by BulkEditBar and RecordClerk.' 'provenance' +Assert-Clean 'provenance-clean' '// Applies the pending edit to every selected row.' + +# non-ascii-punctuation +Assert-Category 'non-ascii-punctuation' ("// Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) 'non-ascii-punctuation' +Assert-Clean 'non-ascii-punctuation-clean' '// Uses a double hyphen -- inline.' + +# non-ascii-punctuation targets only Western-typography punctuation, not non-ASCII in general -- +# real non-English script or IPA/emoji content must not fire. +Assert-Clean 'non-ascii-punctuation-cyrillic' ("// Folds the Cyrillic letter {0} into the wrong letter group." -f (Get-Codepoint 0x0493)) +Assert-Clean 'non-ascii-punctuation-emoji' ('// The input string is "x{0}y" (a surrogate pair).' -f (Get-Codepoint 0x1F600)) + +# Repair-CommentLine -- mapped characters produce a fixed ASCII line +Assert-Repair 'repair-em-dash' ("// Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) '// Uses an em dash -- inline.' +Assert-Repair 'repair-arrow' ("// Flows left {0} right." -f (Get-Codepoint 0x2192)) '// Flows left -> right.' +Assert-Repair 'repair-ellipsis' ("// And so on {0}" -f (Get-Codepoint 0x2026)) '// And so on ...' +Assert-Repair 'repair-bullet' ("// {0} first item" -f (Get-Codepoint 0x2022)) '// - first item' +Assert-Repair 'repair-multiply' ("// A {0} B grid" -f (Get-Codepoint 0x00d7)) '// A x B grid' +Assert-Repair 'repair-doc-slash' ("/// Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) '/// Uses an em dash -- inline.' + +# Detection and repair share one character set by construction (the pattern is built from +# the replacement map's keys), so there is no "detected but unmapped" character to fail on. +$cjkLine = "// Some text with {0} inline." -f (Get-Codepoint 0x4e2d) +Assert-Repair 'repair-noop-cjk' $cjkLine $cjkLine + +# Repair-CommentLine -- not a whole-line comment at all +Assert-Unrepairable 'repair-not-a-comment' ("int x = 1; // trailing {0} comment" -f (Get-Codepoint 0x2014)) + +# PowerShell (#) gets the same categories as C# (//) -- the gap that let this tooling's own comments ship unscanned. +Assert-Category 'ps-phase-framing' '# Phase 3 test: picking a style applies it to the selection' 'process-framing' '.ps1' +Assert-Category 'ps-non-ascii-punctuation' ("# Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) 'non-ascii-punctuation' '.ps1' +Assert-Repair 'repair-ps-em-dash' ("# Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) '# Uses an em dash -- inline.' + +# Guards against a regression back to the PS7-only backtick-u{} escape. +$emDash = Get-Codepoint 0x2014 +if (-not (Get-NonAsciiReplacementMap).Contains($emDash)) { + [void]$script:failures.Add("FAIL [non-ascii-map-real-codepoint]: Get-NonAsciiReplacementMap does not key on the real em dash character (PSVersion $($PSVersionTable.PSVersion))") +} + +# comment-too-long fires on a 200-char budget across the whole block, not a line count -- a +# doc comment or PowerShell help block is exempt and may run long-form regardless of length. +Assert-CategoryLines 'too-long-cs' @( + '// This explains the first reason the approach was chosen over the alternative approach taken here for this specific case.', + '// This explains a second reason that would not fit on the first line at all today either, adding more detail.' +) 'comment-too-long' +Assert-CategoryLines 'too-long-single-line-cs' @( + '// This one very long line explains the reasoning all by itself without wrapping and keeps going for quite a while past what used to be the one-line cap until it is clearly over the two hundred character budget on its own.' +) 'comment-too-long' +Assert-CleanLines 'one-line-cs' @('// A single reason, on a single line, is exactly the budget.') +Assert-CleanLines 'under-budget-multiline-cs' @( + '// A short first reason for the approach, stated plainly.', + '// A short second reason that rounds out the explanation.' +) +Assert-CleanLines 'doc-comment-long-cs' @( + '/// A public API doc comment may run several lines when the contract genuinely needs it,', + '/// because a reader hovering the symbol has nowhere else to find this.' +) +Assert-CategoryLines 'too-long-ps' @( + '# This explains the first reason the approach was chosen over the alternative approach taken here for this specific case.', + '# This explains a second reason that would not fit on the first line at all today either, adding more detail.' +) 'comment-too-long' '.ps1' +Assert-CleanLines 'help-block-long-ps' @( + '<#', + '.SYNOPSIS', + ' Comment-based help is allowed to run long-form, the PowerShell equivalent of a doc comment.', + '#>' +) '.ps1' + +Remove-Item -LiteralPath $tempDir -Recurse -Force + +if ($failures.Count -gt 0) { + Write-Host '' + foreach ($f in $failures) { Write-Host $f -ForegroundColor Red } + Write-Host '' + Write-Host "$($failures.Count) test(s) failed." -ForegroundColor Red + exit 1 +} + +Write-Host 'All CommentHygiene tests passed.' -ForegroundColor Green +exit 0 diff --git a/Build/Agent/CommentHygiene.psm1 b/Build/Agent/CommentHygiene.psm1 new file mode 100644 index 0000000000..ae7b79df03 --- /dev/null +++ b/Build/Agent/CommentHygiene.psm1 @@ -0,0 +1,342 @@ +<# +.SYNOPSIS + Shared comment-hygiene scanning engine for FieldWorks C# and PowerShell files. + +.DESCRIPTION + Implements the mechanical (regex-detectable) banned-content categories + from the fieldworks-code-commenting skill, the ASCII-only rule, and a + one-line cap on implementation comments. Judgment-based rules + (accuracy, WHAT-not-HOW, standalone clarity) are not checked here. + + Scans .cs (//, ///) and .ps1 (#, block-comment) files. Only whole-line + comments are scanned; a trailing same-line comment is not. A doc comment + or a PowerShell help block is exempt from the length cap. (This help + block cannot spell out that block-comment syntax literally -- PowerShell + does not nest it, and the first close token would end this block early.) + +.NOTES + Import this module from comment-hygiene.ps1, comment-hygiene-repair.ps1, + and comment-hygiene-blame.ps1: + Import-Module "$PSScriptRoot/CommentHygiene.psm1" -Force +#> + +Set-StrictMode -Version Latest + +function Get-CommentHygieneCategories { + <# + .SYNOPSIS + Returns the ordered category-name to regex-pattern map. + #> + return [ordered]@{ + # "Stage N"/"this commit" excluded: this codebase's own "stage-1/2" and Commit() are domain terms, not migration framing. + 'process-framing' = '(?i)\bPhase[\s-]?\d+\b|\blater we\x27ll\b|\bwe\x27ll (?:later|eventually)\b' + # (?-i:...) forces case-sensitivity despite the earlier (?i); F-keys and T-generics are excluded as letter-plus-digit look-alikes. + 'doc-pointer' = '\b[\w./-]+\.md\b|(?i)\bsection\s+\d+[a-z]?\b|(?-i:(? + $escaped = (Get-NonAsciiReplacementMap).Keys | ForEach-Object { [regex]::Escape($_) } + return '(?:' + ($escaped -join '|') + ')' +} + +function Get-CommentBody { + <# + .SYNOPSIS + Returns the text after // or ///, or $null if the line is not a whole-line C# comment. + #> + param([Parameter(Mandatory)][AllowEmptyString()][string] $Line) + + $trimmed = $Line.Trim() + if ($trimmed.StartsWith('///')) { return $trimmed.Substring(3) } + if ($trimmed.StartsWith('//')) { return $trimmed.Substring(2) } + return $null +} + +function Get-NonAsciiReplacementMap { + <# + .SYNOPSIS + Returns the ordered map of non-ASCII characters to their ASCII replacement text. + + .DESCRIPTION + Keys are built from [char] code points, not the backtick-u{} escape + (that escape is PowerShell 6+ only; under Windows PowerShell 5.1 the + backtick is silently dropped and the literal text "u{2014}" remains, + so the map would never match a real em dash). + #> + $map = [ordered]@{} + $map[[string][char]0x2014] = '--' + $map[[string][char]0x2013] = '-' + $map[[string][char]0x2192] = '->' + $map[[string][char]0x2190] = '<-' + $map[[string][char]0x2194] = '<->' + $map[[string][char]0x2026] = '...' + $map[[string][char]0x22ee] = '...' + $map[[string][char]0x2022] = '-' + $map[[string][char]0x00d7] = 'x' + $map[[string][char]0x2018] = "'" + $map[[string][char]0x2019] = "'" + $map[[string][char]0x201c] = '"' + $map[[string][char]0x201d] = '"' + $map[[string][char]0x00a7] = 'Section' + return $map +} + +function Set-CommentHygieneFileContent { + <# + .SYNOPSIS + Writes lines back to a file with an explicit BOM choice. + + .DESCRIPTION + Uses System.Text.UTF8Encoding directly instead of + Set-Content -Encoding utf8BOM/utf8NoBOM: those encoding names are + PowerShell 7+ only and throw a parameter-binding error under + Windows PowerShell 5.1. + + .PARAMETER Utf8Bom + Whether the written file should carry a UTF-8 byte-order mark. + #> + param( + [Parameter(Mandatory)][string] $Path, + [Parameter(Mandatory)][AllowEmptyCollection()][string[]] $Lines, + [Parameter(Mandatory)][bool] $Utf8Bom + ) + + $encoding = [System.Text.UTF8Encoding]::new($Utf8Bom) + [System.IO.File]::WriteAllLines($Path, $Lines, $encoding) +} + +function Repair-CommentLine { + <# + .SYNOPSIS + Applies the ASCII replacement map to a whole-line comment. + + .PARAMETER Line + A single raw source line. + + .OUTPUTS + The fixed line if the line is a whole-line // or # comment and every + non-ascii-punctuation character in it is covered by + Get-NonAsciiReplacementMap; otherwise $null (not a whole-line comment, + or an unmapped non-ascii-punctuation character remains). Any other + non-ASCII content (real script/IPA text) is left untouched and does + not block repair -- only the specific characters in the replacement + map are ever in scope. + + Only ever called on lines Get-CommentHygieneViolations already + classified as a comment for that file's language, so a bare `#` + here is never a PowerShell string or a C# directive -- the caller + guarantees that, this function does not re-derive the file's language. + #> + param([Parameter(Mandatory)][AllowEmptyString()][string] $Line) + + $trimmed = $Line.Trim() + $prefixLength = $Line.Length - $Line.TrimStart().Length + $leadingWhitespace = $Line.Substring(0, $prefixLength) + + $prefix = $null + $body = $null + if ($trimmed.StartsWith('///')) { $prefix = '///'; $body = $trimmed.Substring(3) } + elseif ($trimmed.StartsWith('//')) { $prefix = '//'; $body = $trimmed.Substring(2) } + elseif ($trimmed.StartsWith('#') -and -not $trimmed.StartsWith('#>')) { $prefix = '#'; $body = $trimmed.Substring(1) } + + if ($null -eq $prefix) { return $null } + + $fixedBody = $body + $replacementMap = Get-NonAsciiReplacementMap + foreach ($key in $replacementMap.Keys) { + $fixedBody = $fixedBody -replace [regex]::Escape($key), $replacementMap[$key] + } + + if ($fixedBody -match (Get-NonAsciiPunctuationPattern)) { return $null } + + return "$leadingWhitespace$prefix$fixedBody" +} + +function Get-CommentLineClassification { + <# + .SYNOPSIS + Classifies every line of a file as an implementation comment (capped + at one line), an exempt doc/help comment, or neither. + + .PARAMETER Lines + The file's lines. + + .PARAMETER IsPowerShell + Whether to use PowerShell (#, block-comment) or C# (//, ///) comment + syntax. A bare # is never treated as a comment for a C# file, so a + preprocessor directive (#region, #if) is never misread as one. + + .OUTPUTS + A hashtable with parallel arrays Kinds ('impl', 'exempt', or $null + per line) and Bodies (comment text per line, or $null). + #> + param( + [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Lines, + [Parameter(Mandatory)][bool] $IsPowerShell + ) + + $kinds = New-Object 'object[]' $Lines.Count + $bodies = New-Object 'object[]' $Lines.Count + $inHelpBlock = $false + + for ($i = 0; $i -lt $Lines.Count; $i++) { + $trimmed = $Lines[$i].Trim() + + if ($IsPowerShell) { + if ($inHelpBlock) { + $kinds[$i] = 'exempt' + $bodies[$i] = $trimmed + if ($trimmed.EndsWith('#>')) { $inHelpBlock = $false } + continue + } + if ($trimmed.StartsWith('<#')) { + $kinds[$i] = 'exempt' + $bodies[$i] = $trimmed.Substring(2).TrimEnd('#', '>', ' ') + if (-not $trimmed.EndsWith('#>')) { $inHelpBlock = $true } + continue + } + if ($trimmed.StartsWith('#')) { + $kinds[$i] = 'impl' + $bodies[$i] = $trimmed.Substring(1) + continue + } + } + else { + $body = Get-CommentBody -Line $Lines[$i] + if ($null -ne $body) { + $kinds[$i] = if ($trimmed.StartsWith('///')) { 'exempt' } else { 'impl' } + $bodies[$i] = $body + continue + } + } + + $kinds[$i] = $null + $bodies[$i] = $null + } + + return @{ Kinds = $kinds; Bodies = $bodies } +} + +function Get-CommentHygieneViolations { + <# + .SYNOPSIS + Scans the given files for mechanical comment-hygiene violations. + + .PARAMETER Files + Absolute paths to .cs or .ps1 files to scan. + + .PARAMETER LineFilter + Optional hashtable mapping an absolute file path to a + HashSet[int] of 1-based line numbers to check. Omit to scan every + line in every file. + + .OUTPUTS + One PSCustomObject per violation: File, Line, Category, Text. + Category 'comment-too-long' additionally covers a run of + consecutive implementation-comment lines whose combined text + exceeds a character budget; a doc comment or a PowerShell help + block is exempt from that one. + #> + param( + [Parameter(Mandatory)][string[]] $Files, + [hashtable] $LineFilter + ) + + $categories = Get-CommentHygieneCategories + $violations = New-Object System.Collections.ArrayList + $maxImplCommentChars = 200 + + foreach ($file in $Files) { + if (-not (Test-Path -LiteralPath $file)) { continue } + + $allowedLines = $null + if ($LineFilter -and $LineFilter.ContainsKey($file)) { + $allowedLines = $LineFilter[$file] + } + + # Explicit UTF8: Windows PowerShell 5.1's Get-Content default for a BOM-less file is the system codepage, not UTF-8. + $lines = @(Get-Content -LiteralPath $file -Encoding UTF8) + $isPowerShell = ($file -like '*.ps1') -or ($file -like '*.psm1') + $classification = Get-CommentLineClassification -Lines $lines -IsPowerShell $isPowerShell + $kinds = $classification.Kinds + $bodies = $classification.Bodies + + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($null -eq $kinds[$i]) { continue } + $lineNumber = $i + 1 + if ($allowedLines -and -not $allowedLines.Contains($lineNumber)) { continue } + + foreach ($category in $categories.Keys) { + if ($bodies[$i] -match $categories[$category]) { + [void]$violations.Add([PSCustomObject]@{ + File = $file + Line = $lineNumber + Category = $category + Text = $bodies[$i].Trim() + }) + } + } + } + + $blockStart = -1 + $blockLength = 0 + for ($i = 0; $i -le $lines.Count; $i++) { + $isImplLine = ($i -lt $lines.Count) -and ($kinds[$i] -eq 'impl') + if ($isImplLine) { + if ($blockStart -lt 0) { $blockStart = $i } + $blockLength++ + continue + } + + if ($blockLength -gt 0) { + $blockIndexes = $blockStart..($blockStart + $blockLength - 1) + $totalChars = [int]($blockIndexes | ForEach-Object { $bodies[$_].Trim().Length } | Measure-Object -Sum).Sum + if ($totalChars -gt $maxImplCommentChars) { + # Blame only this diff's contribution: skip if the untouched lines alone already exceeded budget. + $untouchedChars = 0 + if ($allowedLines) { + $untouchedIndexes = $blockIndexes | Where-Object { -not $allowedLines.Contains($_ + 1) } + if ($untouchedIndexes) { + $untouchedChars = [int]($untouchedIndexes | ForEach-Object { $bodies[$_].Trim().Length } | Measure-Object -Sum).Sum + } + } + if ($untouchedChars -le $maxImplCommentChars) { + [void]$violations.Add([PSCustomObject]@{ + File = $file + Line = $blockStart + 1 + Category = 'comment-too-long' + Text = ("{0} chars: {1}" -f $totalChars, $bodies[$blockStart].Trim()) + }) + } + } + } + $blockStart = -1 + $blockLength = 0 + } + } + + # The unary comma prevents PowerShell's pipeline from unrolling a + # zero- or one-element array into $null or a bare scalar on return. + return ,$violations.ToArray() +} + +Export-ModuleMember -Function @( + 'Get-CommentHygieneCategories', + 'Get-CommentBody', + 'Get-CommentLineClassification', + 'Get-CommentHygieneViolations', + 'Get-NonAsciiReplacementMap', + 'Get-NonAsciiPunctuationPattern', + 'Repair-CommentLine', + 'Set-CommentHygieneFileContent' +) diff --git a/Build/Agent/comment-hygiene-blame.ps1 b/Build/Agent/comment-hygiene-blame.ps1 new file mode 100644 index 0000000000..e8384de2ce --- /dev/null +++ b/Build/Agent/comment-hygiene-blame.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS + Attributes every full-repo comment-hygiene violation to its introducing commit. + +.DESCRIPTION + Runs Get-CommentHygieneViolations in full-repo mode, then git-blames each + violating line to find who introduced it and when, so a human can triage + existing debt deliberately instead of the ratchet gate accepting it + silently forever. Writes one JSON array to -OutputPath. + +.PARAMETER OutputPath + Path to write the JSON report. + +.EXAMPLE + Build/Agent/comment-hygiene-blame.ps1 -OutputPath triage.json +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)][string] $OutputPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +Import-Module (Join-Path $PSScriptRoot 'CommentHygiene.psm1') -Force + +function Test-ExcludedPath { + param([string] $Path) + return ($Path -match '\.g\.cs$') -or ($Path -match 'Designer\.cs$') +} + +function ConvertTo-RepoPath { + param([string] $RelativePath) + return Join-Path $repoRoot ($RelativePath -replace '/', [IO.Path]::DirectorySeparatorChar) +} + +function Get-BlameInfo { + param([string] $File, [int] $Line) + + $porcelain = git blame -L "$Line,$Line" --porcelain -- $File 2>$null + if (-not $porcelain) { return $null } + + $sha = ($porcelain[0] -split ' ')[0] + $authorLine = $porcelain | Where-Object { $_ -like 'author *' } | Select-Object -First 1 + $emailLine = $porcelain | Where-Object { $_ -like 'author-mail *' } | Select-Object -First 1 + $timeLine = $porcelain | Where-Object { $_ -like 'author-time *' } | Select-Object -First 1 + + $author = if ($authorLine) { $authorLine.Substring(7) } else { 'unknown' } + $email = if ($emailLine) { $emailLine.Substring(12).Trim('<', '>') } else { '' } + $epoch = if ($timeLine) { [int64]($timeLine.Substring(12)) } else { 0 } + $date = if ($epoch -gt 0) { [DateTimeOffset]::FromUnixTimeSeconds($epoch).UtcDateTime.ToString('yyyy-MM-dd') } else { '' } + + $subject = (git log -1 --format=%s $sha 2>$null) + $bodyLines = git log -1 --format=%B $sha 2>$null + $body = ($bodyLines -join "`n") + $hasAiTrailer = $body -match '(?i)co-authored-by:.*claude|generated with claude' + + return [PSCustomObject]@{ + CommitSha = $sha + Author = "$author <$email>" + Date = $date + Subject = $subject + HasAiCoAuthorTrailer = [bool]$hasAiTrailer + } +} + +$files = git ls-files '*.cs' '*.ps1' '*.psm1' | ForEach-Object { ConvertTo-RepoPath $_ } | Where-Object { -not (Test-ExcludedPath $_) } +Write-Host "comment-hygiene-blame: scanning $($files.Count) file(s)..." + +$violations = Get-CommentHygieneViolations -Files $files +Write-Host "comment-hygiene-blame: $($violations.Count) violation(s) found; resolving blame..." + +$report = New-Object System.Collections.ArrayList +$index = 0 + +foreach ($v in $violations) { + $index++ + if ($index % 50 -eq 0) { Write-Host " ...$index/$($violations.Count)" } + + $blame = Get-BlameInfo -File $v.File -Line $v.Line + $relative = $v.File.Substring($repoRoot.Length + 1) -replace '\\', '/' + + [void]$report.Add([PSCustomObject]@{ + file = $relative + line = $v.Line + category = $v.Category + text = $v.Text + commitSha = if ($blame) { $blame.CommitSha } else { $null } + author = if ($blame) { $blame.Author } else { $null } + date = if ($blame) { $blame.Date } else { $null } + subject = if ($blame) { $blame.Subject } else { $null } + hasAiCoAuthorTrailer = if ($blame) { $blame.HasAiCoAuthorTrailer } else { $false } + }) +} + +Set-CommentHygieneFileContent -Path $OutputPath -Lines @($report | ConvertTo-Json -Depth 4) -Utf8Bom $false +Write-Host "comment-hygiene-blame: wrote $($report.Count) record(s) to $OutputPath" diff --git a/Build/Agent/comment-hygiene-repair.ps1 b/Build/Agent/comment-hygiene-repair.ps1 new file mode 100644 index 0000000000..d70aec8a8b --- /dev/null +++ b/Build/Agent/comment-hygiene-repair.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + One-time non-ascii-punctuation comment repair sweep for an explicit file list. + +.DESCRIPTION + Runs Get-CommentHygieneViolations against the given files, filtered to + the non-ascii-punctuation category, and applies Repair-CommentLine to each hit, + writing fixes back to disk. Reports how many lines were fixed, in how + many files, and lists any (file, line, text) that could not be + auto-fixed because the line carries a character outside + Get-NonAsciiReplacementMap, so a human can handle those individually. + + Accepts an explicit file list rather than a commit SHA or -Full scope, + since the caller is expected to already know which files are in scope + (e.g. from a comment-hygiene-blame.ps1 triage report). + +.PARAMETER Files + Paths (absolute or relative to the current directory) to .cs files to sweep. + +.EXAMPLE + Build/Agent/comment-hygiene-repair.ps1 -Files (Get-Content scoped-files.txt) +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)][string[]] $Files +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Import-Module (Join-Path $PSScriptRoot 'CommentHygiene.psm1') -Force + +function Test-Utf8Bom { + param([string] $Path) + $bytes = [System.IO.File]::ReadAllBytes($Path) + return ($bytes.Length -ge 3) -and ($bytes[0] -eq 0xEF) -and ($bytes[1] -eq 0xBB) -and ($bytes[2] -eq 0xBF) +} + +$resolvedFiles = $Files | ForEach-Object { (Resolve-Path -LiteralPath $_).Path } + +# Assign before piping -- Get-CommentHygieneViolations's comma-return makes a direct pipe into Where-Object deliver one bundled array instead of filtering per-element. +$allViolations = Get-CommentHygieneViolations -Files $resolvedFiles +$violations = $allViolations | Where-Object { $_.Category -eq 'non-ascii-punctuation' } + +$fixedFiles = @{} +$fixedCount = 0 +$unrepairable = New-Object System.Collections.ArrayList + +foreach ($v in $violations) { + $hadBom = Test-Utf8Bom -Path $v.File + $fileLines = @(Get-Content -LiteralPath $v.File -Encoding UTF8) + $fixedLine = Repair-CommentLine -Line $fileLines[$v.Line - 1] + if ($null -eq $fixedLine) { + [void]$unrepairable.Add($v) + continue + } + + $fileLines[$v.Line - 1] = $fixedLine + Set-CommentHygieneFileContent -Path $v.File -Lines $fileLines -Utf8Bom $hadBom + $fixedFiles[$v.File] = $true + $fixedCount++ +} + +Write-Host "comment-hygiene-repair: fixed $fixedCount non-ascii-punctuation comment line(s) in $($fixedFiles.Keys.Count) file(s)." + +if ($unrepairable.Count -gt 0) { + Write-Host '' + Write-Host "comment-hygiene-repair: $($unrepairable.Count) violation(s) could NOT be auto-fixed (unmapped character(s)); handle these individually:" -ForegroundColor Yellow + foreach ($v in $unrepairable) { + Write-Host (" {0}:{1} {2}" -f $v.File, $v.Line, $v.Text) + } +} diff --git a/Build/Agent/comment-hygiene.ps1 b/Build/Agent/comment-hygiene.ps1 new file mode 100644 index 0000000000..0720dcf9f2 --- /dev/null +++ b/Build/Agent/comment-hygiene.ps1 @@ -0,0 +1,182 @@ +<# +.SYNOPSIS + Diff-scoped comment-hygiene gate for FieldWorks C# and PowerShell comments. + +.DESCRIPTION + Enforces the mechanical banned-content categories, plus a one-line cap on + implementation comments, against lines a diff ADDS, not the whole + repository. Legacy comments are never flagged unless their line is + touched again. + +.PARAMETER BaseRef + Git ref to diff against. Defaults to the PR base in CI + (GITHUB_EVENT_PULL_REQUEST_BASE_SHA, then GITHUB_BASE_REF), else the + local merge-base with the origin default branch. + +.PARAMETER Full + Report-only mode: scans every tracked .cs/.ps1 file at HEAD instead of + the diff, and never exits non-zero. + +.PARAMETER List + Show every violation. Implied by -Full. + +.EXAMPLE + Build/Agent/comment-hygiene.ps1 + Gate the current diff against the local merge-base with the default branch. + +.EXAMPLE + Build/Agent/comment-hygiene.ps1 -Full -List + Report every mechanical violation in the whole repo, without failing. +#> +[CmdletBinding()] +param( + [string] $BaseRef, + [switch] $Full, + [switch] $List +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +Import-Module (Join-Path $PSScriptRoot 'CommentHygiene.psm1') -Force + +function Test-ExcludedPath { + param([string] $Path) + return ($Path -match '\.g\.cs$') -or ($Path -match 'Designer\.cs$') +} + +function Test-Utf8Bom { + param([string] $Path) + $bytes = [System.IO.File]::ReadAllBytes($Path) + return ($bytes.Length -ge 3) -and ($bytes[0] -eq 0xEF) -and ($bytes[1] -eq 0xBB) -and ($bytes[2] -eq 0xBF) +} + +function ConvertTo-RepoPath { + param([string] $RelativePath) + return Join-Path $repoRoot ($RelativePath -replace '/', [IO.Path]::DirectorySeparatorChar) +} + +function Resolve-BaseRef { + param([string] $Explicit) + + if (-not [string]::IsNullOrWhiteSpace($Explicit)) { return $Explicit } + if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_EVENT_PULL_REQUEST_BASE_SHA)) { return $env:GITHUB_EVENT_PULL_REQUEST_BASE_SHA } + if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_BASE_REF)) { return "origin/$env:GITHUB_BASE_REF" } + + $defaultBranch = 'main' + foreach ($remoteLine in (git remote show origin 2>$null)) { + if ($remoteLine -match 'HEAD branch:\s*(\S+)') { + $defaultBranch = $Matches[1] + break + } + } + return "origin/$defaultBranch" +} + +function Get-AddedLineFilter { + param([string] $Base) + + $diff = git diff --unified=0 "$Base...HEAD" -- '*.cs' '*.ps1' '*.psm1' 2>$null + if ($LASTEXITCODE -ne 0) { + throw "git diff against '$Base' failed. Is the base ref fetched? (CI needs fetch-depth: 0.)" + } + + $filter = @{} + $currentFile = $null + $currentLine = 0 + + foreach ($rawLine in $diff) { + if ($rawLine -match '^\+\+\+ b/(.+)$') { + $currentFile = ConvertTo-RepoPath $Matches[1] + continue + } + if ($rawLine -match '^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@') { + $currentLine = [int]$Matches[1] + continue + } + if ($null -eq $currentFile) { continue } + if ($rawLine.StartsWith('+') -and -not $rawLine.StartsWith('+++')) { + if (-not (Test-ExcludedPath $currentFile)) { + if (-not $filter.ContainsKey($currentFile)) { + $filter[$currentFile] = [System.Collections.Generic.HashSet[int]]::new() + } + [void]$filter[$currentFile].Add($currentLine) + } + $currentLine++ + } + } + + return $filter +} + +function Write-Violation { + param($Violation) + $relative = $Violation.File.Substring($repoRoot.Length + 1) + Write-Host (" {0}:{1} [{2}] {3}" -f $relative, $Violation.Line, $Violation.Category, $Violation.Text) +} + +if ($Full) { + $files = git ls-files '*.cs' '*.ps1' '*.psm1' | ForEach-Object { ConvertTo-RepoPath $_ } | Where-Object { -not (Test-ExcludedPath $_) } + $violations = Get-CommentHygieneViolations -Files $files + + Write-Host "comment-hygiene -Full: $($violations.Count) violation(s) across $($files.Count) file(s)" + foreach ($v in $violations) { Write-Violation $v } + exit 0 +} + +$base = Resolve-BaseRef -Explicit $BaseRef +Write-Host "comment-hygiene: scanning lines added since $base" + +$lineFilter = Get-AddedLineFilter -Base $base +if ($lineFilter.Count -eq 0) { + Write-Host 'comment-hygiene: no added .cs/.ps1/.psm1 lines to check.' + exit 0 +} + +$violations = Get-CommentHygieneViolations -Files @($lineFilter.Keys) -LineFilter $lineFilter + +# CI can't commit a fix back, so only auto-fix where a human can review and commit it. +$isCI = ($env:GITHUB_ACTIONS -eq 'true') -or ($env:CI -eq 'true') + +$remainingViolations = New-Object System.Collections.ArrayList +$fixedFiles = @{} +$fixedCount = 0 + +foreach ($v in $violations) { + if ($isCI -or $v.Category -ne 'non-ascii-punctuation') { + [void]$remainingViolations.Add($v) + continue + } + + $hadBom = Test-Utf8Bom -Path $v.File + $fileLines = @(Get-Content -LiteralPath $v.File -Encoding UTF8) + $fixedLine = Repair-CommentLine -Line $fileLines[$v.Line - 1] + if ($null -eq $fixedLine) { + [void]$remainingViolations.Add($v) + continue + } + + $fileLines[$v.Line - 1] = $fixedLine + Set-CommentHygieneFileContent -Path $v.File -Lines $fileLines -Utf8Bom $hadBom + $fixedFiles[$v.File] = $true + $fixedCount++ +} + +$violations = $remainingViolations.ToArray() + +if ($fixedCount -gt 0) { + Write-Host "comment-hygiene: auto-fixed $fixedCount non-ascii-punctuation comment line(s) in $($fixedFiles.Keys.Count) file(s) (review and include in your commit)." -ForegroundColor Yellow +} + +if ($violations.Count -eq 0) { + Write-Host 'comment-hygiene: clean.' + exit 0 +} + +Write-Host '' +Write-Host "comment-hygiene: $($violations.Count) violation(s) in added lines" -ForegroundColor Red +foreach ($v in $violations) { Write-Violation $v } +Write-Host '' +Write-Host 'Fix per .claude/skills/fieldworks-code-commenting/SKILL.md, or rewrite the comment.' -ForegroundColor Red +exit 1 diff --git a/Build/Agent/powershell-compat.ps1 b/Build/Agent/powershell-compat.ps1 new file mode 100644 index 0000000000..4072b67a77 --- /dev/null +++ b/Build/Agent/powershell-compat.ps1 @@ -0,0 +1,144 @@ +<# +.SYNOPSIS + Static PowerShell-version-compatibility gate for the comment-hygiene tooling. + +.DESCRIPTION + Two independent static layers, neither of which requires more than one + PowerShell engine to actually be installed: + + 1. A dependency-free regex scan for known gotchas where 5.1 and 7 both + parse the same text successfully but disagree on its meaning, so no + AST-based tool can see the difference: the backtick u{} escape + (5.1 silently drops it instead of resolving the code point) and the + utf8BOM/utf8NoBOM -Encoding values (5.1 does not recognize them at + all). This layer always runs and never needs installing anything. + + 2. PSScriptAnalyzer's PSUseCompatibleSyntax rule, which catches + structural grammar additions (ternary, null-coalescing, the + null-conditional operators, pipeline chain operators, and more) by + checking the parsed script against Microsoft's maintained per-version + grammar profiles -- it does not need 5.1 itself to be installed, + only its own module. This layer is best-effort: if the module is + missing and cannot be installed (offline, restricted network), this + script warns and skips it rather than failing the build over a + missing optional dependency. + + Neither layer is a substitute for actually running under both engines: + that is the only way to catch every possible semantic difference, and + it requires both engines to be present, which this script does not + assume. CI (windows-2022 runners ship both powershell.exe and pwsh) runs + CommentHygiene.Tests.ps1 under both as that real-runtime check; this + script is the cheaper static complement, runnable anywhere. + +.PARAMETER Full + Report every hit without failing. + +.EXAMPLE + Build/Agent/powershell-compat.ps1 +#> +[CmdletBinding()] +param( + [switch] $Full +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Import-Module (Join-Path $PSScriptRoot 'CommentHygiene.psm1') -Force + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$targetFiles = @( + 'Build/Agent/CommentHygiene.psm1', + 'Build/Agent/CommentHygiene.Tests.ps1', + 'Build/Agent/comment-hygiene.ps1', + 'Build/Agent/comment-hygiene-repair.ps1', + 'Build/Agent/comment-hygiene-blame.ps1', + 'Build/Agent/powershell-compat.ps1' +) | ForEach-Object { Join-Path $repoRoot $_ } | Where-Object { Test-Path -LiteralPath $_ } + +$violations = New-Object System.Collections.ArrayList + +# Layer 1: dependency-free regex scan for known parse-both-ways-differently gotchas. +$gotchaPatterns = [ordered]@{ + 'backtick-unicode-escape' = @{ + Pattern = '`[uU]\{[0-9a-fA-F]+\}' + Message = 'Backtick u{} escape is PowerShell 6+ only; 5.1 drops the backtick and keeps the literal text. Use [char] / [char]::ConvertFromUtf32 instead.' + } + 'ps7-only-encoding-value' = @{ + Pattern = '-Encoding\s+([''"]?)(utf8BOM|utf8NoBOM)\1\b' + Message = '-Encoding utf8BOM/utf8NoBOM is PowerShell 6+ only and throws a parameter-binding error under 5.1. Use System.Text.UTF8Encoding directly for explicit BOM control.' + } +} + +# Excludes itself: $gotchaPatterns must spell out each pattern's literal +# text, which would otherwise flag the definition line as an instance of it. +$regexScanFiles = $targetFiles | Where-Object { $_ -ne (Join-Path $repoRoot 'Build/Agent/powershell-compat.ps1') } + +foreach ($file in $regexScanFiles) { + $lines = @(Get-Content -LiteralPath $file -Encoding UTF8) + # Comment lines are excluded: this tooling's own doc comments describe the + # gotcha patterns in prose, which would otherwise self-match as a violation. + $classification = Get-CommentLineClassification -Lines $lines -IsPowerShell $true + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($null -ne $classification.Kinds[$i]) { continue } + foreach ($gotchaName in $gotchaPatterns.Keys) { + if ($lines[$i] -match $gotchaPatterns[$gotchaName].Pattern) { + [void]$violations.Add([PSCustomObject]@{ + File = $file.Substring($repoRoot.Length + 1) + Line = $i + 1 + Message = $gotchaPatterns[$gotchaName].Message + }) + } + } + } +} + +# Layer 2: PSScriptAnalyzer's PSUseCompatibleSyntax, best-effort. $Global: +# persists across repeated calls in one terminal session, so a failed +# install costs one timeout, not one per build. +if ((-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) -and -not $Global:FwPowerShellCompatAnalyzerUnavailable) { + try { + Write-Host 'powershell-compat: installing PSScriptAnalyzer (first run only)...' + Install-Module -Name PSScriptAnalyzer -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop | Out-Null + } + catch { + Write-Host "powershell-compat: PSScriptAnalyzer unavailable and could not be installed ($($_.Exception.Message)); skipping the PSUseCompatibleSyntax layer for the rest of this session." -ForegroundColor Yellow + $Global:FwPowerShellCompatAnalyzerUnavailable = $true + } +} + +if (Get-Module -ListAvailable -Name PSScriptAnalyzer) { + Import-Module PSScriptAnalyzer -Force + $settings = @{ + IncludeRules = @('PSUseCompatibleSyntax') + Rules = @{ + PSUseCompatibleSyntax = @{ + Enable = $true + TargetVersions = @('5.1', '7.0') + } + } + } + foreach ($file in $targetFiles) { + $results = Invoke-ScriptAnalyzer -Path $file -Settings $settings + foreach ($r in $results) { + [void]$violations.Add([PSCustomObject]@{ + File = $file.Substring($repoRoot.Length + 1) + Line = $r.Line + Message = $r.Message + }) + } + } +} + +if ($violations.Count -eq 0) { + Write-Host 'powershell-compat: clean (5.1 and 7.0).' + exit 0 +} + +Write-Host "powershell-compat: $($violations.Count) syntax incompatibility(ies) found" -ForegroundColor Red +foreach ($v in $violations) { + Write-Host (" {0}:{1} {2}" -f $v.File, $v.Line, $v.Message) +} + +if ($Full) { exit 0 } +exit 1 diff --git a/Build/Src/FwBuildTasks/FwBuildTasksTests/LocalizeFieldWorksTests.cs b/Build/Src/FwBuildTasks/FwBuildTasksTests/LocalizeFieldWorksTests.cs index 0722de67cd..723f9ea913 100644 --- a/Build/Src/FwBuildTasks/FwBuildTasksTests/LocalizeFieldWorksTests.cs +++ b/Build/Src/FwBuildTasks/FwBuildTasksTests/LocalizeFieldWorksTests.cs @@ -597,7 +597,7 @@ public void AddedStringsReported() CreateResX(m_FdoFolder, badFilenameBase, "some text"); var badFile = CreateLocalizedResXFor(m_FdoFolder, badFilenameBase, LocaleGe, "just fine", dataName2: extraDataName, textValue2: "not fine"); - // ProjectLocalizer.CheckResXForErrors no longer fails builds on added/missing keys. + // ProjectLocalizer.CheckResXForErrors treats added/missing keys as non-fatal. Assert.That(m_sut.Execute(), Is.True, m_sut.ErrorMessages); Assert.That(m_sut.ErrorMessages, Does.Not.Contain(badFile)); Assert.That(m_sut.ErrorMessages, Does.Not.Contain(extraDataName)); @@ -612,7 +612,7 @@ public void MissingStringsReported() CreateResX(m_FdoFolder, badFilenameBase, "some text", dataName2: extraDataName, textValue2: "you can't find me!"); var badFile = CreateLocalizedResXFor(m_FdoFolder, badFilenameBase, LocaleGe, "only one"); - // ProjectLocalizer.CheckResXForErrors no longer fails builds on added/missing keys. + // ProjectLocalizer.CheckResXForErrors treats added/missing keys as non-fatal. Assert.That(m_sut.Execute(), Is.True, m_sut.ErrorMessages); Assert.That(m_sut.ErrorMessages, Does.Not.Contain(badFile)); Assert.That(m_sut.ErrorMessages, Does.Not.Contain(extraDataName)); diff --git a/Build/Src/FwBuildTasks/FwBuildTasksTests/RegFreeCreatorTests.cs b/Build/Src/FwBuildTasks/FwBuildTasksTests/RegFreeCreatorTests.cs index 06560ce753..523b839f5c 100644 --- a/Build/Src/FwBuildTasks/FwBuildTasksTests/RegFreeCreatorTests.cs +++ b/Build/Src/FwBuildTasks/FwBuildTasksTests/RegFreeCreatorTests.cs @@ -50,7 +50,6 @@ public void ProcessManagedAssembly_PlacesClrClassAsChildOfAssembly() // Windows SxS requires clrClass to be a direct child of assembly, not nested under file. // Nesting under file causes "side-by-side configuration is incorrect" errors at runtime. - // See: specs/003-convergence-regfree-com-coverage/REGFREE_BEST_PRACTICES.md var nestedClrClass = fileNode.SelectSingleNode("asmv1:clrClass", ns); Assert.That(nestedClrClass, Is.Null, "clrClass must NOT be nested under file element (causes SxS errors)."); diff --git a/Build/Src/FwBuildTasks/RegFreeCreator.cs b/Build/Src/FwBuildTasks/RegFreeCreator.cs index a24d8873ea..d6acc7578f 100644 --- a/Build/Src/FwBuildTasks/RegFreeCreator.cs +++ b/Build/Src/FwBuildTasks/RegFreeCreator.cs @@ -431,7 +431,6 @@ out bool typeVal fileNode = GetOrCreateFileNode(parent, fileName); } // clrClass must be a direct child of assembly, not of file - // See: specs/003-convergence-regfree-com-coverage/REGFREE_BEST_PRACTICES.md AddOrReplaceClrClass( parent, clsId, @@ -907,7 +906,6 @@ string proxyStubClsid32 /// Windows SxS requires clrClass elements to be direct children of the assembly element, /// not nested inside file elements. Nesting under file causes "side-by-side configuration /// is incorrect" errors at runtime. - /// See: specs/003-convergence-regfree-com-coverage/REGFREE_BEST_PRACTICES.md /// /// The assembly element (must be the root assembly, not a file element). /// The CLSID string. diff --git a/Docs/CONTRIBUTING.md b/Docs/CONTRIBUTING.md index d00ba02912..de6cfe1b5c 100644 --- a/Docs/CONTRIBUTING.md +++ b/Docs/CONTRIBUTING.md @@ -55,6 +55,8 @@ FieldWorks builds can be significantly slowed by Windows Defender real-time scan This adds exclusions for build outputs, NuGet caches and development tools. Use `-DryRun` to preview changes without applying them. +If you also work in sibling repos in the same parent folder (e.g. PanGloss, motif, foma-rs), run `..\Setup-DefenderExclusions.ps1` instead (one level up) — it covers this repo plus the Rust toolchain (`.cargo`/`.rustup`) and CMake/Rust process exclusions the FieldWorks-only script doesn't need. + ### 2. Clone the Repository Clone the FieldWorks repository using HTTPS or SSH: diff --git a/Docs/superpowers/specs/2026-07-31-retired-avalonia-lesson-cards-design.md b/Docs/superpowers/specs/2026-07-31-retired-avalonia-lesson-cards-design.md deleted file mode 100644 index 8f5b375332..0000000000 --- a/Docs/superpowers/specs/2026-07-31-retired-avalonia-lesson-cards-design.md +++ /dev/null @@ -1,75 +0,0 @@ -# Retired Avalonia lesson cards - -## Purpose - -Preserve verified knowledge from retired Avalonia follow-up work without preserving its code, treating its implementation choices as current requirements, or turning it into an autonomous roadmap. - -The immediate sources are PRs #965, #966, and #967, the removal and retirement work in PR #964, and the corresponding historical commits and OpenSpec material. - -## Repository artifacts - -Create a repository-wide lesson library under `Docs/lessons/`: - -- `README.md`: the repository-wide index of lesson areas. -- `TEMPLATE.md`: the shared structure and human-review fields for every lesson area. -- `avalonia-migration/README.md`: a capability-oriented index for this migration. -- `avalonia-migration/interlinear-analysis.md`: lessons from retired PR #965. -- `avalonia-migration/rule-formula-editors.md`: lessons from retired PR #966. -- `avalonia-migration/browse-table-activation.md`: lessons from retired PR #967 and the later removal of the dormant browse implementation. -- `avalonia-migration/migration-pivot.md`: cross-cutting lessons from PR #964's scope correction and retirement work. - -Each card records status, sources, human ownership, the question tested, observations, retired approaches, no more than five durable lessons, evidence required next time, its decision boundary, and explicit conclusions that must not be inferred. - -Cards must be capability-first and code-free. Historical type names may appear only in source citations when needed for archaeology. - -## Discovery - -Future humans and agents must be able to find the cards without knowing an old PR number or branch name. - -Discovery paths are: - -1. `Docs/lessons/README.md`, which lets future lesson areas sit alongside Avalonia migration rather than treating one migration as the permanent top-level category. -2. `Docs/lessons/avalonia-migration/README.md`, indexed by problem and capability vocabulary. -3. One general link from the root `AGENTS.md` to `Docs/lessons/README.md`; repository guidance must not encode topic-specific lesson routing. -4. Avalonia migration skills linking directly to the Avalonia lesson index, framed as historical constraints rather than implementation authority. -5. A concise link from PR #964's main description, with expanded context in its existing sticky provenance comment. - -## Git and pull-request workflow - -The lesson framework and migration-skill references land directly on `phase1-base` as part of PR #964. This keeps the new skills and the lessons they depend on in one review and one merge boundary. The temporary `document-retired-avalonia-lessons` branch is not published as a separate PR. - -After the lesson commits are pushed to PR #964: - -1. Update PR #964's main description with a short lesson-index reference. -2. Update its existing sticky provenance comment in place; do not create another provenance comment. -3. Close PRs #965, #966, and #967 as superseded, linking PR #964 and the relevant lesson cards. -4. Leave the three remote branches intact as temporary archaeological references. - -No product code, old tests, archived task lists, or branch commits are copied into this branch. - -## Jira boundary - -Lesson cards are durable institutional memory. Jira issues are execution records created only after a human approves a product outcome or a bounded discovery spike. - -The lesson-card PR does not create implementation stories for the three retired PRs. A future Jira issue may cite a lesson card for constraints and evidence, but the issue must independently state its desired outcome and must not treat the historical implementation as authorized. - -## Validation - -Before publishing: - -- Check every source commit and PR reference. -- Search the current tree to avoid claiming retired types or routes are present. -- Ensure each card distinguishes observation, rejected approach, durable lesson, and unresolved hypothesis. -- Ensure no card contains copied source code or an implementation checklist. -- Verify all repository links resolve. -- Inspect the final diff for scope and encoding damage. -- Use repository build/test scripts only if a changed validation surface requires them; documentation-only changes do not require a product build. - -## Non-goals - -- Do not restore, rebase, cherry-pick, or rewrite code from PRs #965-#967. -- Do not delete their branches. -- Do not endorse their UI architecture, class layout, activation scope, or completion claims. -- Do not recreate the removed OpenSpec changes or their task checklists. -- Do not create Jira implementation work without a separate human product decision. -- Do not use cards as a substitute for current-tree discovery, legacy characterization, domain-owner decisions, or real product validation. diff --git a/Lib/src/Converter/Convertlib/Convert.cs b/Lib/src/Converter/Convertlib/Convert.cs index 5fec878955..5027731c65 100644 --- a/Lib/src/Converter/Convertlib/Convert.cs +++ b/Lib/src/Converter/Convertlib/Convert.cs @@ -1132,4 +1132,4 @@ private void ProcessModel(XmlTextReader modFile) modFile.Close(); } } -} \ No newline at end of file +} diff --git a/Lib/src/FormLanguageSwitch/FormLanguageSwitch.cs b/Lib/src/FormLanguageSwitch/FormLanguageSwitch.cs index bdd0d58733..378a0207b4 100644 --- a/Lib/src/FormLanguageSwitch/FormLanguageSwitch.cs +++ b/Lib/src/FormLanguageSwitch/FormLanguageSwitch.cs @@ -552,4 +552,4 @@ private void ReloadItems(string controlName, IList list, int itemsNumber, System /// private System.Globalization.CultureInfo m_cultureInfo; } -} \ No newline at end of file +} diff --git a/Src/CacheLight/MetaDataCache.cs b/Src/CacheLight/MetaDataCache.cs index 2915d8d6c1..3eae87e935 100644 --- a/Src/CacheLight/MetaDataCache.cs +++ b/Src/CacheLight/MetaDataCache.cs @@ -988,4 +988,4 @@ public MetaFieldRec() m_fieldXml = null; } } -} \ No newline at end of file +} diff --git a/Src/Common/Controls/DetailControls/DataTree.cs b/Src/Common/Controls/DetailControls/DataTree.cs index e7facb4100..69cf22f3d4 100644 --- a/Src/Common/Controls/DetailControls/DataTree.cs +++ b/Src/Common/Controls/DetailControls/DataTree.cs @@ -426,7 +426,7 @@ private void AdjustSliceSplitPosition(Slice otherSlice) protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); - // Skip O(N) splitter adjustment during bulk slice construction — + // Skip O(N) splitter adjustment during bulk slice construction -- // HandleLayout1 will set correct widths + positions after construction. if (ConstructingSlices) return; @@ -3698,13 +3698,13 @@ private int FindFirstPotentiallyVisibleSlice(int clipTop) int sliceBottom = slice.Top + slice.Height; if (sliceBottom <= clipTop) { - // Slice ends at or before the clip top — entirely above viewport. + // Slice ends at or before the clip top -- entirely above viewport. result = mid + 1; lo = mid + 1; } else { - // Slice extends below clip top — could be visible. + // Slice extends below clip top -- could be visible. hi = mid - 1; } } @@ -4298,14 +4298,7 @@ protected override void OnPaint(PaintEventArgs e) protected override void WndProc(ref Message m) { base.WndProc(ref m); - // After any scroll input (scrollbar drag, mouse wheel, horizontal wheel), - // force the parent background to repaint so separator lines are redrawn at - // correct positions. Without this, Windows bitblts stale line pixels - // from the old scroll position and only repaints the newly-exposed strip. - // Invalidate(false) skips child invalidation — slice HWNDs repaint - // themselves — so only the gap areas between slices are redrawn. - // Update() forces synchronous processing so stale lines don't accumulate - // across multiple scroll events before the low-priority WM_PAINT fires. + // Without this, Windows bitblts stale separator-line pixels from the old scroll position; Invalidate(false)+Update() forces a synchronous repaint of just the inter-slice gaps before WM_PAINT coalesces. const int WM_VSCROLL = 0x0115; const int WM_HSCROLL = 0x0114; const int WM_MOUSEWHEEL = 0x020A; diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeRenderTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeRenderTests.cs index 10fb9895f3..090a1b6d21 100644 --- a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeRenderTests.cs +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeRenderTests.cs @@ -114,8 +114,8 @@ private void CreateSimpleEntry() /// /// Creates a lex entry with triple-nested senses (depth 3, breadth 2). - /// 2 senses × 2 subsenses × 2 sub-sub-senses = 14 total senses (2+4+8). - /// This is the "slow" scenario — realistic deeply nested entry. + /// 2 senses x 2 subsenses x 2 sub-sub-senses = 14 total senses (2+4+8). + /// This is the "slow" scenario -- realistic deeply nested entry. /// private void CreateDeepEntry() { @@ -255,7 +255,7 @@ private void EnrichEntry(ILexEntry entry, string testName) /// /// Creates a minimal lex entry with a single sense and no optional fields. - /// Exercises the "collapsed" view — bare minimum rendering path. + /// Exercises the "collapsed" view -- bare minimum rendering path. /// private void CreateCollapsedEntry() { @@ -271,7 +271,7 @@ private void CreateCollapsedEntry() m_entry.CitationForm.VernacularDefaultWritingSystem = MakeRenderString( $"CitationForm - {testName}", Cache.DefaultVernWs); - // Single sense — minimal entry, no enrichment + // Single sense -- minimal entry, no enrichment var senseFactory = Cache.ServiceLocator.GetInstance(); var sense = senseFactory.Create(); m_entry.SensesOS.Add(sense); @@ -281,7 +281,7 @@ private void CreateCollapsedEntry() /// /// Creates a fully enriched lex entry with all available optional fields populated. /// 4 senses with all sense-level fields, plus full entry enrichment. - /// Exercises the "expanded" view — maximum slice count for fields we can safely render. + /// Exercises the "expanded" view -- maximum slice count for fields we can safely render. /// private void CreateExpandedEntry() { @@ -462,7 +462,7 @@ public async Task DataTreeRender_Simple() /// /// Verifies the full DataTree rendering for a triple-nested lex entry. - /// 2 senses × 2 subsenses × 2 sub-sub-senses = 14 total senses. + /// 2 senses x 2 subsenses x 2 sub-sub-senses = 14 total senses. /// This is the "slow" scenario for realistic deep nesting. /// [Test] @@ -661,7 +661,7 @@ public async Task DataTreeRender_Extreme() /// /// Verifies the DataTree rendering for a minimal entry with a single sense. - /// Exercises the bare minimum rendering path — collapsed view. + /// Exercises the bare minimum rendering path -- collapsed view. /// [Test] public async Task DataTreeRender_Collapsed() @@ -802,7 +802,7 @@ public void DataTreeTiming(int depth, int breadth, string label) /// /// Measures paint/capture time for the extreme scenario. - /// Exercises the full OnPaint → HandlePaintLinesBetweenSlices pipeline + /// Exercises the full OnPaint -> HandlePaintLinesBetweenSlices pipeline /// via DrawToBitmap. This provides a baseline for paint optimizations /// (clip-rect culling, double-buffering). /// @@ -821,7 +821,7 @@ public void DataTreeTiming_PaintPerformance() Assert.That(warmup, Is.Not.Null, "Warm-up capture should succeed"); warmup.Dispose(); - // Timed capture: DrawToBitmap → OnPaint → HandlePaintLinesBetweenSlices + // Timed capture: DrawToBitmap -> OnPaint -> HandlePaintLinesBetweenSlices var sw = System.Diagnostics.Stopwatch.StartNew(); var bitmap = harness.CaptureCompositeBitmap(); sw.Stop(); @@ -886,7 +886,7 @@ public void DataTreeOpt_WidthStabilityAfterLayout() for (int i = 0; i < dt.Slices.Count; i++) initialWidths[i] = ((Slice)dt.Slices[i]).Width; - // Force a second paint/layout pass — widths should remain identical + // Force a second paint/layout pass -- widths should remain identical var bitmap = harness.CaptureCompositeBitmap(); Assert.That(bitmap, Is.Not.Null, "Second paint should succeed"); bitmap.Dispose(); @@ -1100,7 +1100,7 @@ public void DataTreeOpt_SequentialPaintsProduceIdenticalOutput() Assert.That(capture2.Height, Is.EqualTo(capture1.Height), "Bitmap heights should match"); - // Compare pixel-by-pixel — paint must be deterministic + // Compare pixel-by-pixel -- paint must be deterministic int mismatchCount = 0; for (int y = 0; y < capture1.Height; y++) { @@ -1220,7 +1220,7 @@ public void DataTreeOpt_FullLayoutAndPaintPathPositionsAgree() var dt = harness.DataTree; - // Record positions after full layout (set by OnLayout → HandleLayout1(fFull=true)) + // Record positions after full layout (set by OnLayout -> HandleLayout1(fFull=true)) var fullLayoutPositions = new int[dt.Slices.Count]; var fullLayoutHeights = new int[dt.Slices.Count]; for (int i = 0; i < dt.Slices.Count; i++) @@ -1253,7 +1253,7 @@ public void DataTreeOpt_FullLayoutAndPaintPathPositionsAgree() /// /// Verifies that AutoScrollPosition does not drift across multiple paint passes. /// The paint path adjusts scroll position when slices above the viewport change - /// height (e.g., DummyObjectSlice → real slice). After initial convergence, + /// height (e.g., DummyObjectSlice -> real slice). After initial convergence, /// scroll position must be stable. /// Failure mode: binary search skips the desiredScrollPosition adjustment for /// above-viewport slices, causing scroll jumps. @@ -1270,7 +1270,7 @@ public void DataTreeOpt_ScrollPositionStableAcrossPaints() var dt = harness.DataTree; - // Warm up — first paint triggers layout convergence + // Warm up -- first paint triggers layout convergence var warmup = harness.CaptureCompositeBitmap(); Assert.That(warmup, Is.Not.Null); warmup.Dispose(); @@ -1458,7 +1458,7 @@ public void DataTreeOpt_NoDummySlicesInViewportAfterPaint() /// Verifies that slice heights are stable after layout convergence. /// A binary search for the first visible slice depends on accumulated /// heights being deterministic: if heights change between paint calls - /// (e.g., because DummyObjectSlice→real changes weren't finalized), + /// (e.g., because DummyObjectSlice->real changes weren't finalized), /// the binary search would compute wrong yTop offsets and skip or /// double-show slices. /// After the initial full-layout pass, heights should never change @@ -1475,7 +1475,7 @@ public void DataTreeOpt_SliceHeightsStableAfterConvergence() harness.PopulateSlices(1024, 800, false); Assert.That(harness.SliceCount, Is.GreaterThan(0), "Should have slices"); - // Force full convergence — first paint makes everything real + // Force full convergence -- first paint makes everything real var warmup = harness.CaptureCompositeBitmap(); Assert.That(warmup, Is.Not.Null); warmup.Dispose(); diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeReshowTimingTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeReshowTimingTests.cs index e84d471c43..94d72746cb 100644 --- a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeReshowTimingTests.cs +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeReshowTimingTests.cs @@ -15,7 +15,7 @@ namespace SIL.FieldWorks.Common.Framework.DetailControls { /// - /// Measures the legacy DataTree's re-show cost — a second ShowObject on the live tree after + /// Measures the legacy DataTree's re-show cost -- a second ShowObject on the live tree after /// a model edit, which exercises the slice-reuse (ObjSeqHashMap) refresh path RecordEditView drives /// on record navigation and refresh. Numbers accumulate into the same /// Output/RenderBenchmarks/datatree-timings.json artifact the entry-open baselines use. diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs index 4c3f3a2629..549f31a41f 100644 --- a/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs @@ -99,8 +99,8 @@ public override void TestTearDown() /// DoNotRefresh window. Callers (like SwapValues) must explicitly set /// RefreshListNeeded=true before releasing DoNotRefresh. /// - /// RED phase: comment out RefreshListNeeded=true → test FAILS (stale slices). - /// GREEN phase: RefreshListNeeded=true present → test PASSES. + /// Without that RefreshListNeeded=true call, the bibliography slice + /// stays stale after the DoNotRefresh window closes. /// [Test] public void DoNotRefresh_SlicesMustReflectChanges_AfterRelease_LT22414() @@ -128,7 +128,7 @@ public void DoNotRefresh_SlicesMustReflectChanges_AfterRelease_LT22414() m_dtree.DoNotRefresh = false; - // Assert: after refresh, bibliography slice should be gone (no data → ifdata hides it) + // Assert: after refresh, bibliography slice should be gone (no data -> ifdata hides it) Assert.That(m_dtree.Controls.Count, Is.EqualTo(1), "LT-22414: After DoNotRefresh=false, slices should reflect data changes. " + "Bibliography has no data so ifdata should hide it. " + @@ -158,7 +158,7 @@ public void DoNotRefresh_WithoutRefreshListNeeded_DoesNotRefresh_LT22414_BugDemo // Intentionally NOT setting RefreshListNeeded (simulates buggy SwapValues) m_dtree.DoNotRefresh = false; - // Assert: slices are STALE — bibliography still visible despite no data + // Assert: slices are STALE -- bibliography still visible despite no data Assert.That(m_dtree.Controls.Count, Is.EqualTo(2), "Without RefreshListNeeded, DoNotRefresh=false does not trigger refresh; " + "slices remain stale (bibliography still visible despite no data)."); diff --git a/Src/Common/Controls/DetailControls/ObjSeqHashMap.cs b/Src/Common/Controls/DetailControls/ObjSeqHashMap.cs index b878bfddd7..22620ac6dd 100644 --- a/Src/Common/Controls/DetailControls/ObjSeqHashMap.cs +++ b/Src/Common/Controls/DetailControls/ObjSeqHashMap.cs @@ -188,4 +188,4 @@ bool IEqualityComparer.Equals(object xArg, object yArg) return true; } } -} \ No newline at end of file +} diff --git a/Src/Common/Controls/DetailControls/Slice.cs b/Src/Common/Controls/DetailControls/Slice.cs index 79bdb223e3..17137af32b 100644 --- a/Src/Common/Controls/DetailControls/Slice.cs +++ b/Src/Common/Controls/DetailControls/Slice.cs @@ -91,7 +91,7 @@ public class Slice : UserControl, IxCoreColleague protected bool m_widthHasBeenSetByDataTree = false; protected IPersistenceProvider m_persistenceProvider; - // Cached XML configuration attributes — parsed once from ConfigurationNode on first access. + // Cached XML configuration attributes -- parsed once from ConfigurationNode on first access. // Invalidated when ConfigurationNode is re-set (rare). private bool? m_cachedIsHeader; private bool? m_cachedSkipSpacerLine; diff --git a/Src/Common/Controls/XMLViews/XMLViewsTests/TestXmlBrowseView.cs b/Src/Common/Controls/XMLViews/XMLViewsTests/TestXmlBrowseView.cs index 25be18618f..4f2001cb50 100644 --- a/Src/Common/Controls/XMLViews/XMLViewsTests/TestXmlBrowseView.cs +++ b/Src/Common/Controls/XMLViews/XMLViewsTests/TestXmlBrowseView.cs @@ -194,7 +194,7 @@ public void HiddenSentinel_AllColumnsVisible_AllowsAutoAddOfNewCommonColumns() bool hasHiddenTracking = hiddenNodes.Count > 0; Assert.That(hasHiddenTracking, Is.True, "Sentinel should enable hidden tracking"); - // Collect hidden labels — sentinel has no label, so hiddenLabels should be empty + // Collect hidden labels -- sentinel has no label, so hiddenLabels should be empty var hiddenLabels = new HashSet(); foreach (XmlNode hidden in hiddenNodes) { diff --git a/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs b/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs index 613317e2a5..f8669ba2c4 100644 --- a/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs +++ b/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs @@ -249,7 +249,7 @@ public XmlBrowseViewBaseVc(XmlNode xnSpec, int fakeFlid, XmlBrowseViewBase xbv) if (!hasHiddenTracking) { // Bootstrap: no hidden tracking yet (pre-upgrade save). - // Don't auto-add anything — all missing columns are presumed + // Don't auto-add anything -- all missing columns are presumed // deliberately removed. They'll be properly tracked on next save. continue; } diff --git a/Src/Common/Framework/MainWindowDelegate.cs b/Src/Common/Framework/MainWindowDelegate.cs index 0c25dfe186..22ac64f4b3 100644 --- a/Src/Common/Framework/MainWindowDelegate.cs +++ b/Src/Common/Framework/MainWindowDelegate.cs @@ -970,4 +970,4 @@ private static string MakeLauncherPath(string directory, string projectName, return Path.Combine(directory, projectName + tail + pathExtension); } } -} \ No newline at end of file +} diff --git a/Src/Common/FwAvalonia/AvaloniaDialogHost.cs b/Src/Common/FwAvalonia/AvaloniaDialogHost.cs index 18bf818f66..3ec3564bd4 100644 --- a/Src/Common/FwAvalonia/AvaloniaDialogHost.cs +++ b/Src/Common/FwAvalonia/AvaloniaDialogHost.cs @@ -13,7 +13,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia { /// /// Reusable host that shows an Avalonia dialog body (a UserControl) inside a WinForms-owned - /// modal during coexistence — the turn-key piece for the MVVM dialog stack. Because + /// modal during coexistence -- the turn-key piece for the MVVM dialog stack. Because /// Avalonia modal windows are not supported while WinForms owns the message loop, /// the dialog body is hosted in a WinForms modal window owned by the caller's form; the view-model /// closes it by raising (no windowing in the VM). @@ -27,11 +27,11 @@ public static class AvaloniaDialogHost /// result (true = OK, false = Cancel), or null if the window was closed without an OK/Cancel. /// /// The optional parameters extend the fixed-size default WITHOUT changing it for existing callers: - /// * — when true the modal gets a sizable border and a minimum size + /// * -- when true the modal gets a sizable border and a minimum size /// (defaulting to the initial / unless /// / are supplied). Default false keeps the /// legacy behavior. - /// * / — an optional + /// * / -- an optional /// size-persistence hook (mirrors the label-column-width persistence pattern: the caller owns the /// remembered value, keyed by dialog identity, so a resized dialog reopens at its last size). The /// get-hook (when it returns a value) seeds the initial client size in place of @@ -82,7 +82,7 @@ public static class AvaloniaDialogHost ShowInTaskbar = false }) { - // Border / min-size / initial (possibly remembered) size — extracted so it is unit-testable + // Border / min-size / initial (possibly remembered) size -- extracted so it is unit-testable // without spinning a real modal window. ApplySizing(form, width, height, resizable, minWidth, minHeight, getRememberedSize); @@ -145,7 +145,7 @@ public static class AvaloniaDialogHost /// /// Picks the owner to show a modal dialog over: (the form truly - /// topmost/focused right now — ) when there is one, else the caller- + /// topmost/focused right now -- ) when there is one, else the caller- /// supplied . Factored out of so the nested-modal /// owner-chain decision is unit-testable without spinning a real modal window; see the call site for /// why a stale breaks pointer input on a dialog opened from another dialog. @@ -186,9 +186,9 @@ public static void DisposeDialogResources(AvControl dialogBody, IDialogViewModel /// Applies the border style, minimum size and initial (possibly remembered) client size to the hosting /// modal . Factored out of so the sizing/persistence /// contract is unit-testable without spinning a real modal window: - /// * false → and no min-size + /// * false -> and no min-size /// (the legacy default; //get-hook ignored); - /// * true → with a min client size + /// * true -> with a min client size /// (/, defaulting to the initial /// /), and the get-hook (when it returns a value) /// seeds the initial client size in place of /. @@ -239,7 +239,7 @@ public static void ApplySizing( /// /// Focuses the first keyboard-focusable INPUT inside so a dialog opens /// with the caret in its first field (legacy WinForms parity). Buttons (OK/Cancel/Help) are - /// never the initial focus — initial focus belongs to an input, and Enter/Escape already activate the + /// never the initial focus -- initial focus belongs to an input, and Enter/Escape already activate the /// default/cancel buttons. Returns the control it focused, or null if none qualifies. Factored out /// (like / ) so the selection contract is unit-testable /// headlessly without spinning a real WinForms-hosted modal window; invokes it @@ -268,7 +268,7 @@ int EffectiveTabIndex(AvControl c) return max; } - // The first focusable INPUT in tab order — never a command button. If a picker-driven dialog + // The first focusable INPUT in tab order -- never a command button. If a picker-driven dialog // exposes no focusable field (the owned FwOptionChooser is deliberately Focusable=false and // handles keys directly), focus nothing rather than landing on OK, where Enter would accept the // dialog. So this is a no-op for picker dialogs (no regression) and focuses the first text field diff --git a/Src/Common/FwAvalonia/CompactDialogStyles.cs b/Src/Common/FwAvalonia/CompactDialogStyles.cs index 597f732fea..071278f36f 100644 --- a/Src/Common/FwAvalonia/CompactDialogStyles.cs +++ b/Src/Common/FwAvalonia/CompactDialogStyles.cs @@ -12,10 +12,10 @@ namespace SIL.FieldWorks.Common.FwAvalonia { /// - /// Compact density for Avalonia dialogs — the design baseline so migrated dialogs match the legacy + /// Compact density for Avalonia dialogs -- the design baseline so migrated dialogs match the legacy /// WinForms dialog density (small font, tight padding, no Fluent min-height floors) rather than the /// roomy Fluent defaults. Applied once by to every hosted dialog - /// body, so EVERY dialog shown through the host inherits it automatically — new dialogs need no + /// body, so EVERY dialog shown through the host inherits it automatically -- new dialogs need no /// per-dialog density work. Scoped to the dialog's control subtree (added to its Styles), so /// it never affects the detail/table views, which own their own density (). /// @@ -70,7 +70,7 @@ private static IEnumerable Build() // NOTE: the deterministic CheckBox style (FwCheckBoxStyle) is NOT added here. It is applied once, // to every dialog body, by DialogThemeBootstrap.Apply (called from each dialog ctor in BOTH the // runtime host and the headless dialog tests), so it reaches the headless path that never runs this - // runtime chokepoint — and stays a single application rather than a double one. + // runtime chokepoint -- and stays a single application rather than a double one. yield return new Style(s => s.OfType()) { diff --git a/Src/Common/FwAvalonia/Detail/DataTree.cs b/Src/Common/FwAvalonia/Detail/DataTree.cs index 2ab8b3b2d9..dbda5c5037 100644 --- a/Src/Common/FwAvalonia/Detail/DataTree.cs +++ b/Src/Common/FwAvalonia/Detail/DataTree.cs @@ -24,7 +24,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// /// Editing: when an is supplied, /// field editors stage writes through it (which opens the fenced LCModel session on the first - /// edit) and the session auto-commits on focus loss — the legacy save-as-you-go behavior, one + /// edit) and the session auto-commits on focus loss -- the legacy save-as-you-go behavior, one /// undo step per field, no Save/Cancel buttons. Validation failures show inline and block the /// commit; Escape rolls the session back. Without a context the view is read-only display. /// @@ -33,9 +33,9 @@ public sealed class DataTree : UserControl private readonly IDetailEditContext _editContext; private readonly Action _writingSystemFocused; private readonly List> _rowControls = new List>(); - // Collapsible section toggle buttons, keyed by field stable id — captured at build time so - // WireCollapsibleHeaders finds the toggle directly (the header is now wrapped in the field-menu - // gutter, and the kebab is also a Button, so a tree search would be ambiguous). + // Collapsible section toggles, keyed by field stable id, captured at build + // time: WireCollapsibleHeaders finds them since the header now wraps in + // the field-menu gutter, where the kebab is also a Button. private readonly Dictionary _collapsibleToggles = new Dictionary(); private readonly Action _labelColumnWidthChanged; private TextBlock _validationBlock; @@ -50,10 +50,10 @@ public sealed class DataTree : UserControl /// Optional expansion-state hooks (11.8): supplies the /// persisted state per header stable id (overriding the layout's initial state) and /// records toggles, so collapse state survives record - /// switches/re-shows — the legacy PropertyTable expansion persistence. + /// switches/re-shows -- the legacy PropertyTable expansion persistence. /// / persist /// the splitter position the same way (11.15): the host owns the remembered width so it - /// survives re-shows WITHOUT a process-global field — each host/window keeps its own. + /// survives re-shows WITHOUT a process-global field -- each host/window keeps its own. /// public DataTree(DetailModel model, IDetailEditContext editContext = null, Action writingSystemFocused = null, @@ -80,9 +80,9 @@ public DataTree(DetailModel model, IDetailEditContext editContext = null, AutomationProperties.SetAutomationId(this, "DataTree"); AutomationProperties.SetName(this, FwAvaloniaStrings.DetailAreaName); - // WinForms-density font baseline for the detail view, applied to this view's own control - // subtree so it renders in both the runtime host and the headless tests. The view stays FLAT with - // subtle field separators (FwAvaloniaDensity) — this only drops the Fluent ~14px default font. + // WinForms-density font baseline for the detail view, applied to this view's + // own subtree so runtime and headless hosts render it the same. Stays FLAT + // (FwAvaloniaDensity); only drops the Fluent ~14px font. FwSurfaceStyles.Apply(this); // Viewing parity (11.15): a draggable splitter divides the label and value columns like @@ -142,7 +142,7 @@ public DataTree(DetailModel model, IDetailEditContext editContext = null, // the ScrollViewer is arranged against the full viewport extent, while a grid inside a StackPanel // is arranged against its own desired height; those two arrange contexts round the grid's Auto // content rows to whole-pixel heights 1px differently, so wrapping only in the editable state - // would shift every row by 1px on the edit toggle — a visible rhythm mismatch. + // would shift every row by 1px on the edit toggle -- a visible rhythm mismatch. // Wrapping identically in both states keeps the rows pixel-for-pixel stable across the toggle; the // validation footer is the only edit-only child added. var panel = new StackPanel(); @@ -161,12 +161,12 @@ public DataTree(DetailModel model, IDetailEditContext editContext = null, Content = scroller; // Screen-local command shortcuts: - // Enter commits (validation-gated), Escape cancels — handled at the view so they work + // Enter commits (validation-gated), Escape cancels -- handled at the view so they work // from any field editor. AddHandler(Avalonia.Input.InputElement.KeyDownEvent, OnViewKeyDown, Avalonia.Interactivity.RoutingStrategies.Bubble); - // Auto-save (14.4): legacy slices commit as the user moves on — any editor losing focus + // Auto-save (14.4): legacy slices commit as the user moves on -- any editor losing focus // while a session is open commits it (validation-gated; one undo step per field). AddHandler(Avalonia.Input.InputElement.LostFocusEvent, (s, e) => { @@ -201,7 +201,7 @@ private void OnViewKeyDown(object sender, Avalonia.Input.KeyEventArgs e) /// public event EventHandler EditCompleted; - // 14.4: no Save/Cancel buttons — the legacy view saves as you go. The footer carries only the + // 14.4: no Save/Cancel buttons -- the legacy view saves as you go. The footer carries only the // inline validation messages (a failed autosave is never silent). private Control CreateEditFooter() { @@ -283,13 +283,13 @@ private void AddField(Grid grid, int row, DetailField field) AutomationProperties.SetAutomationId(header, automationId); AutomationProperties.SetName(header, field.Label ?? string.Empty); - // 13.3/13.5: the section menu/hotlinks open from the hover "⋮" field-menu button (which + // 13.3/13.5: the section menu/hotlinks open from the hover "..." field-menu button (which // replaced right-click), in a thin gutter to the left of the header. var headerCell = WrapWithFieldMenu(header, field, automationId, out var headerKebab); // Discoverability parity (legacy SummaryCommandControl): a section header with hotlinks // shows its commands as an ALWAYS-VISIBLE inline command-link strip directly beneath the - // header — the kebab alone is a hover-gated discoverability regression. The strip raises + // header -- the kebab alone is a hover-gated discoverability regression. The strip raises // the SAME hotlinks request the kebab does (DetailMenuKind.Hotlinks), so it dispatches // through the existing host bridge identically. var hotlinkStrip = CreateHotlinkStrip(field, automationId, indent); @@ -349,7 +349,7 @@ private void AddField(Grid grid, int row, DetailField field) AutomationProperties.SetAutomationId(labelBlock, automationId + ".Label"); AutomationProperties.SetName(labelBlock, field.Label ?? field.Field ?? string.Empty); ToolTip.SetTip(labelBlock, field.Label ?? field.Field); // 11.17: legacy label tooltips - // 13.3: the field's slice menu opens from the hover "⋮" button in the left gutter (which + // 13.3: the field's slice menu opens from the hover "..." button in the left gutter (which // replaced right-click on the label). var labelCell = WrapWithFieldMenu(labelBlock, field, automationId, out var labelKebab); Grid.SetRow(labelCell, row * 2); @@ -364,9 +364,9 @@ private void AddField(Grid grid, int row, DetailField field) grid.Children.Add(editor); _rowControls[row].Add(editor); - // Hover-reveal affordances: the WHOLE row (label cell + editor) is the hover/focus surface for its - // secondary affordances — the field-options "⋮" and any editor affordances (chooser gear, - // vector bars/launcher). Both attach against the same sources so they reveal together. + // Hover-reveal: the WHOLE row (label cell + editor) is the hover/focus + // surface for the field-options "..." and any editor affordance (chooser + // gear, vector bars/launcher); both reveal together. var hoverSources = new Control[] { labelCell, editor }; if (labelKebab != null) HoverReveal.Attach(hoverSources, new[] { labelKebab }); @@ -374,18 +374,13 @@ private void AddField(Grid grid, int row, DetailField field) HoverReveal.Attach(hoverSources, provider.HoverAffordances); } - // The width of the left gutter that holds the per-row field-options "⋮" button. Reserved on + // The width of the left gutter that holds the per-row field-options "..." button. Reserved on // every row (when a host bridge is present) so labels align whether or not a row has a menu. private const double FieldMenuGutterWidth = 18; - // Section 13: each field/header row surfaces its legacy slice menu (or the section's hotlinks when - // only those exist) through the host bridge — the same menu ids legacy DTMenuHandler resolves from - // the layout. The affordance is a hover/keyboard-focus-revealed "⋮" button in a thin left gutter - // (it REPLACED right-click): clicking or pressing Enter/Space on it raises the SAME DetailMenuRequest - // as right-click, anchored at the icon. Returns wrapped with that - // gutter for the row, and reports the revealed kebab (or null) so the caller folds it into the row's - // hover group. With no host bridge the content is returned unwrapped, so non-product views - // (previews/tests with no menu callback) are unchanged. + // The hover/keyboard-revealed "..." kebab replaces right-click, raising the + // same DetailMenuRequest; with no host bridge the content returns unwrapped + // so preview/test hosts stay unchanged. private Control WrapWithFieldMenu(Control inner, DetailField field, string automationId, out Control kebab) { @@ -416,7 +411,7 @@ private Control WrapWithFieldMenu(Control inner, DetailField field, string autom // affordance is fully keyboard-operable once Tab focus reveals it. button.Click += (s, e) => { - // Anchor the menu to the icon (drop from its bottom-left) — the screen-coordinate + // Anchor the menu to the icon (drop from its bottom-left) -- the screen-coordinate // contract the host's DetailMenuRequest handler positions the xCore menu by. var screen = button.PointToScreen(new Point(0, button.Bounds.Height)); _menuRequested(new DetailMenuRequest(field, kind, screen.X, screen.Y)); @@ -436,15 +431,9 @@ private Control WrapWithFieldMenu(Control inner, DetailField field, string autom private static readonly IBrush HotlinkBrush = new SolidColorBrush(Color.FromRgb(0x00, 0x66, 0xCC)); - // Discoverability parity: the always-visible inline hotlinks command strip beneath a section - // header (legacy SummaryCommandControl). The host bridge resolves the hotlinks MENU id at click - // time and exposes no per-command labels to this layer, so we render a SINGLE always-visible flat - // command link (not per-command links) that raises the SAME DetailMenuRequest(kind=Hotlinks) the - // kebab raises — it dispatches through the existing host bridge identically, and the host's - // hotlinks handler then surfaces the individual commands. Returns null when the header has no - // hotlinks or no host bridge is wired (previews/tests with no menu callback), so those hosts - // are unchanged. The strip is NOT hover-gated — it stays fully visible and clickable at rest, - // which is the whole point versus the kebab. + // Renders a single always-visible flat command link (not per-command) + // because the host bridge exposes no per-command labels, and stays + // un-hover-gated -- unlike the kebab -- since visibility is the point. private Control CreateHotlinkStrip(DetailField field, string automationId, Thickness indent) { if (_menuRequested == null || string.IsNullOrEmpty(field.HotlinksId)) @@ -479,7 +468,7 @@ private Control CreateHotlinkStrip(DetailField field, string automationId, Thick } // Viewing parity (11.x): a collapsible header owns every following row with greater indent, - // up to the next field at its own indent or shallower — collapsing hides them (nested + // up to the next field at its own indent or shallower -- collapsing hides them (nested // sections collapse with their parent), expanding restores them, and the layout's expansion // attribute supplies the initial state. // @@ -575,7 +564,7 @@ private sealed class CollapsibleHeader public bool Expanded; } - // The field→control dispatch is shared with the browse in-cell editor through + // The field->control dispatch is shared with the browse in-cell editor through // SliceFactory. The detail pane passes its full callback set (per-WS keyboard, slice // menu, link, clipboard) and routes reference-vector gesture completion to its validation-gated // OnSave (the autosave). New DetailFieldKinds are added once, in the factory. diff --git a/Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs b/Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs index f55020c3a4..a4781c0efb 100644 --- a/Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs +++ b/Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs @@ -16,7 +16,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// this, tabbing out of a field (which auto-commits, 14.4) would tear down the editor the user /// just moved into and dump focus on the floor. Capture reads the focused editor's stable /// automation id (and caret) from the outgoing view; restore finds the same id in the incoming - /// view and gives it focus — automation ids are stable per field/writing system by design, so + /// view and gives it focus -- automation ids are stable per field/writing system by design, so /// they are the right cross-rebuild identity. /// public static class DetailFocusMemory @@ -82,7 +82,7 @@ public static bool TryRestoreScroll(Control root, Memento memento) /// /// Focuses the control with the memento's automation id inside - /// (which must already be attached to a TopLevel). Returns false when no match exists — + /// (which must already be attached to a TopLevel). Returns false when no match exists -- /// e.g. the field disappeared in the re-show, or the memento had scroll-only state. /// public static bool TryRestoreFocus(Control root, Memento memento) @@ -90,7 +90,7 @@ public static bool TryRestoreFocus(Control root, Memento memento) if (root == null || string.IsNullOrEmpty(memento?.AutomationId)) return false; - // First pass: the exact stable id (the common case — the same field survived the re-show). + // First pass: the exact stable id (the common case -- the same field survived the re-show). foreach (var visual in root.GetVisualDescendants()) { if (!(visual is Control control) @@ -104,7 +104,7 @@ public static bool TryRestoreFocus(Control root, Memento memento) } // Post-ghost-commit continuity (legacy RestoreSelection): when a ghost add-prompt commits, - // the host recomposes and the new REAL editor's id differs from the "/ghost" id — the ghost + // the host recomposes and the new REAL editor's id differs from the "/ghost" id -- the ghost // id carries the OWNER's hvo and the "/ghost" marker, the successor carries the newly created // object's hvo and no marker. So the exact match above misses and focus would land on the // floor. When the captured id is a ghost id, fall back to its successor matcher so focus @@ -135,7 +135,7 @@ private static void FocusMatch(Control control, Memento memento) // Maps a "/ghost" editor automation id to a predicate that recognizes the real successor editor // the ghost commit produced. The ghost id has the shape "{node}@{ownerHvo}/ghost.{wsKey}" (the - // owner hvo because the object did not exist yet); the successor has "{node}@{newHvo}.{wsKey}" — + // owner hvo because the object did not exist yet); the successor has "{node}@{newHvo}.{wsKey}" -- // same node-stable prefix and same writing-system suffix, only the owned object's hvo (and the // "/ghost" marker) change. We therefore match on the prefix up to and including "@" plus the WS // suffix after "/ghost", tolerating the hvo difference. Returns null when the id is not a ghost id diff --git a/Src/Common/FwAvalonia/Detail/DetailMenuFlyout.cs b/Src/Common/FwAvalonia/Detail/DetailMenuFlyout.cs index 34d5bf5cea..8020d5603e 100644 --- a/Src/Common/FwAvalonia/Detail/DetailMenuFlyout.cs +++ b/Src/Common/FwAvalonia/Detail/DetailMenuFlyout.cs @@ -10,7 +10,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail { /// /// Framework-neutral context-menu item (15.1): what the host resolved from its menu system - /// (for FieldWorks, the xCore ChoiceGroup — labels, enablement, checkmarks, submenus, and an + /// (for FieldWorks, the xCore ChoiceGroup -- labels, enablement, checkmarks, submenus, and an /// execute action that dispatches through the mediator). FwAvalonia renders these natively; /// it knows nothing about xCore, preserving the engine-isolation boundary. /// @@ -45,7 +45,7 @@ private DetailMenuItem() /// /// Renders host-built trees as a native Avalonia - /// (15.1) — the same items, enablement, checkmarks, and submenus the + /// (15.1) -- the same items, enablement, checkmarks, and submenus the /// legacy WinForms adapter menu shows, rendered with native Avalonia controls. Density: every item carries the /// explicit compact padding/height of the legacy WinForms menus /// (/, diff --git a/Src/Common/FwAvalonia/Detail/DetailModel.cs b/Src/Common/FwAvalonia/Detail/DetailModel.cs index 1343ab96cc..1f98097333 100644 --- a/Src/Common/FwAvalonia/Detail/DetailModel.cs +++ b/Src/Common/FwAvalonia/Detail/DetailModel.cs @@ -34,7 +34,7 @@ public enum DetailFieldKind /// /// An editable reference vector: current items plus the possibility list's options /// (hierarchy on ), edited through - /// / — + /// / -- /// the legacy possibility-vector slice with its trailing type-ahead add slot. /// ReferenceVector, @@ -49,7 +49,7 @@ public enum DetailFieldKind Custom, /// - /// An editable multi-paragraph structured-text (StText) field — the legacy + /// An editable multi-paragraph structured-text (StText) field -- the legacy /// StTextSlice's RootSite rich editor. The row carries an ordered /// list (each a run-aware /// with a per-paragraph named style); the owned @@ -61,7 +61,7 @@ public enum DetailFieldKind StructuredText, /// - /// A literal / "lit" slice (legacy MessageSlice) — static label text rendered + /// A literal / "lit" slice (legacy MessageSlice) -- static label text rendered /// read-only in the value column (the label/message text IS the content). Carries no editable /// value and no setter. /// @@ -72,7 +72,7 @@ public enum DetailFieldKind /// The kind of an embedded object (ORC) a run carries, classified LCModel-free from the /// FIRST character of (the value the xWorks adapter projects /// from the TsString's ktptObjData). The numeric tags mirror - /// SIL.LCModel.Core.KernelInterfaces.FwObjDataTypes — the view layer is LCModel-free, so it + /// SIL.LCModel.Core.KernelInterfaces.FwObjDataTypes -- the view layer is LCModel-free, so it /// reads the opaque ObjectData string the adapter produced rather than the enum itself. /// public enum DetailOrcKind @@ -133,7 +133,7 @@ public DetailTextRun(string text, string writingSystemTag = null, string namedSt internal const char ObjDataFootnoteOwn = (char)5; internal const char ObjDataFootnoteName = (char)3; - /// Whether this run carries an embedded object (ORC) — any non-empty ObjectData. + /// Whether this run carries an embedded object (ORC) -- any non-empty ObjectData. public bool IsOrc => !string.IsNullOrEmpty(ObjectData); /// @@ -181,13 +181,9 @@ public DetailRichTextValue(string plainText, IReadOnlyList runs, RichXml = richXml; RequiresRichEditor = requiresRichEditor; LossyProperties = lossyProperties; - // An embedded object (ORC) does not force the value read-only — a link ORC is fully - // editable (insert/edit/delete) and ANY ORC run is deletable, so the run-replay path rebuilds - // the value with its ObjectData preserved. A value is held read-only ONLY when an edit would - // SILENTLY DROP data: a run carrying a TsString property the DetailTextRun model does not - // round-trip (colour, offset, superscript — flagged lossyProperties) since the first plain-text - // edit skips the lossless RichXml fast-path. The explicit canEditRichText flag still lets a - // caller force read-only for a reason unrelated to runs (e.g. a voice/audio alternative). + // A value goes read-only only when an edit would silently drop data -- a run + // with a non-round-trippable TsString property (lossyProperties), not for + // carrying an ORC, fully editable via run-replay. CanEditRichText = canEditRichText && !lossyProperties; GraphemeClusterStarts = DetailTextGraphemeClusters.GetClusterStarts(PlainText); } @@ -201,7 +197,7 @@ public DetailRichTextValue(string plainText, IReadOnlyList runs, /// /// Whether at least one run carries a TsString text property the /// model does NOT round-trip (e.g. foreground/background colour, character offset, - /// super/subscript — anything beyond ws/named-style/font-family/font-size/bold/italic/underline/ + /// super/subscript -- anything beyond ws/named-style/font-family/font-size/bold/italic/underline/ /// object-data). The neutral run-replay in DetailRichTextAdapter.ToTsString re-emits only /// the supported set, so a first edit (which skips the lossless RichXml fast-path) would silently /// drop the extra property. Such a value is shown read-only with the embedded-object tooltip @@ -551,7 +547,7 @@ public static class DetailRichTextEditAlgorithms /// to while runs outside the span keep their metadata untouched. /// The selection is snapped OUTWARD to Unicode grapheme-cluster boundaries (the same /// boundaries the bidi navigation uses) so a combining cluster is never split mid-character. - /// A zero-length (collapsed) selection — after clamping/snapping — is a no-op (the original + /// A zero-length (collapsed) selection -- after clamping/snapping -- is a no-op (the original /// value is returned); there is no pending caret format. /// The result intentionally carries NO RichXml: the lossless XML fast-path in /// DetailRichTextAdapter.ToTsString would otherwise re-emit the ORIGINAL runs (the plain @@ -617,7 +613,7 @@ public static DetailRichTextValue ApplySpanFormatting(DetailRichTextValue value, /// covered runs revert to the default/no-style paragraph style), matching the picker's /// "Default/None" entry. /// The span is snapped OUTWARD to Unicode grapheme-cluster boundaries so a combining cluster - /// is never split mid-character. A zero-length (collapsed) selection — after clamping/snapping — is + /// is never split mid-character. A zero-length (collapsed) selection -- after clamping/snapping -- is /// a no-op (the original value is returned). Lossy / read-only values are returned unchanged. /// The result carries NO RichXml (same reason as ): /// the lossless XML fast-path would otherwise re-emit the ORIGINAL runs (plain text is unchanged), @@ -685,7 +681,7 @@ public static DetailRichTextValue ApplySpanNamedStyle(DetailRichTextValue value, /// A null/empty is a no-op (a run must always carry a writing system; /// the picker only offers real project writing systems, never a "clear"). /// The span is snapped OUTWARD to Unicode grapheme-cluster boundaries so a combining cluster - /// is never split mid-character. A zero-length (collapsed) selection — after clamping/snapping — is + /// is never split mid-character. A zero-length (collapsed) selection -- after clamping/snapping -- is /// a no-op (the original value is returned). Lossy / read-only values are returned unchanged. /// The result carries NO RichXml (same reason as ): /// the lossless XML fast-path would otherwise re-emit the ORIGINAL runs (plain text is unchanged), @@ -743,7 +739,7 @@ public static DetailRichTextValue RetagSpanWritingSystem(DetailRichTextValue val /// /// Applies an EXTERNAL-LINK ORC (a hyperlink) over the half-open span [start, end), /// returning a NEW with the same plain text whose covered runs - /// carry the link's ObjectData (the kodtExternalPathName tag char + the URL) — the + /// carry the link's ObjectData (the kodtExternalPathName tag char + the URL) -- the /// model side of FwEditingHelper.AddHyperlink. Reuses the same run-split + cluster-snap /// machinery as the style/ws helpers. A collapsed selection or a null/empty URL is a no-op (the /// original value is returned). Lossy / read-only values are returned unchanged. The result drops @@ -815,7 +811,7 @@ public static DetailRichTextValue EditHyperlinkUrl(DetailRichTextValue value, in /// /// Deletes the ORC run that STARTS at plain-text position - /// (removing its text — typically the single object-replacement char), returning a NEW value. + /// (removing its text -- typically the single object-replacement char), returning a NEW value. /// Generic delete: ANY ORC kind (link, picture, footnote, other) is removable. A position that is /// not the start of an ORC run is a no-op. The result drops RichXml so the adapter re-emits /// via run-replay. @@ -988,7 +984,7 @@ public static string SpanNamedStyle(DetailRichTextValue value, int start, int en /// /// Toggle probe: true when EVERY run overlapping the (cluster-snapped, half-open) span /// [start, end) already carries . The UI uses this to decide a - /// Ctrl+B/I/U gesture's direction — an all-on selection toggles off, otherwise it turns on. + /// Ctrl+B/I/U gesture's direction -- an all-on selection toggles off, otherwise it turns on. /// An empty / collapsed span returns false (nothing to toggle off). /// public static bool SpanFullyHasFormat(DetailRichTextValue value, int start, int end, DetailRunFormat which) @@ -1131,7 +1127,7 @@ public static DetailRichTextValue ApplyPlainTextEdit(DetailRichTextValue current } // A pure insertion (nothing removed) defers to legacy TsString behavior: the inserted text - // inherits the PRECEDING run's properties — it attaches to the run that ends at the + // inherits the PRECEDING run's properties -- it attaches to the run that ends at the // insertion point, not the following run. (Position 0 falls to the first run, since // nothing precedes it.) Replacements/deletions keep the containing-run logic below. var startRun = originalEditEnd == prefix @@ -1301,7 +1297,7 @@ public DetailWsValue(string wsAbbrev, string value, string fontFamily = null, do /// /// ITEM 3: whether this alternative belongs to a voice/audio (IsVoice) writing system. The new /// view cannot yet play or record audio, so such a row is composed READ-ONLY with an audio - /// placeholder — the recording stays visible/diagnosable instead of presenting a blank editable + /// placeholder -- the recording stays visible/diagnosable instead of presenting a blank editable /// box whose first keystroke would corrupt the stored recording. Editing stays in the classic view. /// public bool IsAudio { get; } @@ -1325,7 +1321,7 @@ public DetailWsValue(string wsAbbrev, string value, string fontFamily = null, do /// read-only safety carry over verbatim); the /// per-paragraph named style is the legacy StPara.StyleName. An ORC-bearing / lossy paragraph /// is held read-only ( false) and preserved, exactly as a lossy single-WS - /// value is — full editing of such a paragraph stays in the classic view. + /// value is -- full editing of such a paragraph stays in the classic view. /// public sealed class DetailParagraph { @@ -1408,7 +1404,7 @@ public DetailChoiceOption(string key, string name, int depth = 0) /// /// Hierarchy level for deep possibility lists: 0 for top-level items, +1 per - /// sub-possibility nesting, in the list's own document order — drives the legacy indented + /// sub-possibility nesting, in the list's own document order -- drives the legacy indented /// chooser tree. Flat lists (and chooserInfo FlatList specs) stay 0 throughout. /// public int Depth { get; } @@ -1416,7 +1412,7 @@ public DetailChoiceOption(string key, string name, int depth = 0) /// /// A list-editor jump link on a chooser/reference-vector row: the legacy chooser dialog's - /// "Edit the … list" LinkLabel (ReallySimpleListChooser.AddLink with + /// "Edit the ... list" LinkLabel (ReallySimpleListChooser.AddLink with /// LinkType.kGotoLink), composed from the layout's chooserLink type="goto" /// metadata. Clicking it asks the host to jump to the tool that edits the underlying list. /// @@ -1436,7 +1432,7 @@ public DetailChooserLink(string label, string tool, string targetGuid = null) public string Tool { get; } /// - /// The jump's target object guid string, or null for a plain tool jump — the legacy chooser + /// The jump's target object guid string, or null for a plain tool jump -- the legacy chooser /// passes Guid.Empty (m_guidLink) unless a flidTextParam resolved one, /// and none of the lexeme-editor parts carry that. /// @@ -1445,7 +1441,7 @@ public DetailChooserLink(string label, string tool, string targetGuid = null) /// /// A request to follow a chooser jump link: the host dispatches it the way the legacy - /// chooser does on link click — mediator FollowLink with FwLinkArgs(tool, target) + /// chooser does on link click -- mediator FollowLink with FwLinkArgs(tool, target) /// (ReallySimpleListChooser.HandleAnyJump). /// public sealed class DetailLinkRequest @@ -1572,7 +1568,7 @@ public DetailField( public string HotlinksId { get; } /// - /// True when this row is a multi-writing-system text row — the legacy multistring editor + /// True when this row is a multi-writing-system text row -- the legacy multistring editor /// (MultiStringSlice), as opposed to a single-ws string editor. It mirrors the /// legacy slice is MultiStringSlice test so the in-string context menu can add the shared /// mnuDataTree-MultiStringSlice group (with the Writing Systems submenu) for exactly those @@ -1602,7 +1598,7 @@ public DetailField( /// /// The project's available CHARACTER-type style names /// the per-WS editor offers when restyling a selection (sourced by the composer from the project's - /// styles — Cache.LangProject.StylesOC filtered to character styles). Empty when no + /// styles -- Cache.LangProject.StylesOC filtered to character styles). Empty when no /// stylesheet is reachable or the field is not a styleable text row; the style picker affordance is /// then suppressed. The host seam: a settable list the composer populates at compose time (like /// /), keeping this FwAvalonia layer LCModel-free. @@ -1612,7 +1608,7 @@ public DetailField( /// /// The project's available writing systems - /// (stable IETF tag + display name) the per-WS editor offers when retagging a selection — sourced + /// (stable IETF tag + display name) the per-WS editor offers when retagging a selection -- sourced /// by the composer from Cache (analysis + vernacular writing systems). Empty when no /// writing-system list is reachable or the field is not a retaggable text row; the WS picker /// affordance is then suppressed. The host seam: a settable list the composer populates at compose @@ -1643,7 +1639,7 @@ public DetailField( /// /// The project's available PARAGRAPH-type style names the structured-text editor offers in /// its per-paragraph style picker (the host seam the composer populates from - /// Cache.LangProject.StylesOC filtered to paragraph styles — like + /// Cache.LangProject.StylesOC filtered to paragraph styles -- like /// for character styles). Empty when no styles are reachable; /// the per-paragraph style picker affordance is then suppressed. A test can supply its own list. /// @@ -1670,14 +1666,14 @@ public DetailField( /// /// The list-editor jump links of a chooser/reference-vector row: composed from the - /// layout's chooserLink type="goto" metadata (e.g. "Edit the Publications list" → + /// layout's chooserLink type="goto" metadata (e.g. "Edit the Publications list" -> /// publicationsEdit). The gear flyout surfaces them below the options; clicking raises the /// host's DetailLinkRequest callback. Empty for rows without chooser metadata. /// public IReadOnlyList ChooserLinks { get; } } - /// Which legacy menu a right-click maps to (section 13). + /// Which legacy menu a right-click maps to. public enum DetailMenuKind { /// The slice menu (layout `menu=`), legacy right-click on the tree node/label. @@ -1691,7 +1687,7 @@ public enum DetailMenuKind } /// - /// A request to show a legacy-defined context menu for a detail row (section 13): the host + /// A request to show a legacy-defined context menu for a detail row: the host /// resolves the menu id against the xCore window configuration and shows the same menu the /// legacy slice shows, at the given screen point, with the row's bound object as command target. /// diff --git a/Src/Common/FwAvalonia/Detail/DetailModelProjector.cs b/Src/Common/FwAvalonia/Detail/DetailModelProjector.cs index 55b2852e3b..d37e533574 100644 --- a/Src/Common/FwAvalonia/Detail/DetailModelProjector.cs +++ b/Src/Common/FwAvalonia/Detail/DetailModelProjector.cs @@ -122,9 +122,9 @@ private static DetailField CreateField( /// /// Maps a node's editor to a renderable kind. Obsolete editors are unsupported; the - /// chooser categories render as choosers; everything else is treated as text — the + /// chooser categories render as choosers; everything else is treated as text -- the /// deliberately small first-slice projection. The editor-string knowledge itself lives - /// ONCE, in — this method keeps no + /// ONCE, in -- this method keeps no /// heuristics of its own. /// private static DetailFieldKind ClassifyKind(ViewNode node) diff --git a/Src/Common/FwAvalonia/Detail/DetailRichTextChrome.cs b/Src/Common/FwAvalonia/Detail/DetailRichTextChrome.cs index ff9a213c72..de16fa9ded 100644 --- a/Src/Common/FwAvalonia/Detail/DetailRichTextChrome.cs +++ b/Src/Common/FwAvalonia/Detail/DetailRichTextChrome.cs @@ -55,7 +55,7 @@ internal static TextBlock CreatePerRunFontDisplay(DetailRichTextValue rich, TextWrapping = TextWrapping.Wrap, VerticalAlignment = VerticalAlignment.Top, // Flat like the editors it stands in for (the box collapses out of layout while the display - // shows, so no overlay is needed — a pointer press swaps the editable box back in). + // shows, so no overlay is needed -- a pointer press swaps the editable box back in). Background = Brushes.Transparent, FlowDirection = rightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight }; @@ -104,9 +104,9 @@ internal static Button CreateSpanPicker(IReadOnlyList option Foreground = FwAvaloniaDensity.WsAbbrevBrush, FontSize = FwAvaloniaDensity.WsAbbrevFontSize, VerticalAlignment = VerticalAlignment.Top, - // The trigger must NOT take focus — clicking it would blur the editor, and Avalonia - // collapses the TextBox selection to the caret on LostFocus, so onOpen would snapshot an EMPTY - // selection and the gesture would stage nothing. Keeping focus on the editor preserves the span. + // The trigger must NOT take focus: clicking it blurs the editor and + // collapses the selection to the caret on LostFocus. onOpen would then + // snapshot EMPTY and stage nothing; keeping focus preserves the span. Focusable = false }; AutomationProperties.SetAutomationId(button, automationId); diff --git a/Src/Common/FwAvalonia/Detail/DetailStructureRules.cs b/Src/Common/FwAvalonia/Detail/DetailStructureRules.cs index 8f8054d0fd..3b1945e297 100644 --- a/Src/Common/FwAvalonia/Detail/DetailStructureRules.cs +++ b/Src/Common/FwAvalonia/Detail/DetailStructureRules.cs @@ -7,11 +7,11 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail { /// - /// The shared structural projection rules used by BOTH detail projectors — the thin - /// (view-definition → detail model, LCModel-free) and the full + /// The shared structural projection rules used by BOTH detail projectors -- the thin + /// (view-definition -> detail model, LCModel-free) and the full /// xWorks DetailComposer (LCModel-backed). The section-header row /// construction and the child-indent rule live ONCE here so the two paths cannot drift. (The third - /// structural rule — editor → renderable kind — is + /// structural rule -- editor -> renderable kind -- is /// likewise shared, in .) /// public static class DetailStructureRules @@ -25,7 +25,7 @@ public static int ChildIndent(string label, int depth) => string.IsNullOrEmpty(label) ? depth : depth + 1; /// - /// Builds the canonical section-header row — the single construction site for + /// Builds the canonical section-header row -- the single construction site for /// rows across both projectors. The thin mapper passes the /// defaults (no collapse affordance, no menu/HVO); the composer passes its LCModel-enriched /// values (collapsible state from expansion, slice menu/hotlinks, owning object HVO). diff --git a/Src/Common/FwAvalonia/Detail/DetailViewingServices.cs b/Src/Common/FwAvalonia/Detail/DetailViewingServices.cs index d5a6409967..de8806a792 100644 --- a/Src/Common/FwAvalonia/Detail/DetailViewingServices.cs +++ b/Src/Common/FwAvalonia/Detail/DetailViewingServices.cs @@ -96,7 +96,7 @@ public static class DetailViewingServices /// /// Every native viewing capability the detail view now provides managed, with its owner and the /// native symbol it supersedes. Owners all live in the FwAvalonia production assembly, which - /// (per EngineIsolationAuditTests) cannot load native Views — so by construction these + /// (per EngineIsolationAuditTests) cannot load native Views -- so by construction these /// replacements use Avalonia's own Skia/HarfBuzz text stack, not the C++ engine. /// public static IReadOnlyList Replacements { get; } = diff --git a/Src/Common/FwAvalonia/Detail/FwFieldControls.cs b/Src/Common/FwAvalonia/Detail/FwFieldControls.cs index 4c0a16d00e..1663d9d87a 100644 --- a/Src/Common/FwAvalonia/Detail/FwFieldControls.cs +++ b/Src/Common/FwAvalonia/Detail/FwFieldControls.cs @@ -19,7 +19,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail { /// /// FieldWorks-owned multi-writing-system text field over an IR-projected detail field - /// (tasks 6.1/6.2): one compact row per writing-system alternative — abbreviation gutter plus a + /// (tasks 6.1/6.2): one compact row per writing-system alternative -- abbreviation gutter plus a /// text editor carrying the project WS font, right-to-left flow direction for RTL scripts, and /// per-WS keyboard activation on focus through the supplied callback (the same behavior legacy /// slices get from EditingHelper.SetKeyboardForWs). Write-through staging goes to the @@ -27,13 +27,13 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// Multi-run/styled content IS editable here as plain-text-over-preserved-runs: the original /// TsString runs are projected into , a keystroke replays the /// untouched runs around the edit, and the edit context rebuilds the TsString. A value is held - /// read-only ONLY when that replay would corrupt it — an embedded object the runs cannot rebuild, + /// read-only ONLY when that replay would corrupt it -- an embedded object the runs cannot rebuild, /// or a run carrying a TsString property the model does not round-trip /// (); such a value shows the explanatory tooltip /// and stays full-fidelity in the classic view. /// Menus: a row whose layout binds a slice menu (`menu=`, e.g. the Lexeme Form's /// mnuDataTree-LexemeForm with Swap/Convert commands) surfaces it on RIGHT-CLICK only (the - /// label/value right-click paths) — text rows draw NO gear. The gear is reserved for the + /// label/value right-click paths) -- text rows draw NO gear. The gear is reserved for the /// "configure the supporting list" jump on chooser/vector rows; it never opens a menu. /// Rich-text operations (character style, per-run writing-system retag, insert/edit external link, /// delete embedded object) are NOT always-visible inline controls: like the legacy detail slice, which @@ -47,7 +47,7 @@ public sealed class FwMultiWsTextField : StackPanel, IHoverAffordanceProvider, I { // Teardown registered as each handler/subscription is wired, so a recycled or // active-cell-deactivated field can detach EVERY handler (several capture closures over box, - // currentRich, clipboard) and release its flyouts — preventing the handler-closure leak on the + // currentRich, clipboard) and release its flyouts -- preventing the handler-closure leak on the // editor path when VirtualizingStackPanel discards the container. private readonly CompositeDisposable _teardown = new CompositeDisposable(); private bool _disposed; @@ -83,17 +83,9 @@ private void AddValueRow(DetailField field, string automationId, var currentRich = value.RichText; var abbrev = CreateWsAbbrev(value); - // Legacy look (12.2): values render flat like RootSite views — no box, no fill. - // Local values outrank the theme's pointer-over/focus setters, so the editor stays flat. - // Data-safety read-only: a value whose plain-text run-replay would corrupt it stays - // READ-ONLY — and says so explicitly (tooltip) — rather than presenting an editable box - // whose first keystroke silently drops content. Two cases feed CanEditRichText: a run - // carrying an embedded object (ORC) the managed editor cannot rebuild, and a run carrying - // a TsString property the DetailTextRun model does not round-trip (e.g. fore/back colour, - // offset, superscript). The original TsString is preserved losslessly (RichXml), so the - // field round-trips and remains fully editable in the classic view. - // A voice/audio writing-system alternative renders as READ-ONLY text (the audio filename); - // there is no in-pane player. Full audio editing stays in the classic view. + // Values render flat with no box/fill, and go read-only with a tooltip + // -- instead of corrupting on the first keystroke -- when a run carries + // an ORC or non-round-trippable TsString property, or is audio. var valueIsReadOnly = editContext == null || !field.IsEditable || !value.CanEditRichText || value.IsAudio; var box = CreateValueBox(field, value, valueIsReadOnly); @@ -169,7 +161,7 @@ private void AddValueRow(DetailField field, string automationId, // (MoveCaret steps by clusters) and the anchor is a cluster-aligned caret/selection // edge, so the span [anchor..nextCaret] never splits a cluster. Set the caret FIRST: // Avalonia's CaretIndex setter clears the selection, so assigning SelectionStart/ - // SelectionEnd AFTER leaves the caret on the moving edge without re-collapsing — the + // SelectionEnd AFTER leaves the caret on the moving edge without re-collapsing -- the // original order (selection then CaretIndex) collapsed the span to an empty caret, // which is why Shift+Arrow moved the caret with nothing selected. box.CaretIndex = nextCaret; @@ -248,7 +240,7 @@ private void AddValueRow(DetailField field, string automationId, // floating toolbar: they match the legacy Views editor (FwEditingHelper's // Ctrl+B/I/U), need no extra decorations in the dense detail rows, and act on the same // SelectionStart..SelectionEnd the bidi/clipboard handlers already use. The gesture - // only stages when the selection is non-empty (a collapsed caret is a no-op — + // only stages when the selection is non-empty (a collapsed caret is a no-op -- // there is no pending format for the next insert) and only on an editable, // non-lossy value (this whole block is gated on value.CanEditRichText already). EventHandler formatKeyDown = (s, e) => @@ -299,15 +291,7 @@ private void AddValueRow(DetailField field, string automationId, box.AddHandler(InputElement.KeyDownEvent, formatKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); _teardown.Add(() => box.RemoveHandler(InputElement.KeyDownEvent, formatKeyDown)); - // Apply/clear a NAMED CHARACTER STYLE over the selection. A right-click menu - // item "Character style…" opens the shared FwOptionChooser (single-select) seeded with a - // leading "Default (no style)" entry that CLEARS the style, followed by the project's - // available character style names. It acts on the TextBox's current - // SelectionStart..SelectionEnd; committing calls ApplySpanNamedStyle and stages through - // TrySetRichText — exactly the rich-text seam Ctrl+B/I/U uses. Only built when the field - // actually carries available styles (so plain-text-only projects show no item), and only - // off a bridged row (whose host menu owns the field commands); the whole block is already - // gated on the editable, non-lossy value. + // Committing routes through ApplySpanNamedStyle + TrySetRichText -- the same rich-text seam Ctrl+B/I/U uses -- and is only built off a bridged row when the field actually has available styles. if (!hasBridge && field.AvailableNamedStyles != null && field.AvailableNamedStyles.Count > 0) { // The picker's option set: a clear-style entry (empty key) plus one option per @@ -409,7 +393,7 @@ private void AddValueRow(DetailField field, string automationId, // System" button opening the shared FwOptionChooser (single-select) seeded with the // project's available writing systems (tag = key, display name = caption). It acts on the // TextBox's current SelectionStart..SelectionEnd; committing calls RetagSpanWritingSystem - // and stages through TrySetRichText — the same rich-text seam Ctrl+B/I/U and the style + // and stages through TrySetRichText -- the same rich-text seam Ctrl+B/I/U and the style // picker use. Built only when the field carries available writing systems; the whole block // is already gated on the editable, non-lossy value. There is no "clear" entry: a run must // always carry a writing system, so the picker offers only real project writing systems. @@ -506,7 +490,7 @@ private void AddValueRow(DetailField field, string automationId, } } - // External-link insert / edit prompt. A right-click menu item "Insert/edit link…" + // External-link insert / edit prompt. A right-click menu item "Insert/edit link..." // opens a flyout with a URL TextBox + Apply (the dialog-light prompt the decision calls // for). On open it snapshots the selection and, when that selection sits on an existing // link run, pre-fills the URL for editing. Apply over a real selection inserts/edits the @@ -681,7 +665,7 @@ private void AddValueRow(DetailField field, string automationId, WireWritingSystemKeyboard(box, value, writingSystemFocused); // The value box's right-click menu (legacy MultiStringSlice parity: operations live OFF the - // row, not as always-visible inline controls). A non-bridged row carries a local menu — Copy + // row, not as always-visible inline controls). A non-bridged row carries a local menu -- Copy // plus whatever rich-text operations its gate built (character style / writing-system retag / // insert-or-edit link / delete embedded object). A bridged row's host xCore menu is // authoritative and already wired above, so it gets no local menu here. @@ -772,7 +756,7 @@ private void WireGhostPrompt(TextBox box, DetailField field) { if (!string.IsNullOrEmpty(field.GhostPrompt)) { - // 14.1: the legacy ghost add-prompt is a watermark — it disappears the moment the + // 14.1: the legacy ghost add-prompt is a watermark -- it disappears the moment the // user clicks in (focus), and reappears only if they leave without typing. box.Watermark = field.GhostPrompt; EventHandler ghostGot = (s2, e2) => box.Watermark = string.Empty; @@ -787,7 +771,7 @@ private void WireGhostPrompt(TextBox box, DetailField field) } } - // Section 13: a row with a legacy `contextMenu=` binding shows the SAME xCore-defined + // A row with a legacy `contextMenu=` binding shows the SAME xCore-defined // menu the legacy string view shows (MultiStringSlice.HandleRightMouseClickedEvent // path), routed through the host bridge. That host menu owns this field's commands, so it // stays the single right-click menu for a bridged row; the relocated rich-text operations @@ -809,9 +793,9 @@ private bool WireBridgeContextMenu(TextBox box, DetailField field, EventHandler swallowContext = (s2, e2) => e2.Handled = true; box.AddHandler(InputElement.PointerPressedEvent, menuPressed, Avalonia.Interactivity.RoutingStrategies.Tunnel); - // 15.2: exactly ONE menu — drop the TextBox theme flyout (Cut/Copy/Paste, which - // opens from ContextRequested on right-button RELEASE) so only the bridged menu - // shows, and swallow the request so nothing else opens. + // 15.2: exactly ONE menu -- drop the TextBox flyout (Cut/Copy/Paste, + // which opens from ContextRequested on right-button RELEASE) so only the + // bridged menu shows; swallow the request so nothing else opens. box.ContextFlyout = null; box.AddHandler(Control.ContextRequestedEvent, swallowContext, Avalonia.Interactivity.RoutingStrategies.Tunnel); @@ -847,7 +831,7 @@ private static Grid CreateRowPanel(TextBlock abbrev, Control valueContent, bool { var rowPanel = new Grid { - // 14.2: a null background only hit-tests the glyphs — the whole row must + // 14.2: a null background only hit-tests the glyphs -- the whole row must // receive hover/right-click over the gaps too. Background = Brushes.Transparent }; @@ -938,7 +922,7 @@ private static DetailTextRun RunAt(DetailRichTextValue rich, int start) public IReadOnlyList HoverAffordances => Array.Empty(); /// - /// The count of still-attached handler/subscription teardowns — zero after . + /// The count of still-attached handler/subscription teardowns -- zero after . /// Exposed so a recycling test can assert the editor released every handler it wired. /// public int AttachedHandlerCount => _teardown.Count; @@ -979,9 +963,9 @@ private static async System.Threading.Tasks.Task CopySelectionAsync(TextBox box, /// /// GEAR = CONFIGURE: the shared gear semantics of the chooser and reference-vector rows. - /// Clicking the gear DIRECTLY dispatches the list-editor jump — the host's + /// Clicking the gear DIRECTLY dispatches the list-editor jump -- the host's /// callback rides the same path the legacy chooser dialog's - /// "Edit the … list" LinkLabel rides (ReallySimpleListChooser.AddLink kGotoLink → + /// "Edit the ... list" LinkLabel rides (ReallySimpleListChooser.AddLink kGotoLink -> /// FollowLink). NO flyout, NO context menu opens from the gear; option flyouts carry zero /// link items. The gear renders ONLY when a list-edit target resolved at compose time (the /// row carries at least one goto ); the FIRST link wins when @@ -1017,15 +1001,15 @@ internal static Button CreateConfigureGear(DetailField field, string automationI /// /// FieldWorks-owned chooser field: a button opening a flyout of service-backed options /// (the options come from the LCModel-sourced detail model, not the control). The flyout is the - /// shared compact — an AutoCompleteBox-based OPTIONS ONLY selector, + /// shared compact -- an AutoCompleteBox-based OPTIONS ONLY selector, /// no link items. Committing an /// option stages it through the edit context, closes the flyout, and returns focus to the button - /// — the popup-focus-return behavior the seam specs require. Without an edit context the chooser + /// -- the popup-focus-return behavior the seam specs require. Without an edit context the chooser /// is a read-only display of the current selection. - /// Styling: the button is transparent/borderless — the value text reads flat like the legacy + /// Styling: the button is transparent/borderless -- the value text reads flat like the legacy /// combo. When the row's supporting list resolved a list-editor target (a composed goto /// ), a hover-revealed CONFIGURE gear sits after the value and - /// directly dispatches the host jump () — it never opens the + /// directly dispatches the host jump () -- it never opens the /// options. Rows without a resolvable list editor draw no gear. /// public sealed class FwChooserField : Button, IHoverAffordanceProvider, IDisposable @@ -1068,11 +1052,7 @@ public FwChooserField( if (_gear != null) content.Children.Add(_gear); Content = content; - // Read-only rows stay ENABLED: disabling the whole button would suppress its pointer - // events (killing hover-reveal) and disable the nested configure gear — which is - // NAVIGATION (the "Edit the … list" jump), not editing. Like FwDialogLauncherField, - // only the value-editing affordance is withheld: no option flyout is wired below, so - // clicking the value of a read-only row does nothing. + // Read-only rows stay ENABLED because disabling the button would kill hover-reveal and disable the nested configure gear, which is navigation, not editing; only the value-editing flyout is withheld. AutomationProperties.SetAutomationId(this, automationId); AutomationProperties.SetName(this, field.Label ?? field.Field ?? automationId); @@ -1116,7 +1096,7 @@ public FwChooserField( }); } - /// The count of still-attached subscriptions — zero after . + /// The count of still-attached subscriptions -- zero after . public int AttachedHandlerCount => _teardown.Count; /// @@ -1133,7 +1113,7 @@ public void Dispose() _teardown.Clear(); } - // Restyled appearance only — the control keeps the Button theme (template, flyout-on-click, + // Restyled appearance only -- the control keeps the Button theme (template, flyout-on-click, // focus, automation peer), not a lookup by this derived type's key. protected override Type StyleKeyOverride => typeof(Button); @@ -1157,15 +1137,15 @@ private static string CurrentName(DetailField field) /// /// FieldWorks-owned editable reference-vector field: the current items rendered /// inline, each followed by the thin grey separator bar legacy reference slices draw - /// (VwSeparatorBox), with the TRAILING bar fronting the add slot — a "+" launcher whose flyout + /// (VwSeparatorBox), with the TRAILING bar fronting the add slot -- a "+" launcher whose flyout /// is the shared compact (AutoCompleteBox-based OPTIONS ONLY, /// zero link items): the /// possibility tree indented by for enumerated lists, /// or the host search delegate's results for search-backed vectors (lexicons search, lists /// enumerate), both behind the same filter box and virtualized capped list. /// Right-clicking an item offers Remove. Without an edit context the row is read-only display. - /// Hover-reveal polish: the separator bars, the "+" launcher, and — only when the - /// row's list resolved a list-editor target — the CONFIGURE gear (which directly dispatches + /// Hover-reveal polish: the separator bars, the "+" launcher, and -- only when the + /// row's list resolved a list-editor target -- the CONFIGURE gear (which directly dispatches /// the host jump, never a flyout: ) fade in on row hover; the /// items/text stay always visible. /// @@ -1173,7 +1153,7 @@ public sealed class FwReferenceVectorField : StackPanel, IHoverAffordanceProvide { private readonly List _affordances = new List(); // Teardown for the per-item Remove handlers, the add picker's OptionCommitted/Dismissed - // subscriptions, the gear click, and the option flyout — so a recycled vector cell releases + // subscriptions, the gear click, and the option flyout -- so a recycled vector cell releases // every closure it wired and drops its flyout, mirroring FwChooserField/FwMultiWsTextField // (wiring these with NO teardown leaks the editor path // when VirtualizingStackPanel discards the container). Empty for read-only rows. @@ -1183,7 +1163,7 @@ public sealed class FwReferenceVectorField : StackPanel, IHoverAffordanceProvide /// /// (optional, like the other field callbacks): invoked /// after a SUCCESSFUL add/remove stage, so the host view can commit the gesture immediately - /// — legacy commits each chooser-dialog gesture as it lands, and the row's Items are a + /// -- legacy commits each chooser-dialog gesture as it lands, and the row's Items are a /// compose-time snapshot, so without a commit + re-show nothing visibly changes. /// Failed stages never fire it. /// @@ -1195,7 +1175,7 @@ public FwReferenceVectorField( Action linkRequested = null) { Orientation = Orientation.Horizontal; - // 14.2-style hit-testing rule: a null background only hit-tests the glyphs — the WHOLE + // 14.2-style hit-testing rule: a null background only hit-tests the glyphs -- the WHOLE // row must receive hover so the reveal affordances work over the gaps between items. Background = Brushes.Transparent; AutomationProperties.SetAutomationId(this, automationId); @@ -1209,7 +1189,7 @@ public FwReferenceVectorField( Text = item.Name, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), - // 14.2: a null background only hit-tests the glyphs — the whole item must take + // 14.2: a null background only hit-tests the glyphs -- the whole item must take // the right-click or the Remove flyout only opens over ink. Background = Brushes.Transparent }; @@ -1262,7 +1242,7 @@ public FwReferenceVectorField( AutomationProperties.SetAutomationId(addButton, automationId + ".Add"); AutomationProperties.SetName(addButton, FwAvaloniaStrings.AddItem); - // "+" = OPTIONS ONLY: the one compact filterable picker — static options enumerate + // "+" = OPTIONS ONLY: the one compact filterable picker -- static options enumerate // (with Depth hierarchy), search-backed vectors ride the host search delegate. // No link items ever ride this flyout. The vector add slot opens in MULTI-SELECT mode // (checkboxes + an "Add" button): the user checks several candidates and commits the @@ -1306,7 +1286,7 @@ public FwReferenceVectorField( _affordances.Add(addButton); // GEAR = CONFIGURE (only when the row's list resolved a list-editor target): clicking - // dispatches the host jump directly — it does NOT open the add flyout. + // dispatches the host jump directly -- it does NOT open the add flyout. var gearButton = DetailGearChrome.CreateConfigureGear(field, automationId, linkRequested); if (gearButton != null) { @@ -1323,7 +1303,7 @@ public FwReferenceVectorField( public IReadOnlyList HoverAffordances => _affordances; /// - /// The count of still-attached subscriptions/handlers — zero after . + /// The count of still-attached subscriptions/handlers -- zero after . /// Exposed so a recycling test can assert the editor released every handler it wired. /// public int AttachedHandlerCount => _teardown.Count; @@ -1345,7 +1325,7 @@ public void Dispose() } // The legacy VwSeparatorBox: a ~2px, font-height, light grey vertical bar after each item - // (and fronting the add slot) — the affordance that marks where content can be added. + // (and fronting the add slot) -- the affordance that marks where content can be added. private void AddSeparatorBar() { var bar = new Border @@ -1366,7 +1346,7 @@ private void AddSeparatorBar() /// trailing launcher button, drawn as the SAME hover-revealed settings gear the chooser and /// reference vector draw. The button invokes a host-injected callback (a plain delegate; this /// layer stays LCModel-free). Without a callback the gear renders DISABLED with an explanatory - /// tooltip — the value still shows, the affordance is visibly unavailable once hover reveals it. + /// tooltip -- the value still shows, the affordance is visibly unavailable once hover reveals it. /// public sealed class FwDialogLauncherField : DockPanel, IHoverAffordanceProvider { @@ -1378,19 +1358,19 @@ public FwDialogLauncherField(string value, string label, Action launch) _launch = launch; Value = value ?? string.Empty; LastChildFill = true; - // A null background only hit-tests the glyphs — the WHOLE row must receive hover + // A null background only hit-tests the glyphs -- the WHOLE row must receive hover // so the gear reveal works over the gaps. Background = Brushes.Transparent; AutomationProperties.SetName(this, label ?? string.Empty); - // The legacy ButtonLauncher launch affordance, docked at the row's end like m_panel — + // The legacy ButtonLauncher launch affordance, docked at the row's end like m_panel -- // drawn as the shared settings gear, hover-revealed like the chooser/vector ones. _button = DetailChrome.CreateGearButton(); _button.IsEnabled = launch != null; AutomationProperties.SetName(_button, FwAvaloniaStrings.LaunchDialog); if (launch == null) { - // Degraded mode: no host dialog service — the gear shows but cannot launch. + // Degraded mode: no host dialog service -- the gear shows but cannot launch. ToolTip.SetTip(_button, FwAvaloniaStrings.LauncherUnavailable); } _button.Click += (s, e) => Launch(); diff --git a/Src/Common/FwAvalonia/Detail/FwOptionChooser.cs b/Src/Common/FwAvalonia/Detail/FwOptionChooser.cs index c9658be13d..f451e5771b 100644 --- a/Src/Common/FwAvalonia/Detail/FwOptionChooser.cs +++ b/Src/Common/FwAvalonia/Detail/FwOptionChooser.cs @@ -24,11 +24,11 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// /// The ONE compact, filterable select-from-list control every Avalonia option picker uses: /// the chooser's single-select flyout, the reference vector's "+" add flyout, and preview - /// morph-type chooser. It is a small NATIVE composite — a filter box - /// stacked over a of options — shown INLINE inside the host flyout. The + /// morph-type chooser. It is a small NATIVE composite -- a filter box + /// stacked over a of options -- shown INLINE inside the host flyout. The /// host flyout is therefore the only popup; there is no second floating dropdown (an /// AutoCompleteBox would spawn a separate grey-chromed PART_SuggestionsContainer - /// popup — the source of a heavy grey border and focus/arrow-key flakiness). + /// popup -- the source of a heavy grey border and focus/arrow-key flakiness). /// /// Keyboard handling is trivial because focus never leaves the filter box: the options list is /// = false, and Down/Up/Enter/Escape are handled directly @@ -41,18 +41,13 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// from committing the highlighted item. /// /// Committing/dismissing is still the HOST field's signal to stage (TrySetOption / - /// TryAddReferenceItem) and hide its flyout — the picker itself never stages. The picker keeps + /// TryAddReferenceItem) and hide its flyout -- the picker itself never stages. The picker keeps /// the typed search text visible while arrowing through results; it does not overwrite the query /// with the highlighted row's display text. /// public sealed class FwOptionChooser : Border { - // Diagnostics for the picker's focus/keyboard routing (the historic arrow-key trouble spot). - // OFF by default. Enable either via the "FwOptionPicker" switch (value >= 3) in - // Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config (or any app .config), or — handy - // for the Avalonia Preview Host, whose generated .config has no switches section — by setting - // the FW_OPTIONPICKER_TRACE environment variable (e.g. to 3) before launching. Output lands - // in FieldWorks.trace.log. + // OFF by default; besides the "FwOptionPicker" config switch, set FW_OPTIONPICKER_TRACE (e.g. to 3) since the Avalonia Preview Host's generated .config has no switches section. private static readonly TraceSwitch s_trace = CreateTraceSwitch(); private static TraceSwitch CreateTraceSwitch() @@ -91,16 +86,9 @@ private static TraceSwitch CreateTraceSwitch() private string _anchorKey; private readonly Button _addButton; - // Dropdown (collapsed) presentation mode — opt-in, default OFF so every existing consumer - // (chooser single-select flyout, reference-vector "+" multi-select add picker, preview morph - // chooser) is byte-for-byte unchanged: those mount the picker INLINE inside a host flyout and - // want the search box + list always visible. In dropdown mode the picker is instead a compact - // ComboBox-like control: the Border shows a toggle button with the current selection, and the - // existing filter+list panel is hosted in a focus-gated Flyout anchored to the toggle button — - // the very Flyout the inline consumers already open through CreateOptionFlyout — that opens on - // click and closes on pick, reusing the same filtering + keyboard behavior. A flyout positions - // itself in the trigger's own window, so it stays correct under fractional display scaling; only - // the single-select path supports dropdown mode (the MorphType picker is single-select). + // Opt-in, default OFF so existing inline consumers stay byte-for-byte + // unchanged; dropdown mode reuses the same focus-gated Flyout as + // CreateOptionFlyout; only single-select supports it. private readonly bool _dropdown; private readonly ToggleButton _dropdownButton; private readonly TextBlock _dropdownLabel; @@ -242,7 +230,7 @@ public FwOptionChooser(IReadOnlyList options, _dropdownButton.IsCheckedChanged += OnDropdownButtonCheckedChanged; // The filter+list panel, bordered as in inline mode, becomes the flyout body so the user - // sees the same clean selection panel — just floating on top instead of inline. + // sees the same clean selection panel -- just floating on top instead of inline. var popupPanel = new Border { Background = FwAvaloniaDensity.PickerBackgroundBrush, @@ -292,7 +280,7 @@ public FwOptionChooser(IReadOnlyList options, if (_dropdown) { - // Keep the collapsed label in sync with the list selection — both the up-front default and + // Keep the collapsed label in sync with the list selection -- both the up-front default and // any later external move (the VM's derive-on-type SelectedIndex reselection). _list.SelectionChanged += (s, e) => SyncDropdownLabel(); SyncDropdownLabel(); @@ -300,7 +288,7 @@ public FwOptionChooser(IReadOnlyList options, else { // Inline mode auto-focuses the filter on open (flyout). Dropdown mode is collapsed on - // attach, so it must NOT grab focus — focus moves to the filter only when the user opens it. + // attach, so it must NOT grab focus -- focus moves to the filter only when the user opens it. AttachedToVisualTree += (s, e) => { Log("AttachedToVisualTree; posting focus (Loaded)."); @@ -319,7 +307,7 @@ private void Log(string message) /// /// Focuses the filter box. Called on attach AND from the host flyout's Opened event, because /// a windowed desktop popup does not synchronously lay out its content, so the flyout's own - /// auto-focus can no-op (GetNext returns null before the template is applied) — leaving focus + /// auto-focus can no-op (GetNext returns null before the template is applied) -- leaving focus /// on the launching button, where arrow keys never reach the picker. /// public void FocusFilter() @@ -396,7 +384,7 @@ private void UpdateDropdownLabel() /// /// Builds the host flyout for an option picker with the Fluent FlyoutPresenter's heavy - /// grey decorations (its padding, border, and grey background) stripped to nothing — so the + /// grey decorations (its padding, border, and grey background) stripped to nothing -- so the /// picker's own thin border is the ONLY boundary the user sees, instead of the default thick /// grey box wrapping it. Every option picker (chooser, "+" vector add, preview chooser) /// opens through here so the styling stays consistent. @@ -407,11 +395,11 @@ public static Flyout CreateOptionFlyout(FwOptionChooser picker, PlacementMode pl /// /// The single option-flyout construction path: a chromeless that re-requests /// filter focus once open. The inline consumers pass the picker itself as content; dropdown mode - /// passes its filter+list panel and the same picker for focus — so both open through one flyout + /// passes its filter+list panel and the same picker for focus -- so both open through one flyout /// implementation instead of a hand-placed popup. /// A windowed desktop popup is shown non-activated (Win32 ShowNoActivate) and the flyout's own /// auto-focus can no-op before the presenter template is applied. Re-request focus once the popup - /// is open, posted at Input priority so it runs AFTER layout/render — otherwise focus stays on the + /// is open, posted at Input priority so it runs AFTER layout/render -- otherwise focus stays on the /// launching button and the arrow keys never reach the picker. /// private static Flyout CreateOptionFlyout(object content, FwOptionChooser picker, PlacementMode placement) @@ -466,7 +454,7 @@ public void OpenDropdown() public event Action OptionCommitted; /// - /// Raised when the user commits the CHECKED SET (the "Add" button) in multi-select mode — the + /// Raised when the user commits the CHECKED SET (the "Add" button) in multi-select mode -- the /// whole batch in one signal so the host stages it as one undoable step. Never raised in /// single-select mode. Empty checked set does not raise it (the Add button is disabled). /// @@ -677,7 +665,7 @@ private void ApplyFilter() /// Handles navigation at the picker ROOT (the AutoCompleteBox pattern): a single-line /// TextBox does not mark Up/Down as handled, so they bubble from the filter box up to here /// regardless of where exactly focus sits inside the picker. Registered with - /// handledEventsToo so it still fires if some inner control already marked the key handled — + /// handledEventsToo so it still fires if some inner control already marked the key handled -- /// far more reliable than a tunnel handler pinned to the TextBox, which only fires when focus /// is exactly on the TextBox. /// @@ -787,7 +775,7 @@ private IDataTemplate OptionTemplate() // Multi-select: a leading checkbox tracking the persisted checked set. The checkbox is // display-only (not hit-test-visible, not focusable) so a single row pointer-release or // Enter toggles it exactly ONCE through ToggleChecked (the row, not the box, owns the - // gesture) — matching the legacy multi-check chooser's row-toggle behavior and keeping + // gesture) -- matching the legacy multi-check chooser's row-toggle behavior and keeping // focus in the filter box. var check = new CheckBox { diff --git a/Src/Common/FwAvalonia/Detail/FwStructuredTextField.cs b/Src/Common/FwAvalonia/Detail/FwStructuredTextField.cs index 278f0a2395..a57f15a413 100644 --- a/Src/Common/FwAvalonia/Detail/FwStructuredTextField.cs +++ b/Src/Common/FwAvalonia/Detail/FwStructuredTextField.cs @@ -18,7 +18,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// /// FieldWorks-owned editable multi-paragraph structured-text (StText) field. A vertical stack of one /// bordered, dense editor row per paragraph; each row carries a run-aware text editor (the SAME - /// staging the single-WS uses — TextChanged replays the untouched + /// staging the single-WS uses -- TextChanged replays the untouched /// runs around the edit and stages through ), /// a per-paragraph named-style picker (the shared ), and add/delete /// paragraph affordances. Enter at a paragraph's end inserts a paragraph after it; Backspace in an @@ -26,10 +26,10 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// Commit timing mirrors the reference-vector rule: per-paragraph TEXT edits stage and ride the /// detail view's focus-loss autosave (one undo step per field edit), while STRUCTURAL gestures /// (add/delete/style) commit immediately through the gestureCompleted callback - /// and the host re-shows — the paragraph list is a compose-time snapshot, so without an immediate + /// and the host re-shows -- the paragraph list is a compose-time snapshot, so without an immediate /// commit + re-show the change would not appear. /// An ORC-bearing / lossy paragraph ( - /// false) renders a READ-ONLY box with the embedded-object tooltip and is preserved losslessly — full + /// false) renders a READ-ONLY box with the embedded-object tooltip and is preserved losslessly -- full /// editing of such a paragraph stays in the classic view. /// public sealed class FwStructuredTextField : StackPanel, IDisposable @@ -64,9 +64,9 @@ public FwStructuredTextField( gestureCompleted, clipboard, paragraphs[i], i, paragraphs.Count, editable)); } - // An StText always has at least one paragraph in the model; if the composer handed an empty - // list (a not-yet-materialized StText), show a single empty editable row so the user can type - // — the first keystroke materializes the StText through the edit-context setter (index 0). + // An StText always has at least one paragraph in the model; if the composer + // handed an empty list, show a single empty editable row: the first keystroke + // materializes it via the edit-context setter. if (paragraphs.Count == 0) { Children.Add(CreateParagraphRow(field, automationId, structuredText, writingSystemFocused, @@ -299,7 +299,7 @@ private Control CreateStyleAffordance(DetailField field, string automationId, Foreground = FwAvaloniaDensity.WsAbbrevBrush, FontSize = FwAvaloniaDensity.WsAbbrevFontSize, VerticalAlignment = VerticalAlignment.Top, - // Keep focus on the editor — a focusable trigger blurs the TextBox, Avalonia collapses + // Keep focus on the editor -- a focusable trigger blurs the TextBox, Avalonia collapses // the selection to caret on LostFocus, and the style would apply to an empty span (no-op). Focusable = false }; @@ -555,7 +555,7 @@ private static string FirstRunWsTag(DetailRichTextValue rich) return null; } - /// The count of still-attached handler teardowns — zero after . + /// The count of still-attached handler teardowns -- zero after . public int AttachedHandlerCount => _teardown.Count; /// diff --git a/Src/Common/FwAvalonia/Detail/HoverReveal.cs b/Src/Common/FwAvalonia/Detail/HoverReveal.cs index d656e10a27..1050c544ab 100644 --- a/Src/Common/FwAvalonia/Detail/HoverReveal.cs +++ b/Src/Common/FwAvalonia/Detail/HoverReveal.cs @@ -17,7 +17,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// /// A field editor whose row decorations include hover-revealed affordances (the chooser's settings /// gear, the reference vector's separator bars and "+" launcher). The detail view reads this - /// to widen the hover surface to the WHOLE row (label + editor) — presentation only, no behavior. + /// to widen the hover surface to the WHOLE row (label + editor) -- presentation only, no behavior. /// public interface IHoverAffordanceProvider { @@ -27,11 +27,11 @@ public interface IHoverAffordanceProvider /// /// Modern hover-reveal presentation for secondary affordances: the affordances start hidden by - /// OPACITY (they stay in layout — rows never reflow — and stay in the UIA tree, focusable), + /// OPACITY (they stay in layout -- rows never reflow -- and stay in the UIA tree, focusable), /// fade in (~120ms) while the pointer is over any hover source or any affordance (entering /// the gear itself must not flicker it away), and fade out when the pointer leaves them all. /// Keyboard access: an affordance gaining focus (Tab) also reveals; losing focus hides again - /// unless the pointer is over. Pure presentation — no flyout, staging, or automation-id changes. + /// unless the pointer is over. Pure presentation -- no flyout, staging, or automation-id changes. /// public static class HoverReveal { @@ -40,7 +40,7 @@ public static class HoverReveal // The reveal registration of an affordance: stamped the first time the affordance is // attached, looked up (and merged into) by every later Attach. The property is the - // idempotence anchor — without it each Attach call would stack an independent handler + // idempotence anchor -- without it each Attach call would stack an independent handler // set with its own watched list, and the groups would fight over the opacity. private static readonly AttachedProperty RevealGroupProperty = AvaloniaProperty.RegisterAttached("HoverRevealGroup", typeof(HoverReveal)); @@ -49,8 +49,8 @@ public static class HoverReveal /// Wires to reveal while the pointer is over any of /// (or over an affordance itself) and hide otherwise. /// Idempotent per affordance: attaching again (the view widening the hover surface to the - /// row after the control wired itself) merges into the existing registration — one handler - /// set, one watched list — instead of stacking a second independent one. + /// row after the control wired itself) merges into the existing registration -- one handler + /// set, one watched list -- instead of stacking a second independent one. /// public static void Attach(IReadOnlyList hoverSources, IReadOnlyList affordances) { @@ -59,9 +59,9 @@ public static void Attach(IReadOnlyList hoverSources, IReadOnlyList()).Where(s => s != null).Distinct().ToList(); - // Resolve the registration this call lands in: the first already-registered target's - // group wins; targets registered in OTHER groups merge into it (an Attach spanning - // previously separate registrations unifies them — they reveal together from then on). + // Resolve the registration this call lands in: the first registered + // target's group wins; targets in OTHER groups merge into it (an Attach + // spanning registrations unifies them -- they reveal together). RevealGroup group = null; foreach (var affordance in targets) { @@ -199,12 +199,12 @@ internal static class DetailChrome // glyph: 8 teeth on a 24-unit canvas rendered at ~14px in the muted ws-abbreviation hue. private static readonly Geometry GearGeometry = CreateGearGeometry(); - // A vertical ellipsis ("⋮", the "kebab" field-menu glyph): three stacked dots on the same - // 24-unit canvas, drawn as model EllipseGeometry (no stream context) so it renders in the - // headless unit tests that build these controls with no Avalonia platform loaded. + // A vertical ellipsis ("...", the "kebab" field-menu glyph): three + // stacked dots on the same 24-unit canvas, drawn as model EllipseGeometry + // so it renders in headless tests with no Avalonia platform loaded. private static readonly Geometry KebabGeometry = CreateKebabGeometry(); - /// The "⋮" field-options glyph, in the muted affordance hue. + /// The "..." field-options glyph, in the muted affordance hue. internal static Control CreateKebabIcon() => new Avalonia.Controls.Shapes.Path { @@ -216,7 +216,7 @@ internal static Control CreateKebabIcon() VerticalAlignment = VerticalAlignment.Center }; - /// A flat (transparent, borderless) button carrying the "⋮" glyph as its face. + /// A flat (transparent, borderless) button carrying the "..." glyph as its face. internal static Button CreateKebabButton() => new Button { @@ -265,7 +265,7 @@ internal static Button CreateGearButton() // Built from MODEL segments (PathGeometry/ArcSegment/LineSegment), NOT StreamGeometry.Open: // opening a stream context demands the IPlatformRenderInterface, and xWorks hosts construct - // these controls in plain unit tests with no Avalonia platform loaded — model geometry only + // these controls in plain unit tests with no Avalonia platform loaded -- model geometry only // touches the platform when actually rendered. private static Geometry CreateGearGeometry() { diff --git a/Src/Common/FwAvalonia/Detail/IDetailEditContext.cs b/Src/Common/FwAvalonia/Detail/IDetailEditContext.cs index f6f0fa942e..e512716018 100644 --- a/Src/Common/FwAvalonia/Detail/IDetailEditContext.cs +++ b/Src/Common/FwAvalonia/Detail/IDetailEditContext.cs @@ -12,7 +12,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// fenced commit/cancel boundary. The product implementation (xWorks) opens one fenced LCModel /// undo task lazily on the first staged edit, applies writes directly to the domain inside it, /// and ends it on (one step on the single global undo stack shared with - /// the legacy UI) or rolls it back on — the model the + /// the legacy UI) or rolls it back on -- the model the /// `avalonia-edit-sessions` and `avalonia-undo-redo` seam specs require. This layer stays /// LCModel-free so the Avalonia view can drive editing without a domain dependency; tests use a /// fake context. @@ -46,14 +46,14 @@ public interface IDetailEditContext : IEditSession /// /// Stages adding an item (by option key) to a - /// row (6.3). Returns false — WITHOUT opening the session — for keys outside the field's + /// row (6.3). Returns false -- WITHOUT opening the session -- for keys outside the field's /// possibility list, duplicates, or non-vector rows, like the legacy chooser. /// bool TryAddReferenceItem(DetailField field, string optionKey); /// /// Stages removing an item (by option key) from a - /// row. Returns false — without opening the session — when the item is not in the vector. + /// row. Returns false -- without opening the session -- when the item is not in the vector. /// bool TryRemoveReferenceItem(DetailField field, string optionKey); diff --git a/Src/Common/FwAvalonia/Detail/IStructuredTextEditing.cs b/Src/Common/FwAvalonia/Detail/IStructuredTextEditing.cs index daa6bae38c..ff034704c5 100644 --- a/Src/Common/FwAvalonia/Detail/IStructuredTextEditing.cs +++ b/Src/Common/FwAvalonia/Detail/IStructuredTextEditing.cs @@ -17,7 +17,7 @@ public interface IStructuredTextEditing /// /// Stages a run-aware text edit to ONE paragraph of a /// (StText) field, opening the session on the first - /// edit. Returns false — WITHOUT opening the session — for a non-StText row, an out-of-range + /// edit. Returns false -- WITHOUT opening the session -- for a non-StText row, an out-of-range /// paragraph index, or an ORC/lossy (read-only) paragraph. Like the run-aware single-WS path, /// the rich payload preserves run metadata so the product ITsString rebuilds without /// flattening. @@ -27,22 +27,22 @@ public interface IStructuredTextEditing /// /// Stages setting (or clearing, when is null/empty) the named /// paragraph style of ONE paragraph of a field. - /// Returns false — without opening the session — for a non-StText row or an out-of-range index. + /// Returns false -- without opening the session -- for a non-StText row or an out-of-range index. /// bool TrySetParagraphStyle(DetailField field, int paragraphIndex, string styleName); /// /// Stages inserting a new empty paragraph AFTER in a /// field (a negative index inserts at the start). - /// Returns false — without opening the session — for a non-StText row. The structural gesture + /// Returns false -- without opening the session -- for a non-StText row. The structural gesture /// commits immediately and the host re-shows (the model's paragraph list is a compose snapshot). /// bool TryInsertParagraph(DetailField field, int afterParagraphIndex); /// /// Stages deleting paragraph of a - /// field. Returns false — without opening the - /// session — for a non-StText row, an out-of-range index, or when it would delete the only + /// field. Returns false -- without opening the + /// session -- for a non-StText row, an out-of-range index, or when it would delete the only /// paragraph (the StText always keeps at least one, like the legacy editor). /// bool TryDeleteParagraph(DetailField field, int paragraphIndex); diff --git a/Src/Common/FwAvalonia/Detail/LexiconFirstSlice.cs b/Src/Common/FwAvalonia/Detail/LexiconFirstSlice.cs index bf9fe8bcd0..ed57ef7044 100644 --- a/Src/Common/FwAvalonia/Detail/LexiconFirstSlice.cs +++ b/Src/Common/FwAvalonia/Detail/LexiconFirstSlice.cs @@ -20,7 +20,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail /// - gloss: the LexSense-Detail-GlossAllA part slice (Gloss, multistring) compiled /// from the real parts inventory through a one-line caller layout. The shipped /// LexSense/Normal layout reaches Gloss only through its HeavySummary part ref, - /// which has no part definition in the shipped inventory — legacy DataTree walks the class + /// which has no part definition in the shipped inventory -- legacy DataTree walks the class /// hierarchy and then silently omits it (DataTree.ProcessPartRefNode), so the part inventory, not /// that layout, is the live source for the gloss slice's semantics. /// Stable ids therefore derive from the real layout/part paths. Product metadata (automation ids, @@ -35,7 +35,7 @@ public static class LexiconFirstSlice private const string GlossCallerLayout = ""; - /// Subclass → base class chain for part-ref resolution, mirroring the LCModel hierarchy. + /// Subclass -> base class chain for part-ref resolution, mirroring the LCModel hierarchy. private static readonly Dictionary MoFormBaseClassMap = new Dictionary(StringComparer.Ordinal) { { "MoStemAllomorph", "MoForm" }, diff --git a/Src/Common/FwAvalonia/Detail/MorphTypeSwapLogic.cs b/Src/Common/FwAvalonia/Detail/MorphTypeSwapLogic.cs index 34fc49e180..3ba04aedc8 100644 --- a/Src/Common/FwAvalonia/Detail/MorphTypeSwapLogic.cs +++ b/Src/Common/FwAvalonia/Detail/MorphTypeSwapLogic.cs @@ -56,7 +56,7 @@ public static class MorphTypeSwapLogic MorphTypeKind.DiscontiguousPhrase }; - // The ONE GUID → kind table. The seam is + // The ONE GUID -> kind table. The seam is // the cleaner home because it already owns MorphTypeKind and the stem/affix decision, and // both the xWorks composer and any future view can consume it without dragging WinForms // along. This project is deliberately LCModel-free, so the fixed MoMorphTypeTags model GUIDs @@ -100,7 +100,7 @@ public static bool TryClassify(Guid morphTypeGuid, out MorphTypeKind kind) public static bool IsStemType(MorphTypeKind type) => StemTypes.Contains(type); /// - /// True if the morph-type GUID classifies as a stem-type — the guid-level twin of the + /// True if the morph-type GUID classifies as a stem-type -- the guid-level twin of the /// legacy MorphTypeAtomicLauncher.IsStemType (an unknown guid is not a stem type, /// exactly like the legacy null/guard behavior). /// diff --git a/Src/Common/FwAvalonia/Detail/SliceFactory.cs b/Src/Common/FwAvalonia/Detail/SliceFactory.cs index 794cf63e86..af75f12897 100644 --- a/Src/Common/FwAvalonia/Detail/SliceFactory.cs +++ b/Src/Common/FwAvalonia/Detail/SliceFactory.cs @@ -15,7 +15,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Detail { /// /// The small bundle of (all-nullable) collaborators a editor needs, - /// passed to so the SAME field→control dispatch serves + /// passed to so the SAME field->control dispatch serves /// every host (today the detail-pane detail view, DataTree.CreateEditor; /// any future in-cell editor passes only the collaborators it has). Every member is optional: a null /// edit context yields read-only display; a null callback simply disables that affordance. @@ -41,19 +41,19 @@ public SliceFactoryContext( ShowWritingSystemAbbreviation = showWritingSystemAbbreviation; } - /// The shared edit-session/staging context; null → read-only display. + /// The shared edit-session/staging context; null -> read-only display. public IDetailEditContext EditContext { get; } - /// Per-WS keyboard activation callback for text fields (null → no keyboard switch). + /// Per-WS keyboard activation callback for text fields (null -> no keyboard switch). public Action WritingSystemFocused { get; } /// Right-click slice/section menu callback (null on hosts without a slice menu). public Action MenuRequested { get; } - /// Hyperlink follow callback for choosers/vectors (null → no link affordance). + /// Hyperlink follow callback for choosers/vectors (null -> no link affordance). public Action LinkRequested { get; } - /// Clipboard seam for text fields (null → framework default). + /// Clipboard seam for text fields (null -> framework default). public IFwClipboard Clipboard { get; } /// @@ -71,11 +71,11 @@ public SliceFactoryContext( } /// - /// The single →Avalonia-control dispatch. The detail + /// The single ->Avalonia-control dispatch. The detail /// pane (DataTree.CreateEditor, all 7 kinds) and the browse in-cell editor /// (EditableCellHost.Activate, a 2-kind Chooser/Text subset) both route here rather than /// hand-rolling their own dispatch, so adding a kind (or changing how a kind is built) happens once. - /// The factory is pure (static) — all per-host variation arrives through the + /// The factory is pure (static) -- all per-host variation arrives through the /// . /// public static class SliceFactory @@ -92,7 +92,7 @@ public static Control Build(DetailField field, string automationId, return CreateCustom(field, automationId); case DetailFieldKind.ReferenceVector: // Reference add/remove gestures commit immediately (legacy chooser-dialog behavior): the - // staged session would otherwise sit open — LCModel broadcasts PropChanged only at + // staged session would otherwise sit open -- LCModel broadcasts PropChanged only at // EndUndoTask and the row's Items are a compose-time snapshot, so the user would see no // change. The gesture-completed callback runs the SAME validation-gated save the // focus-loss autosave uses, whose re-show rebuilds the row from domain truth. A host @@ -139,10 +139,10 @@ private static Control CreateLiteral(DetailField field, string automationId) return block; } - // A plugin-claimed custom slice renders its plugin's own Avalonia - // control in the value column, at the slice's real position. Null guard: a missing, null-returning, - // or throwing factory degrades to the explicit unsupported row — never a crash, never a silently - // blank row. + // A plugin-claimed slice renders its plugin's Avalonia control in + // the value column. A missing, null-returning, or throwing factory + // degrades to the unsupported row -- never a crash, never silently + // blank. private static Control CreateCustom(DetailField field, string automationId) { if (field.ControlFactory == null) diff --git a/Src/Common/FwAvalonia/DetailHostControl.cs b/Src/Common/FwAvalonia/DetailHostControl.cs index 2bcdeda106..f2a5a54a36 100644 --- a/Src/Common/FwAvalonia/DetailHostControl.cs +++ b/Src/Common/FwAvalonia/DetailHostControl.cs @@ -16,7 +16,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia public sealed class DetailHostControl : AvaloniaHostControlBase { // The splitter (label/value column) width the user dragged, remembered across re-shows for - // THIS host only — deliberately per-instance, never a process-global static. Used only + // THIS host only -- deliberately per-instance, never a process-global static. Used only // as the in-process fallback when the host (RecordEditView) supplies no session-persistence // hooks; the product host routes a PropertyTable LocalSetting through ShowDetail so the width // also survives across SESSIONS, mirroring legacy slice-splitter persistence. @@ -41,7 +41,7 @@ public void ShowDetail(DetailModel detail, IDetailEditContext editContext = null { if (detail == null) throw new ArgumentNullException(nameof(detail)); // Splitter position persists per-HOST across re-shows: this long-lived host owns - // the in-process remembered width, so each window/preview keeps its own — no process-global + // the in-process remembered width, so each window/preview keeps its own -- no process-global // field. When the product host supplies persistence hooks, the read/write chains // through them too, so a width dragged in one session is restored in the next; otherwise it // falls back to the process-only field (e.g. the preview host / headless tests). diff --git a/Src/Common/FwAvalonia/FilterableDropdownSupport.cs b/Src/Common/FwAvalonia/FilterableDropdownSupport.cs index 44612ea58a..4dacb3aefa 100644 --- a/Src/Common/FwAvalonia/FilterableDropdownSupport.cs +++ b/Src/Common/FwAvalonia/FilterableDropdownSupport.cs @@ -18,8 +18,8 @@ namespace SIL.FieldWorks.Common.FwAvalonia /// The filter-box-over-list machinery the detail pickers share: a case-insensitive contains /// filter that swaps a tree out for a flat result list, keyboard highlight movement, the /// pointer-release-over-own-item guard, and the compact/chromeless themes. Three controls draw - /// on it — FwOptionChooser (flat list, optional flyout), FwPosChooser (tree + flyout), - /// and FwFeatureStructureEditor (inline tree) — so the parts they truly hold in common live + /// on it -- FwOptionChooser (flat list, optional flyout), FwPosChooser (tree + flyout), + /// and FwFeatureStructureEditor (inline tree) -- so the parts they truly hold in common live /// here once rather than in each. A static helper rather than a base class: the three differ in /// their content shape, popup hosting, and selection model, so shared state is passed in per call. /// diff --git a/Src/Common/FwAvalonia/FinalizerSafeSynchronizationContext.cs b/Src/Common/FwAvalonia/FinalizerSafeSynchronizationContext.cs index ec2c50e7e4..aa729ee0de 100644 --- a/Src/Common/FwAvalonia/FinalizerSafeSynchronizationContext.cs +++ b/Src/Common/FwAvalonia/FinalizerSafeSynchronizationContext.cs @@ -13,16 +13,16 @@ namespace SIL.FieldWorks.Common.FwAvalonia /// Crash guard for hosting Avalonia inside WinForms. Avalonia's MicroCom COM proxies /// capture the ambient at creation /// (MicroComProxyBase._synchronizationContext) and their FINALIZERS post the native Release - /// back through it. When that post lands after the WinForms marshaling window is gone — - /// project switch, window teardown, shutdown, or simply an idle-time GC afterwards — + /// back through it. When that post lands after the WinForms marshaling window is gone -- + /// project switch, window teardown, shutdown, or simply an idle-time GC afterwards -- /// WindowsFormsSynchronizationContext.Post throws /// on the FINALIZER thread, which terminates the whole process: - /// InvalidOperationException → Control.MarshaledInvoke → BeginInvoke - /// → WindowsFormsSynchronizationContext.Post → MicroCom.Runtime.MicroComProxyBase.Finalize(). + /// InvalidOperationException -> Control.MarshaledInvoke -> BeginInvoke + /// -> WindowsFormsSynchronizationContext.Post -> MicroCom.Runtime.MicroComProxyBase.Finalize(). /// Installed as the UI thread's ambient context BEFORE Avalonia initializes, this wrapper is /// what every proxy captures; it delegates to the real context but swallows POST marshal /// failures whose only victim would be a moot native Release (synchronous Send failures still - /// surface — the caller is waiting on the result). WinForms will not displace it — + /// surface -- the caller is waiting on the result). WinForms will not displace it -- /// InstallIfNeeded only replaces null/base-type contexts, never custom ones. /// public sealed class FinalizerSafeSynchronizationContext : SynchronizationContext @@ -56,7 +56,7 @@ public override void Post(SendOrPostCallback d, object state) } catch (InvalidOperationException e) { - // Marshaling window gone (ObjectDisposedException is a subtype) — for a MicroCom + // Marshaling window gone (ObjectDisposedException is a subtype) -- for a MicroCom // finalizer's native Release the posted work is moot; anything else was collateral. ReportSwallowedPost(d, e); } @@ -69,7 +69,7 @@ public override void Post(SendOrPostCallback d, object state) /// /// True when the callback is a MicroCom finalizer post (the crash class this wrapper exists - /// for) — identified by the callback's declaring type living in the MicroCom runtime. A pin + /// for) -- identified by the callback's declaring type living in the MicroCom runtime. A pin /// test guards this namespace assumption so an Avalonia bump that relocates it fails loudly /// instead of silently reclassifying every finalizer Release as a dropped post. /// @@ -103,7 +103,7 @@ private static void ReportSwallowedPost(SendOrPostCallback d, Exception e) // Send is NOT swallowed: the finalizer rationale above only covers Post (MicroCom proxy // finalizers post their native Release). Send is a synchronous call whose caller is - // waiting on the result — silently skipping the callback would corrupt that caller's + // waiting on the result -- silently skipping the callback would corrupt that caller's // state, so marshal failures surface to it. public override void Send(SendOrPostCallback d, object state) => _inner.Send(d, state); diff --git a/Src/Common/FwAvalonia/FwAvaloniaDensity.cs b/Src/Common/FwAvalonia/FwAvaloniaDensity.cs index fe4305e230..d6a76bb849 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaDensity.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaDensity.cs @@ -41,19 +41,19 @@ public static class FwAvaloniaDensity /// The DETERMINISTIC, GLOBAL checkbox glyph-box size (px), a fixed function of the surface font /// (): the box reads about as tall as a capital /// letter, not the Fluent ~20px box on a 32px-tall layout slot. The single - /// restyles the CheckBox TEMPLATE so the LAYOUT footprint (not just the paint) is this size — so a + /// restyles the CheckBox TEMPLATE so the LAYOUT footprint (not just the paint) is this size -- so a /// checkbox never inflates a browse/list/tree/table row past the text-row height /// ( = 18). NOT a RenderTransform scale (that leaves the layout box /// tall, the inflation the user rejected); a concrete size applied to the box + the inner template grid. public const double CheckboxBoxSize = 14d; /// The gap between a checkbox box and its label text, so the words never butt against the box - /// (the deterministic CheckBox template uses this as the box→label spacing). ~6px reads as a clear gap + /// (the deterministic CheckBox template uses this as the box->label spacing). ~6px reads as a clear gap /// at the surface font size, matching the breathing room a radio button has. public const double CheckboxLabelGap = 6d; /// The DETERMINISTIC, GLOBAL radio-button outer-circle size (px), the radio counterpart of - /// — the same 14px so a radio and a checkbox read at the same density and + /// -- the same 14px so a radio and a checkbox read at the same density and /// neither inflates a row past the text line. The single restyles the /// RadioButton TEMPLATE so the LAYOUT footprint (not just the paint) is this size, exactly as /// does for the checkbox box. @@ -65,7 +65,7 @@ public static class FwAvaloniaDensity /// separator () for the clearest cases. public const double GroupSeparation = 8d; - /// The selected browse/table row fill — the legacy pale blue (XmlBrowseViewBaseVc + /// The selected browse/table row fill -- the legacy pale blue (XmlBrowseViewBaseVc /// kclrBackgroundSelRow 0xFFE6D7 = RGB 215,230,255) rather than the Fluent accent, so the whole /// selected row (including the first column) reads as highlighted like the WinForms browse. public static readonly Avalonia.Media.IBrush SelectedRowBrush = @@ -96,7 +96,7 @@ public static class FwAvaloniaDensity public static readonly Avalonia.Media.IBrush BrowseGridLineBrush = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(0xDC, 0xDC, 0xDC)); - /// The browse table surface fill — plain white like the legacy XMLViews browse, rather + /// The browse table surface fill -- plain white like the legacy XMLViews browse, rather /// than the Fluent panel tint. public static readonly Avalonia.Media.IBrush BrowseBackgroundBrush = Avalonia.Media.Brushes.White; @@ -123,7 +123,7 @@ public static class FwAvaloniaDensity /// The text color for the owned pickers, paired with the concrete /// surface. A single named token (rather than an ad-hoc Brushes.Black at each row/item template) so - /// every owned picker shares one foreground and reads legibly dark-on-light — matching the concrete-brush + /// every owned picker shares one foreground and reads legibly dark-on-light -- matching the concrete-brush /// convention the rest of the dialog stack paints its WinForms-density surfaces with, so it renders the same in the /// runtime host and the headless tests regardless of the OS theme variant. public static readonly Avalonia.Media.IBrush PickerForegroundBrush = @@ -144,4 +144,4 @@ public static class FwAvaloniaDensity /// collapsed control reads as a field-sized box rather than shrinking to its current text. public const double DropdownMinWidth = 160d; } -} \ No newline at end of file +} diff --git a/Src/Common/FwAvalonia/FwAvaloniaPlatform.cs b/Src/Common/FwAvalonia/FwAvaloniaPlatform.cs index acc15643d1..551049119a 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaPlatform.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaPlatform.cs @@ -19,7 +19,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia /// /// Detection is by reflection on the active IWindowingPlatform's assembly (it is /// Avalonia.Headless under the headless platform). This keeps the production FwAvalonia DLL - /// free of any compile-time dependency on Avalonia.Headless — production never loads that assembly, + /// free of any compile-time dependency on Avalonia.Headless -- production never loads that assembly, /// so the probe simply returns false and the real Win32 embed path is unchanged. /// internal static class FwAvaloniaPlatform @@ -29,7 +29,7 @@ internal static class FwAvaloniaPlatform /// /// True when the active Avalonia windowing platform is the headless one. False on the real Win32 /// platform (production) and false if the runtime is not yet initialized or the platform cannot be - /// resolved — i.e. it never claims headless unless it can prove it, so production behavior is safe. + /// resolved -- i.e. it never claims headless unless it can prove it, so production behavior is safe. /// internal static bool IsHeadless { @@ -53,7 +53,7 @@ internal static bool IsHeadless /// /// Makes the WinForms/Avalonia embed (the Win32 HWND reparent in /// WinFormsAvaloniaControlHost.OnHandleCreated) a deliberate no-op when the active platform - /// is HEADLESS, by marking as design-mode — the control's own escape hatch: + /// is HEADLESS, by marking as design-mode -- the control's own escape hatch: /// its handle-created path skips creating the embeddable root, getting the (nonexistent) Win32 top /// level handle, and calling SetParent/AddMessageFilter when DesignMode is true. /// The Avalonia content still constructs and lays out when shown (tests assert logic, not pixels). diff --git a/Src/Common/FwAvalonia/FwAvaloniaRuntime.cs b/Src/Common/FwAvalonia/FwAvaloniaRuntime.cs index 37070c9aad..334473e6f3 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaRuntime.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaRuntime.cs @@ -20,13 +20,13 @@ public static class FwAvaloniaRuntime /// /// Test-only hook that lets a test assembly substitute the used by - /// — without this production DLL referencing Avalonia.Headless. - /// xWorks integration tests that drive the product UI (RecordEditView/RecordBrowseView/…) + /// -- without this production DLL referencing Avalonia.Headless. + /// xWorks integration tests that drive the product UI (RecordEditView/RecordBrowseView/...) /// otherwise initialize the REAL Win32 Avalonia platform process-wide, so any flyout/dialog/popup /// becomes a real on-screen OS window that flashes and can steal keypresses. A test /// [SetUpFixture] sets this to a headless builder before any test runs; production leaves it /// null and behavior is identical to calling directly. - /// Only honored on the first (winning) call — once the runtime is + /// Only honored on the first (winning) call -- once the runtime is /// set up it cannot be re-platformed, so this must be set before the first host is constructed. /// public static Func AppBuilderOverride { get; set; } @@ -39,7 +39,7 @@ public static class FwAvaloniaRuntime /// already-live as "initialized": some test hosts (the /// Avalonia.Headless.NUnit [AvaloniaTestApplication] attribute, e.g. /// FwAvaloniaDialogsTests/FwAvaloniaTests) set up the Avalonia platform themselves, - /// per test session, without ever calling this method — calling SetupWithoutStarting again + /// per test session, without ever calling this method -- calling SetupWithoutStarting again /// on top of that would throw (Avalonia only allows one Setup per process). /// public static void EnsureInitialized() diff --git a/Src/Common/FwAvalonia/FwAvaloniaStrings.cs b/Src/Common/FwAvalonia/FwAvaloniaStrings.cs index ce1118ac44..f907badbe4 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaStrings.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaStrings.cs @@ -6,7 +6,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia { /// /// Localized product-facing strings for the FwAvalonia module. Strings - /// resolve through ResourceManager over FwAvaloniaStrings.resx — the neutral resx is the English + /// resolve through ResourceManager over FwAvaloniaStrings.resx -- the neutral resx is the English /// source of truth and translations ship as satellite assemblies (the FieldWorks .resx /// localization strategy). Automation ids remain nonlocalized constants in code, never resource /// lookups. @@ -62,13 +62,13 @@ public static class FwAvaloniaStrings public static string RedoEditEntry => Text("FwAvalonia.RedoEditEntry"); /// - /// "Undo change to {0}" — field-specific undo label for the fenced lexical-edit session when a + /// "Undo change to {0}" -- field-specific undo label for the fenced lexical-edit session when a /// single field's edit opened it ({0} = the field label). Falls back to /// for the batch/bulk path where no single field applies. /// public static string UndoChangeToFormat => Text("FwAvalonia.UndoChangeToFormat"); - /// "Redo change to {0}" — the redo counterpart of . + /// "Redo change to {0}" -- the redo counterpart of . public static string RedoChangeToFormat => Text("FwAvalonia.RedoChangeToFormat"); public static string LexemeFormRequired => Text("FwAvalonia.LexemeFormRequired"); @@ -78,7 +78,7 @@ public static class FwAvaloniaStrings /// /// Warning shown when a pending lexical edit is rolled back on navigate/close because it fails - /// validation (the edit is not silently lost — the user is told why). {0} is the validation reason(s). + /// validation (the edit is not silently lost -- the user is told why). {0} is the validation reason(s). /// public static string EditDiscardedInvalidFormat => Text("FwAvalonia.EditDiscardedInvalid"); @@ -100,16 +100,16 @@ public static class FwAvaloniaStrings public static string Copy => Text("FwAvalonia.Copy"); - /// "Remove" — reference-vector item context command. + /// "Remove" -- reference-vector item context command. public static string Remove => Text("FwAvalonia.Remove"); - /// "Add item" — reference-vector add-slot launcher name. + /// "Add item" -- reference-vector add-slot launcher name. public static string AddItem => Text("FwAvalonia.AddItem"); - /// "Type to search" — the search-backed add slot's type-ahead watermark. + /// "Type to search" -- the search-backed add slot's type-ahead watermark. public static string SearchPrompt => Text("FwAvalonia.SearchPrompt"); - /// "Add" — confirm button of the multi-select reference-vector add picker; commits the checked set in one undoable step. + /// "Add" -- confirm button of the multi-select reference-vector add picker; commits the checked set in one undoable step. public static string AddSelected => Text("FwAvalonia.AddSelected"); /// Accessible name of the "..." dialog-launcher button. @@ -118,27 +118,27 @@ public static class FwAvaloniaStrings /// Tooltip of a disabled launcher button: no host dialog service. public static string LauncherUnavailable => Text("FwAvalonia.LauncherUnavailable"); - /// "{0} settings" — accessible name of a chooser's hover-revealed settings gear. + /// "{0} settings" -- accessible name of a chooser's hover-revealed settings gear. public static string FieldSettingsFormat => Text("FwAvalonia.FieldSettings"); /// - /// "Edit the {0} list" — label/tooltip of a configure-gear jump derived from the row's - /// possibility list (the legacy chooser dialog's "Edit the … list" link text). + /// "Edit the {0} list" -- label/tooltip of a configure-gear jump derived from the row's + /// possibility list (the legacy chooser dialog's "Edit the ... list" link text). /// public static string EditListFormat => Text("FwAvalonia.EditListFormat"); - /// "Lexeme Form" — first-slice row label (compiled override and authored fallback). + /// "Lexeme Form" -- first-slice row label (compiled override and authored fallback). public static string LexemeFormLabel => Text("FwAvalonia.LexemeFormLabel"); - /// "Morph Type" — first-slice row label (authored fallback). + /// "Morph Type" -- first-slice row label (authored fallback). public static string MorphTypeLabel => Text("FwAvalonia.MorphTypeLabel"); - /// "Gloss" — first-slice row label (authored fallback). + /// "Gloss" -- first-slice row label (authored fallback). public static string GlossLabel => Text("FwAvalonia.GlossLabel"); /// - /// Accessible name / tooltip of the hover-revealed "⋮" field-options button on each field row - /// (opens the Field Visibility / Move Field / Help menu — the affordance that replaced right-click). + /// Accessible name / tooltip of the hover-revealed "..." field-options button on each field row + /// (opens the Field Visibility / Move Field / Help menu -- the affordance that replaced right-click). /// public static string FieldOptionsMenu => Text("FwAvalonia.FieldOptionsMenu"); @@ -172,13 +172,13 @@ public static class FwAvaloniaStrings // override, as MSAGroupBox does), and the inline create affordance is the tree's "More..." item, // reworded to the clearer "Create a new Part of Speech..." for the new view. APPEND-ONLY. - /// "<Not sure>" — the default empty / unspecified Part-of-Speech entry (legacy PopupTreeManager "<Not sure>"). + /// "<Not sure>" -- the default empty / unspecified Part-of-Speech entry (legacy PopupTreeManager "<Not sure>"). public static string PosNotSure => Text("FwAvalonia.Pos.NotSure"); - /// "<Any>" — the unspecified Part-of-Speech entry when the host treats unspecified as "any" (legacy MSAGroupBox NotSureIsAny). + /// "<Any>" -- the unspecified Part-of-Speech entry when the host treats unspecified as "any" (legacy MSAGroupBox NotSureIsAny). public static string PosAny => Text("FwAvalonia.Pos.Any"); - /// "Create a new Part of Speech..." — the inline create affordance at the bottom of the POS tree (legacy "More..." item that launched MasterCategoryListDlg). + /// "Create a new Part of Speech..." -- the inline create affordance at the bottom of the POS tree (legacy "More..." item that launched MasterCategoryListDlg). public static string PosCreateNew => Text("FwAvalonia.Pos.CreateNew"); /// Accessible name of the collapsed Part-of-Speech chooser dropdown. @@ -224,7 +224,7 @@ public static class FwAvaloniaStrings /// /// Accessible name / tooltip of the delete-embedded-object affordance that removes the embedded - /// object (link, picture, footnote, …) under the selection. Any ORC kind is deletable here even + /// object (link, picture, footnote, ...) under the selection. Any ORC kind is deletable here even /// when its insert/edit path lives elsewhere. /// public static string DeleteEmbeddedObject => Text("FwAvalonia.DeleteEmbeddedObject"); diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/AvaloniaDialogHostTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/AvaloniaDialogHostTests.cs index 42aff7773f..a46335037a 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/AvaloniaDialogHostTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/AvaloniaDialogHostTests.cs @@ -94,14 +94,14 @@ public void CompactDialogStyles_Apply_NullControl_DoesNotThrow() /// /// The UI-thread guard (the one cheaply-testable slice of the otherwise desktop-only modal path): /// must fail fast with - /// when the owner is a WinForms whose InvokeRequired is true — i.e. + /// when the owner is a WinForms whose InvokeRequired is true -- i.e. /// the call is on the wrong thread for the owner's message loop. Modal hosting + Avalonia share the /// single WinForms UI thread during coexistence; touching them off that thread is a re-entrancy / /// cross-thread bug, so the guard runs before any windowing. /// /// To make InvokeRequired true deterministically, the owner control's window handle is created /// on a dedicated worker thread (kept alive for the duration), then ShowModal is invoked from this test - /// thread — a different thread than the one that owns the handle. + /// thread -- a different thread than the one that owns the handle. /// [Test] public void ShowModal_OwnerOnAnotherThread_ThrowsInvalidOperation() @@ -145,7 +145,7 @@ public void ShowModal_OwnerOnAnotherThread_ThrowsInvalidOperation() // modal loop (not headless-runnable), so these cover the extracted ApplySizing helper that ShowModal // delegates to: border style, min-size, and the get-hook that seeds the initial (remembered) size. // SANCTIONED EXCEPTION to the no-WinForms-Forms-in-tests rule: these are bare - // `new Form()` property bags — no designer tree, never shown, no message loop — and the subject + // `new Form()` property bags -- no designer tree, never shown, no message loop -- and the subject // under test IS Form property manipulation (frame delta, FixedDialog min-size semantics), which a // fake would untest. App dialogs/designer Forms remain banned; test presenters for those. --- diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/CanonicalJsonTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/CanonicalJsonTests.cs index 1bf1065700..1b18330748 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/CanonicalJsonTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/CanonicalJsonTests.cs @@ -177,8 +177,8 @@ public void EveryViewNodeProperty_SurvivesRoundTrip() } /// - /// User-override-shaped layout XML — label/visibility overrides - /// and a hidden part — imports with the overrides surfaced in the typed IR. + /// User-override-shaped layout XML -- label/visibility overrides + /// and a hidden part -- imports with the overrides surfaced in the typed IR. /// [TestFixture] public class OverrideFixtureImportTests diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailCustomFieldRenderingTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailCustomFieldRenderingTests.cs index eb8c58807f..77d1823574 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailCustomFieldRenderingTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailCustomFieldRenderingTests.cs @@ -21,7 +21,7 @@ namespace FwAvaloniaTests /// A row /// renders its plugin control factory's Avalonia control in-tree in the value column, at the /// slice's real position. The path is guarded: a missing, null-returning, or throwing factory - /// degrades to the explicit unsupported row — never a crash, never a silently blank row. + /// degrades to the explicit unsupported row -- never a crash, never a silently blank row. /// [TestFixture] public class DetailCustomFieldRenderingTests diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditingTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditingTests.cs index 363a60fd5f..c7c4400a98 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditingTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditingTests.cs @@ -50,7 +50,7 @@ public bool TryRemoveReferenceItem(DetailField field, string optionKey) public int CancelCount; /// The text edits actually CAPTURED by a Commit (those staged since the last commit/cancel - /// boundary) — models "commit captures staged, cancel discards" so tests can assert WHICH value was + /// boundary) -- models "commit captures staged, cancel discards" so tests can assert WHICH value was /// committed, not merely that a commit happened. public readonly List<(string Field, string Ws, string Value)> CommittedTextEdits = new List<(string, string, string)>(); @@ -123,7 +123,7 @@ public bool TryDeleteParagraph(DetailField field, int paragraphIndex) public void Commit() { - // Capture everything staged since the last boundary — that is what this commit "writes". + // Capture everything staged since the last boundary -- that is what this commit "writes". for (var i = _stagedBoundary; i < TextEdits.Count; i++) CommittedTextEdits.Add(TextEdits[i]); _stagedBoundary = TextEdits.Count; @@ -132,15 +132,15 @@ public void Commit() public void Cancel() { - // Discard everything staged since the last boundary — a cancelled session writes nothing. + // Discard everything staged since the last boundary -- a cancelled session writes nothing. _stagedBoundary = TextEdits.Count; CancelCount++; } } /// - /// The detail view drives editing through the edit-context seam — staging on - /// text/option change, validation-gated Save, Cancel rollback — with stable automation ids. + /// The detail view drives editing through the edit-context seam -- staging on + /// text/option change, validation-gated Save, Cancel rollback -- with stable automation ids. /// [TestFixture] public class DetailEditingViewTests @@ -284,7 +284,7 @@ public void RichTextChange_StagesThroughTheRichEditContext_AndPreservesRunMetada // DATA-SAFETY: a value flagged lossy (a run carries a // TsString property the model does not round-trip) renders a READ-ONLY editor with the - // not-editable-here tooltip, even though an edit context is supplied — so a keystroke can + // not-editable-here tooltip, even though an edit context is supplied -- so a keystroke can // never silently drop the property. The matching model/composer assertions live in xWorks's // DetailEditContextEditingTests.Compose_RunWithUnsupportedProperty_ComposesReadOnly_*. [AvaloniaTest] @@ -318,7 +318,7 @@ public void RichTextCopy_UsesTheSharedClipboardPayload() var flyout = box.ContextFlyout as MenuFlyout; // The menu also carries the rich-text operations (Link / delete embedded object), - // so Copy is not the sole item — pick it out by header. + // so Copy is not the sole item -- pick it out by header. var copyItem = flyout?.Items.OfType() .FirstOrDefault(i => (string)i.Header == FwAvaloniaStrings.Copy); Assert.That(copyItem, Is.Not.Null); @@ -513,17 +513,16 @@ public void TextField_StagesEditsByWsTag_FallingBackToAbbrevWithoutOne() "tag-less rows keep the abbreviation alias"); // The per-row automation id (DetailFocusMemory's focus-restore key) must - // be unique too, so it uses the same tag-preferred key as edits — abbreviations collide. + // be unique too, so it uses the same tag-preferred key as edits -- abbreviations collide. Assert.That(AutomationProperties.GetAutomationId(boxes[0]), Is.EqualTo("TagField.qaa-x-one"), "a tagged row's automation id keys on the unique IETF tag, not the collidable abbreviation"); Assert.That(AutomationProperties.GetAutomationId(boxes[1]), Is.EqualTo("TagField.du"), "tag-less rows keep the abbreviation-suffixed id"); } - // Voice/sound writing systems: a voice/audio alternative renders as READ-ONLY text (the audio - // filename) with no in-pane player. There are no play/record - // affordances, so the recording can never be corrupted by an edit — full audio editing stays - // in the classic view. + // Voice/sound writing systems: a voice/audio alternative renders as READ-ONLY + // text with no in-pane player, so recording can never be corrupted by an edit + // -- full audio editing stays in the classic view. [AvaloniaTest] public void AudioValue_RendersReadOnlyText_WithNoPlayerAndNoStagedEdit() { @@ -708,7 +707,7 @@ public void VectorItemText_HasATransparentBackground_SoTheWholeItemTakesTheRight } // Bug "removing Publish In items not working": a successful remove stage completes the - // gesture — the callback (which the view wires to its commit/re-show) fires exactly once. + // gesture -- the callback (which the view wires to its commit/re-show) fires exactly once. [AvaloniaTest] public void ReferenceRemove_Success_StagesAndFiresTheGestureCallbackOnce() { @@ -1282,7 +1281,7 @@ public void TryParse_RejectsNonKeys(string input) /// /// GEAR = CONFIGURE: a chooser or reference-vector row whose supporting list /// resolved a list-editor target (a goto ) draws the gear, and - /// clicking it DIRECTLY raises the host's — no flyout, no + /// clicking it DIRECTLY raises the host's -- no flyout, no /// context menu. Option flyouts (single-select chooser click, vector "+") are OPTIONS ONLY: /// they contain zero link items. Rows without a resolvable list editor draw no gear; text /// rows NEVER draw one (the Lexeme Form slice menu is right-click only). @@ -1400,7 +1399,7 @@ public void RowsWithoutAResolvableListEditor_HaveNoGear() new FakeDetailEditContext()); Assert.That(noCallback.HoverAffordances, Is.Empty, "no host bridge, no gear"); - // A vector without links: bars + "+" only — no Settings button at all. + // A vector without links: bars + "+" only -- no Settings button at all. var vectorField = new DetailField("LexEntry/x/#1", "Publish Entry In", "PublishIn", null, DetailFieldKind.ReferenceVector, EditorClassification.Known, "PlainVector", null, HostRouting.Inherit, null, @@ -1414,8 +1413,8 @@ public void RowsWithoutAResolvableListEditor_HaveNoGear() } // Gears never open context menus: the Lexeme Form text row draws NO gear; its - // slice menu (menu="mnuDataTree-LexemeForm") stays on right-click only — the label path in - // the detail view (DetailMenuTests) and the in-string path below are unchanged. + // slice menu (menu="mnuDataTree-LexemeForm") stays on right-click only -- the + // label and in-string paths below are unchanged. [AvaloniaTest] public void TextRows_NeverDrawAGear_TheSliceMenuStaysOnRightClickOnly() { diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditorParityTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditorParityTests.cs index 0fb0ddb6d6..79d129b3ac 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditorParityTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailEditorParityTests.cs @@ -21,7 +21,7 @@ namespace FwAvaloniaTests /// Editor-type parity for the lexical detail view: /// the importer carries an enumComboBox's stringList ids/group onto the node (the metadata /// survives even though the detail view does not render a closed enum combo); - /// FwReferenceVectorField.Dispose detaches every handler it wired (count >0 → 0). + /// FwReferenceVectorField.Dispose detaches every handler it wired (count >0 -> 0). /// [TestFixture] public class DetailEditorParityTests @@ -103,7 +103,7 @@ public void ReferenceVector_Dispose_DetachesEveryHandler() [AvaloniaTest] public void ReferenceVector_ReadOnly_HasNothingToDetach() { - // A read-only vector (no edit context) wires no edit handlers, so its teardown is empty — + // A read-only vector (no edit context) wires no edit handlers, so its teardown is empty -- // Dispose is a safe no-op. var vector = new FwReferenceVectorField(VectorFieldWithItems(), "PublishIn", editContext: null); Assert.That(vector.AttachedHandlerCount, Is.EqualTo(0)); diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailFocusMemoryTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailFocusMemoryTests.cs index 492a5a2f9e..67763d52f3 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailFocusMemoryTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailFocusMemoryTests.cs @@ -18,7 +18,7 @@ namespace FwAvaloniaTests /// /// Focus continuity across detail-view re-shows (14.4 usability): the host replaces the entire view /// after every committed edit, so the focused editor (identified by its stable automation id) - /// and caret must carry over to the rebuilt view — otherwise tabbing out of a field would + /// and caret must carry over to the rebuilt view -- otherwise tabbing out of a field would /// destroy the editor the user just moved into. /// [TestFixture] @@ -182,9 +182,9 @@ public void CaptureAndRestore_CarryScrollOffset_AcrossAViewRebuild() "rebuilding the detail view should keep the user at the same scroll position instead of jumping back to the top"); } - // A single-text-field view whose editor's stable automation id is exactly - // + ".vern" (null AutomationId falls back to StableId; the WS suffix is the WsTag). This lets the - // test reproduce the ghost id ("…@ownerHvo/ghost.vern") and its real successor ("…@newHvo.vern"). + // A single-text-field view's editor automation id is exactly + // + ".vern", reproducing the ghost id + // ("...@ownerHvo/ghost.vern") and real successor ("...@newHvo.vern"). private static DataTree ViewWithEditorId(string stableId) { var field = new DetailField(stableId, "Lexeme Form", "Form", "vernacular", diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailMenuTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailMenuTests.cs index 9f5dd5d7f9..17dbbe3e26 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/DetailMenuTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/DetailMenuTests.cs @@ -155,9 +155,9 @@ private static T FindOrNull(Visual view, string automationId) where T : Visua => view.GetVisualDescendants().OfType() .FirstOrDefault(c => AutomationProperties.GetAutomationId(c) == automationId); - // The field-options "⋮" affordance opens the menu on a click OR keyboard activation; both arrive - // as Button.Click, so raising it exercises the same path the icon does (no hit-test dependence on - // the hover-reveal opacity state). + // The field-options "..." affordance opens the menu on a click OR + // keyboard activation; both arrive as Button.Click, so raising it exercises + // the icon's own path, without depending on hover-reveal opacity. private static void ClickKebab(Button kebab) { kebab.RaiseEvent(new RoutedEventArgs { RoutedEvent = Button.ClickEvent }); @@ -169,7 +169,7 @@ public void FieldMenuButton_OnLabelRow_RaisesTheSliceMenuRequest_WithTheLegacyMe { var (view, requests) = Show(Field("Gloss", DetailFieldKind.Text, menuId: "mnuDataTree-Help")); - // The "⋮" field-options button (which replaced right-click) opens the slice menu. + // The "..." field-options button (which replaced right-click) opens the slice menu. ClickKebab(Find [TestFixture] public class FinalizerSafeSynchronizationContextTests @@ -266,7 +266,7 @@ public void Analyze_SameSide_DoesNotPrompt() /// FwAvaloniaPlatform.IsHeadless resolves Avalonia internals BY STRING NAME (AvaloniaLocator in /// Avalonia.Base; IWindowingPlatform in Avalonia.Controls; AvaloniaLocator.Current + its GetService). /// Unlike a public API, a version bump can relocate these without a compile break, which would leave - /// the reflection returning null forever — silently reporting "not headless" and disabling the + /// the reflection returning null forever -- silently reporting "not headless" and disabling the /// headless-embed no-op for the WHOLE suite. This pins each target against the referenced Avalonia, /// failing loudly (mirroring the MicroCom pin above) so a bump forces an FwAvaloniaPlatform update. /// diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/SliceFactoryTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/SliceFactoryTests.cs index b0ad71f9af..9597a1d59b 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/SliceFactoryTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/SliceFactoryTests.cs @@ -12,12 +12,12 @@ namespace FwAvaloniaTests { /// - /// The shared →control dispatch both the detail-pane detail view + /// The shared ->control dispatch both the detail-pane detail view /// and the browse in-cell editor route through. These pin that one switch produces the right control /// per surviving kind (Text / Chooser / ReferenceVector / Literal / Custom / Unsupported), and that - /// the all-nullable serves both hosts — the browse cell + /// the all-nullable serves both hosts -- the browse cell /// passes null menu/link callbacks and suppresses the WS-abbreviation gutter while the detail pane - /// passes the full set — without either host hand-rolling its own dispatch. + /// passes the full set -- without either host hand-rolling its own dispatch. /// [TestFixture] public class SliceFactoryTests @@ -51,7 +51,7 @@ public void UnsupportedKind_BuildsUnsupportedTextBlock() => Assert.That(SliceFactory.Build(Field(DetailFieldKind.Unsupported), "Auto.Id", null), Is.InstanceOf()); - // Literal: a static text renderer (legacy MessageSlice) — the label/message text is the + // Literal: a static text renderer (legacy MessageSlice) -- the label/message text is the // content, no editable value column. [AvaloniaTest] public void LiteralKind_BuildsStaticTextBlock_ShowingTheLabel() @@ -85,7 +85,7 @@ public void CustomKind_FactoryControl_IsReturned() public void BrowseStyleContext_TextField_SuppressesWritingSystemAbbreviation() { // The dense browse cell context (null callbacks, no abbreviation gutter) must still build a - // usable text field — the same control the detail pane gets, just configured for the cell. + // usable text field -- the same control the detail pane gets, just configured for the cell. var browseContext = new SliceFactoryContext( editContext: null, writingSystemFocused: _ => { }, showWritingSystemAbbreviation: false); Assert.That(SliceFactory.Build(Field(DetailFieldKind.Text), "Auto.Id", browseContext), diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/StructuredTextEdgeCaseTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/StructuredTextEdgeCaseTests.cs index 94b8b4a59d..971473202d 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaTests/StructuredTextEdgeCaseTests.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/StructuredTextEdgeCaseTests.cs @@ -24,7 +24,7 @@ namespace FwAvaloniaTests /// Edge cases for the owned multi-paragraph structured-text (StText) editor /// (): an empty StText (zero / one paragraph), the /// only-paragraph-cannot-delete invariant, RTL + complex-script (Khmer) content round-tripping, - /// rapid interleaved insert/delete, ORC/lossy interleaving, and clear-style → Normal mapping. + /// rapid interleaved insert/delete, ORC/lossy interleaving, and clear-style -> Normal mapping. /// These pin the corners the happy-path unit tests don't reach. The view side stays LCModel-free /// (a recording fake context); the matching real-LCModel round-trip assertions live in /// StructuredTextAdapterTests / StructuredTextWorkflowTests. @@ -108,7 +108,7 @@ public void EmptyStText_FirstKeystrokeMaterializesParagraph_StagesAtIndex0() var context = new FakeDetailEditContext(); var (control, _) = Show(field, context, gestureCompleted: () => { }); - // Typing into the lone empty row stages a text edit against paragraph index 0 — the seam the + // Typing into the lone empty row stages a text edit against paragraph index 0 -- the seam the // composer's text setter turns into "create paragraphs up to the index" against a null StText. Boxes(control)[0].Text = "first words"; Dispatcher.UIThread.RunJobs(); @@ -183,9 +183,10 @@ public void RtlAndComplexScript_ParagraphStagesAndRoundTrips_LosslessRuns() [AvaloniaTest] public void RapidInterleavedInsertDelete_DoNotCrashOrOrphanUndo() { - // Each structural gesture completes immediately (the gestureCompleted callback the host wires - // to its one validation-gated commit + re-show). Interleaving them rapidly must remain - // one-completed-gesture-per-action — no missed or doubled completion (which would orphan undo). + // Each structural gesture completes via gestureCompleted (its + // validation-gated commit + re-show). Interleaving must stay + // one-completed-gesture-per-action -- missing or doubled completion + // orphans undo. var field = Field(new List { Para("Alpha."), Para("Beta."), Para("Gamma.") }); var context = new FakeDetailEditContext(); var gestures = 0; @@ -194,7 +195,7 @@ public void RapidInterleavedInsertDelete_DoNotCrashOrOrphanUndo() Button AddButton(int i) => control.GetVisualDescendants().OfType