Skip to content

LT-22691: Answer the allomorph, variant and relation menus natively - #1212

Merged
mark-sil merged 2 commits into
mainfrom
LT-22691s
Oct 28, 2026
Merged

mark-sil merged 2 commits into
mainfrom
LT-22691s

Conversation

@mark-sil

@mark-sil mark-sil commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Start here: Src/xWorks/Avalonia/Hosting/RecordEditView.LexiconMenus.cs -- the new answerers (Merge, Delete Reference, Add/Replace Reference, Edit Reference Set Details, Swap Allomorph) at the end of the file, and CommandTargetOf above DeleteItem.

In Lexicon Edit's Avalonia detail view, the menus of an allomorph, an affix process, a variant's Variant Form row and a lexical-relation row now display and run without the hidden WinForms DataTree. Five ids switch over (PinnedOwnedIds 48 -> 53). Three shared rules carry the logic, and the legacy slices and launchers now call them: DetailObjectMerge, VariantBackReferences and LexicalRelationEditing.

Does legacy behave differently now? No. The merge dialog, the relation choosers, Reference Set Details and the delete confirmation are the same WinForms dialogs, and the shared rules keep the LT-13729, LT-14926 and LT-10987 relation rules, each with a test. mnuDataTree-AlternateForm and its four commands are removed: no row in either UI shows them.

One change reaches beyond these menus. After a command that focuses an object (merge, convert, insert, move, media), the re-show restored the old scroll offset, so the focused row could sit off-screen. DetailFocusMemory.RestoreAfterLayout now brings that row into view.

Where to look

  • The Variant Form row's object is the variant's lexeme form, yet Delete Variant and Delete Reference act on the variant entry and its ref -- VariantFormRow_DeleteActsOnTheVariantEntry_AndDeleteReferenceNeedsItsRef, DeleteReference_OnAVariantLinkedTwice_RemovesTheRefOfTheClickedGroup.
  • Delete Relation by mapping type and side -- LexicalRelationEditingTests.
  • The scroll change -- RequestedObjectFocus_ScrollsToTheObjectsRow_InsteadOfKeepingTheOffset.
  • 20 strings move to FdoUiStrings (keys and English unchanged) so the FdoUi rules can use them.

Deliberately not here

  • Sense-level relation rows never compose in the New UI (LT-22846); their menus are proved headlessly.
  • Sense merge, split, demote and promote: PR E.

Verification. Merges into main, no dependency. build.ps1 -CommentHygiene -TokenHygiene is clean. Targeted runs passed: 261 tests across the rule, menu, integrity and focus fixtures, then 66, 58 and 137 after later fixes; the full suite was not run locally. A manual pass in Sena 3 passed in both UIs, with the relation menus tested on entry Cross References rows.

Next: approve, or flag a divergence below to revisit.


Reading this a year from now -- start here

Stage 2 PR D2 of LT-22691, which retires the hidden WinForms DataTree command adapter from the Avalonia detail view one context-menu id at a time. The predicate-owned LexiconMenuAuthority owns an id once every leaf under it has a native answerer, and LexiconMenuAuthorityTests.PinnedOwnedIds pins the exact owned list. This PR adds answerers for DataTreeMerge, DataTreeDeleteReference, DataTreeAddReference, DataTreeEdit, SwapAllomorphWithLexeme and ConvertAllomorph, and the five ids they complete switch over. No working documents were kept on the branch; the reasoning is below.

Deliberate rules
  • A variant's rows act on the variant entry. The Variant Form row's object is the variant's lexeme form, but the variant is linked to the shown entry through its entry ref, not owned by it. VariantBackReferences.CommandTarget swaps in the entry that owns the row's object when that is an entry other than the shown one. That is the rule behind Slice.GetObjectForMenusToOperateOn, which now gets it from VariantBackReferences.BackReferencedEntry. Delete Variant deletes the variant entry; Delete Reference removes the shown entry from the variant's ref and the ref from the variant.
  • A variant linked twice. Delete Reference takes the ref whose rows follow the clicked Form row in the shown model, as Slice.HandleDeleteReferenceCommand takes NextSlice.
  • Delete Relation by type and side (LexicalRelationEditing.DeleteKind). A collection or sequence member, and a tree's part, leave the relation; a pair, a unidirectional relation and a tree seen from its whole are deleted. The confirmation note says the other members stay related, or that the whole tree goes.
  • Replace Reference. A pair replaces its other member in one step, so the pair never drops to one member and deletes itself (LT-13729). A tree's part moves to the relation of the chosen whole: an existing relation of that type with that whole, else its own relation retargeted when it is the only part (LT-14926), else a new relation; choosing the current whole changes nothing (LT-10987).
  • Chooser kind. Sense types open the sense chooser, entry-or-sense types the entry-or-sense chooser (both the Avalonia link dialog), entry types the WinForms entry chooser, as the legacy launchers do.
  • Merge. The candidates and the dialog come from CmObjectUi.MergeUnderlyingObject. Its new overload returns the survivor; the legacy overload still publishes MasterRefresh afterwards, the native path re-shows and focuses the survivor instead. Whether a native sense merge needs MasterRefresh (LT-22155) is PR E's call.
  • Strings. LexicalRelationEditing and VariantBackReferences live in FdoUi, which LexEdDll and DetailControls both reference, so their 20 strings move to FdoUiStrings from LexEdStrings (18) and DetailControlsStrings (ksUndoDeleteRef, ksRedoDeleteRef). LexEdDll reaches them through LexicalRelationEditing's public members, since FdoUiStrings is internal.
Avalonia vs legacy divergences
Where Legacy New UI
Swap Allomorph with Lexeme Form, entry without a lexeme form Enabled; throws when used Disabled
Delete Reference on a row whose variant has no ref to the shown entry Enabled, does nothing Disabled, with "(cannot delete this)"
After Merge MasterRefresh; the clicked slice is gone Re-show, focus on the survivor's first editor row
After a command that focuses an object (merge, convert, insert, move, media) The focused slice scrolls into view Now the same; until this PR the old scroll offset won
Sense-level Lexical Relations rows Shown Not composed (LT-22846)
Decisions, and why
  • One PR, not D2a (merge, delete-reference) and D2b (relations). The relation work is the bulk, but it ships with the ids it completes, and the engines are independent of each other.
  • Reuse the WinForms dialogs through seams. MergeSurvivor, RelationTargetChosen, RelationDetailsEdited and RelationDeleteConfirmation replace the dialogs in tests, as DeleteConfirmation and ConversionLossAccepted do, so every native command runs headlessly.
  • Remove mnuDataTree-AlternateForm rather than own it. WinForms takes a row's menu only from the slice or the part ref that places it, never from a <seq> node, and a headless compose of entries with and without allomorphs shows no Avalonia row binding it. A configuration sweep found no other menu referenced only that way.
  • Keep mnuDataTree-CmMedia. Its media part is not in any shipped parts file, but PartGenerator generates parts at runtime, so WinForms may reach it (LT-22772).
  • Swap Allomorph without a dialog. Unlike Swap Lexeme Form with Allomorph, the row already names the allomorph.
Paths not taken
  • Re-pointing the composer's chip removal (DetailComposer.TryRemoveLexicalRelationTarget) at LexicalRelationEditing.Delete. A chip removal is a different gesture from these menus; left as is.
  • Guarding an empty relation in ReplacePairPartner and Chooser. No row composes for one, and the legacy code never checked.
  • A DisplayParent on the tree-branches and unidirectional launchers. They take the shown object as the relation's first target, which is exact for every relation a row can show.
Evidence
Claim Test
The owned list grows by exactly the five ids OwnsExactlyThePinnedIds_AndAnswersEveryLeafOfEachOne
Owned menus render what the adapter path rendered OwnedMenu_RendersWhatTheAdapterPathRendered_OnEveryLexiconRowThatBindsOne
No owned menu asks the mediator or builds the hidden tree (41 rows) OwnedMenus_AreBuiltWithoutTheAdapterOrTheMediator
Relation rows survive the host's own compose RelationRows_AreShownByTheHost_OnTheEntryAndItsSense
Merge rule: lexeme form, subsense, required vector DetailObjectMergeTests (6)
Variant rows act on the variant entry and its ref VariantBackReferencesTests (4), DeleteVariant_DeletesTheVariantEntry_WhenTheUserConfirms, DeleteReference_*
Relation rules: delete kind, note, chooser, pair and tree replace, details LexicalRelationEditingTests (9), DeleteRelation_*, AddReference_*, ReplaceReference_*, EditReferenceDetails_*
Shared rules stay UI-free DetailRulesBoundaryTests

Nineteen new tests in FdoUiTests, seventeen in xWorksTests, one in FwAvaloniaTests. Manual: a pass in Sena 3 covering allomorph merge, swap and convert, variant Delete Reference and Delete Variant, and relation Add, Replace, Edit Reference Set Details and Delete on entry Cross References rows (relations created in legacy for the pair and tree cases), plus a legacy spot-check. All passed.

Preflight review details

Code Review Summary

Branch: claude/lt-22691s-9d3fff (pushed as LT-22691s)
Base: main (merge base 173a803)
Date: 2026-10-28
Review model: Claude Opus 5.5
Files changed: 37 (36 in commit 1078490, plus one in-review fix)

Overview

Stage 2 PR D2 of LT-22691: the Lexicon detail view's predicate-owned LexiconMenuAuthority
gains native answerers for merge (E6's merge half), variant delete-reference (E10) and the
lexical-relation commands (E12), plus Swap Allomorph with Lexeme Form and Convert Allomorph.
That completes mnuDataTree-Allomorph, -AffixProcess, -VariantForm,
-DeleteAddLexReference and -DeleteReplaceLexReference, which no longer build the hidden
WinForms DataTree. The rules both views need are control-free in Src/FdoUi/DetailRules
(DetailObjectMerge, VariantBackReferences, LexicalRelationEditing) and the WinForms
slices and launchers call them; the WinForms dialogs (merge, choosers, details, delete
confirmation) are reused behind seams. The unreachable mnuDataTree-AlternateForm and its
four commands are removed.

The analysis found no Critical issue. A manual test exposed one defect, fixed before commit:
after a command that focuses an object the re-show restored the old scroll offset, so the
focused row could sit off-screen; DetailFocusMemory.RestoreAfterLayout now brings it into
view.

Contract/API Changes

  • LexReferenceMultiSlice (LexEdDll): public DeleteFromReference(ILexReference) and
    DeleteReference(ILexReference) replaced by one DeleteRelation(ILexReference); all six
    in-repo callers (the relation slices) updated.
  • CmObjectUi (FdoUi): added ConfirmDelete(ITsString note) and
    MergeUnderlyingObject(bool, Func<IReadOnlyList<DummyCmObject>, int>) (returns the
    survivor hvo); the existing overloads keep their behaviour.
  • Resources: 20 keys move to FdoUiStrings (18 from LexEdStrings, 2 from
    DetailControlsStrings), English text unchanged.
  • Configuration: mnuDataTree-AlternateForm and CmdDataTree-{MoveUp,MoveDown,Merge,Delete}-AlternateForm
    removed; the menu= on LexEntry-Detail-AlternateForms's seq node removed.
  • DetailFocusMemory.RestoreAfterLayout: an honoured object-focus request now wins over the
    remembered scroll offset.

Findings

Critical - Must address before merge

None.

Important - Should address before merge

  • Moved strings lose their existing translations: the 20 keys leave LexEdStrings and
    DetailControlsStrings, so Crowdin translations do not follow and the FdoUiStrings entries
    start untranslated.
    (author: as after PR B; once the PR merges, the author tells Jason to
    approve them in Crowdin)

Minor - Consider

  • Public LexEdDll methods replaced (DeleteFromReference, DeleteReference ->
    DeleteRelation).
    (author: fine as is; all callers in-repo, no reviewer note needed)
  • Unused using System.Collections.Generic; in LexReferencePairLauncher.cs
    (fixed during review: removed)
  • Relation delete confirmation now goes through CmObjectUi.ConfirmDelete, which
    consults CanDelete (no override for a relation); the wait cursor covers only the
    confirmation.
    (author: fine as is)
  • ReplacePairPartner/Chooser read TargetsRS[0] without a count check.
    (author: fine as is; no row composes for an empty relation, WinForms did the same)
  • Composer chip removal (TryRemoveLexicalRelationTarget) keeps its own remove
    logic.
    (author: fine as is; a different gesture, outside these menus)
  • Tree-branches and unidirectional launchers take the shown object as
    TargetsRS[0].
    (author: fine as is; only wrong for an unreachable empty relation)

Required Validation / Evidence

  • ./build.ps1 -CommentHygiene -TokenHygiene: clean, including after the in-review fix.
  • Targeted ./test.ps1 -TestFilter runs, all green: 261 tests (the three new rule fixtures,
    DetailRulesBoundaryTests, LexiconMenuAuthorityTests, LexiconMenuExecutionTests,
    DetailObjectCommandExecutionTests, WindowConfigurationIntegrityTests,
    DataTreeMoveReachabilityTests, DetailMenuRequestTests, DetailObjectInsertionTests,
    DetailFocusMemoryTests, DetailEditorParityTests, DetailObjectDeletionTests,
    DetailSequenceMoveTests, MediaFileInsertionTests, LexReferenceTreeRootLauncherTests);
    then 66, 58 and 137 after the later fixes.
  • /code-review high: 5 findings, 1 fixed (tie-break test for a variant linked twice), 4
    skipped and confirmed here as Minor 3-6.
  • Manual testing by the author in Sena 3, New UI and Legacy: allomorph merge, swap and convert;
    variant Delete Reference and Delete Variant; relation Add, Replace, Edit Reference Set
    Details and Delete (with notes) on entry Cross References rows; the Legacy spot-check. All
    passed after the scroll fix.
  • Not run: the full test suite locally (CI runs it). Sense-level relation rows are not
    testable by hand: they never compose in the New UI (LT-22846).

Positive Observations

  • Shared rules are control-free and guarded by DetailRulesBoundaryTests; each has its own
    test file, and the WinForms path now calls the same code, so the two views cannot drift.
  • PinnedOwnedIds records exactly which menus switched over; adapter equivalence holds on
    every Lexicon row binding an owned id, and the no-mediator spy covers the new messages.
  • The tree-whole replacement keeps the LT-14926 and LT-10987 rules, the pair replacement the
    LT-13729 one, each with a test.
  • Every new mutation is one undoable step with its own Undo text, and dialogs sit behind seams
    the tests replace.

Interview Notes

  • Localization: once the PR merges, the author tells Jason to approve the moved strings; no
    question to Jason beforehand.
  • All Minor findings: author accepted them as is, except the unused using, fixed.
  • Most complex part (the Variant Form row's commands act on the variant entry and its ref, not
    the row's lexeme form): the explanation was drafted by the review model at the author's
    request, and the author confirmed it matches their understanding. Summary: the row shows
    another entry inside the shown one; its menu speaks about that variant as a whole, and the
    variant is linked through an entry ref, not owned, so the commands act on the variant entry
    and the ref. This is the WinForms rule (Slice.GetObjectForMenusToOperateOn,
    HandleDeleteReferenceCommand), now shared as VariantBackReferences; for a variant linked
    twice the native path takes the ref whose rows follow the clicked Form row, as NextSlice
    did.
  • Context for reviewers: sense-level relation rows untestable by hand (LT-22846); native merge
    does not publish MasterRefresh (the sense case, LT-22155, is left to PR E); the scroll fix
    reaches every object-focus gesture (merge, convert, insert, move, media insert); Swap
    Allomorph is disabled without a lexeme form, and Delete Reference is disabled with the
    cannot-delete note when the row has no ref to the shown entry.

In-Review Quality Check

  • Removed the unused using in LexReferencePairLauncher.cs; ./build.ps1 -CommentHygiene -TokenHygiene clean afterwards.

Suggested Review Focus

  • VariantBackReferences and its use in RecordEditView.DeleteItem/MergeItem: the row
    object vs command target split on variant rows.
  • LexicalRelationEditing.DeleteKind and ReplaceTreeWhole: the per-mapping, per-side
    behaviour against the WinForms slices it replaces.
  • DetailFocusMemory.RestoreAfterLayout: the scroll offset now yields to the focused
    object's row.
  • CmObjectUi.MergeUnderlyingObject(bool, chooser): the WinForms overload still publishes
    MasterRefresh, the native caller does not.

🤖 Generated with Claude Code


This change is Reviewable

The Lexicon detail view now answers the item menus of allomorphs and
affix processes, a variant's form row and the lexical-relation rows
without the hidden DataTree. Until now their Merge, Delete Reference,
Add and Replace Reference, Edit Reference Set Details, Swap and Convert
leaves were built by the WinForms adapter.

After such a command the view now scrolls to the row it puts the
keyboard on. Until now the re-show restored the old scroll position,
so the focused row could sit off-screen.

The rules both views need live in Src/FdoUi/DetailRules and the
WinForms slices and launchers call them; the strings they share move
to FdoUiStrings.

The menu ids this changes:
* mnuDataTree-Allomorph
* mnuDataTree-AffixProcess
* mnuDataTree-VariantForm
* mnuDataTree-DeleteAddLexReference
* mnuDataTree-DeleteReplaceLexReference
* mnuDataTree-AlternateForm (removed with its four commands: no row in
  either view shows it)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   11m 53s ⏱️ - 2m 22s
6 905 tests +37  6 820 ✅ +37  85 💤 ±0  0 ❌ ±0 
6 914 runs  +37  6 829 ✅ +37  85 💤 ±0  0 ❌ ±0 

Results for commit 8e32a0e. ± Comparison against base commit 5cbbfe8.

♻️ This comment has been updated with latest results.

@codecov-commenter

codecov-commenter commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.03554% with 235 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.29%. Comparing base (173a803) to head (8e32a0e).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...ks/Avalonia/Hosting/RecordEditView.LexiconMenus.cs 64.41% 41 Missing and 38 partials ⚠️
Src/FdoUi/DetailRules/LexicalRelationEditing.cs 83.51% 14 Missing and 16 partials ⚠️
Src/FdoUi/FdoUiCore.cs 41.93% 12 Missing and 6 partials ⚠️
Src/LexText/Lexicon/LexReferenceMultiSlice.cs 0.00% 17 Missing and 1 partial ⚠️
Src/LexText/Lexicon/LexReferencePairLauncher.cs 0.00% 10 Missing and 3 partials ⚠️
...rc/LexText/Lexicon/LexReferenceTreeRootLauncher.cs 7.14% 10 Missing and 3 partials ⚠️
...exText/Lexicon/LexReferenceTreeBranchesLauncher.cs 0.00% 7 Missing and 4 partials ⚠️
...Text/Lexicon/LexReferenceUnidirectionalLauncher.cs 0.00% 7 Missing and 4 partials ⚠️
.../LexText/Lexicon/LexReferenceCollectionLauncher.cs 0.00% 7 Missing and 3 partials ⚠️
...rc/LexText/Lexicon/LexReferenceSequenceLauncher.cs 0.00% 7 Missing and 3 partials ⚠️
... and 11 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1212      +/-   ##
==========================================
+ Coverage   40.00%   40.29%   +0.28%     
==========================================
  Files        1544     1547       +3     
  Lines      355308   355556     +248     
  Branches    41357    41446      +89     
==========================================
+ Hits       142141   143260    +1119     
+ Misses     183833   182976     -857     
+ Partials    29334    29320      -14     
Files with missing lines Coverage Δ
Src/FdoUi/DetailRules/DetailObjectMerge.cs 100.00% <100.00%> (ø)
Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs 73.24% <90.00%> (+0.27%) ⬆️
Src/LexText/Lexicon/LexReferenceCollectionSlice.cs 44.00% <0.00%> (ø)
Src/LexText/Lexicon/LexReferencePairSlice.cs 46.15% <0.00%> (+46.15%) ⬆️
Src/LexText/Lexicon/LexReferenceSequenceSlice.cs 0.00% <0.00%> (ø)
...c/LexText/Lexicon/LexReferenceTreeBranchesSlice.cs 0.00% <0.00%> (ø)
Src/LexText/Lexicon/LexReferenceTreeRootSlice.cs 18.75% <0.00%> (+18.75%) ⬆️
...LexText/Lexicon/LexReferenceUnidirectionalSlice.cs 0.00% <0.00%> (ø)
Src/Common/Controls/DetailControls/Slice.cs 60.69% <60.00%> (+1.62%) ⬆️
...c/LexText/Lexicon/RecordReferenceVectorLauncher.cs 0.00% <0.00%> (ø)
... and 12 more

... and 28 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@thejambi

Copy link
Copy Markdown
Contributor

AddOrReplace returns the relation when nothing changed (LexicalRelationEditing.cs:316, :385). The doc comment says it returns null when nothing changed. When a tree part picks the whole it already belongs to (LT-10987), ReplaceTreeWhole returns relation unchanged, and AddOrReplace passes it on. The native AddOrReplaceReference then does an extra full re-show and moves focus. Nothing else goes wrong: liblcm only puts a unit of work on the undo stack when it has a data change, so no blank Undo entry is left. I'd return null before the unit of work opens and change the assertion at LexicalRelationEditingTests.cs:201 to Is.Null:

else if (IsTree(mapping) && !IsFirstTarget(relation, current))
{
    if (relation.TargetsRS[0] == chosen)
        return null; // LT-10987: already this whole
    ...

Otherwise, reword the comment to name the exception.

@thejambi thejambi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm: aside from comment about suggested change in LexicalRelationEditing.cs

@thejambi reviewed 36 files and all commit messages, and made 1 comment.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on mark-sil).

Replace Reference now does nothing when the user picks the tree whole or
pair partner the relation already gives the shown entry or sense. Until
now the view still re-showed and moved the keyboard, though the relation
did not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mark-sil

Copy link
Copy Markdown
Contributor Author

AddOrReplace returns the relation when nothing changed (LexicalRelationEditing.cs)

Agreed, fixed in 8e32a0e: AddOrReplace now returns null before opening the unit of work when the choice is already the part's whole (LT-10987). The pair case had the same gap, choosing the current partner again, so it returns null there too. The test asserts Is.Null for both.

@mark-sil mark-sil left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mark-sil made 1 comment.
Reviewable status: 34 of 36 files reviewed, 1 unresolved discussion (waiting on thejambi).


a discussion (no related file):

Previously, thejambi (Zach B) wrote…

AddOrReplace returns the relation when nothing changed (LexicalRelationEditing.cs:316, :385). The doc comment says it returns null when nothing changed. When a tree part picks the whole it already belongs to (LT-10987), ReplaceTreeWhole returns relation unchanged, and AddOrReplace passes it on. The native AddOrReplaceReference then does an extra full re-show and moves focus. Nothing else goes wrong: liblcm only puts a unit of work on the undo stack when it has a data change, so no blank Undo entry is left. I'd return null before the unit of work opens and change the assertion at LexicalRelationEditingTests.cs:201 to Is.Null:

else if (IsTree(mapping) && !IsFirstTarget(relation, current))
{
    if (relation.TargetsRS[0] == chosen)
        return null; // LT-10987: already this whole
    ...

Otherwise, reword the comment to name the exception.

Done.

@mark-sil
mark-sil merged commit 10f08c9 into main Oct 28, 2026
8 of 9 checks passed
@mark-sil
mark-sil deleted the LT-22691s branch October 28, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants