Skip to content

LT-22717: Stop the Complex Concordance pattern builder crashing on a bypassing edit - #1088

Draft
johnml1135 wants to merge 8 commits into
mainfrom
fix/complex-conc-pattern-crash
Draft

LT-22717: Stop the Complex Concordance pattern builder crashing on a bypassing edit#1088
johnml1135 wants to merge 8 commits into
mainfrom
fix/complex-conc-pattern-crash

Conversation

@johnml1135

@johnml1135 johnml1135 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The Complex Concordance pattern builder no longer takes FLEx down. Any edit that reached the view without going through the keystroke filter — IME composition on a vernacular keyboard, or dragging text into the pane — fell through to VwBaseVc.UpdateProp and threw an unhandled NotImplementedException, losing whatever was in the window.

The question on opening this is why four layers when one of them fixes the crash. Because they do different jobs, established by ablation rather than assumed. UpdateProp stops the throw. ktptEditable stops a different failure: with UpdateProp alone absorbing the edit, the model reads original while the view displays HACKEDForm: original until something forces a redraw. ReadOnlyView stops neither — its only value is closing the IME and keyboard-controller registration channel, which is the originally reported trigger and remains unverified against a real IME. The review worth your time is whether the read-only rootsite costs anything in a pane users navigate by keyboard.

Where to look

  • ReadOnlyView = true forces AcceptsReturn false. AcceptsTab was already false, Designer-set, so Tab navigation is unchanged; Return was already swallowed by OnKeyPress, so only its disposal point moves. Pinned by test, reasoned rather than verified for the hosting case.
  • SetNotEditable does not persist across AddProp calls — it must be re-asserted before each one. Several markings were only accidentally correct before this was found.
  • The zero-width-space boundary run in PatternVcBase had no editability marking at all. That fix lands in the shared base and benefits the rule formula editor too.
  • Insert and delete must still work: OnKeyDown raises RemoveItemsRequested, pinned by test.
  • PatternView is shared. This branch and LT-22710 both add an identical AllowDisplaySelection override and will conflict on that line; the resolution is mechanical, either side.

Deliberately not here

  • Live IME and drag-and-drop verification. The mechanism is proven by test; the user path is not.
  • PatternEditingHelper.CanCut/CanPaste, which become dead once LT-22710 also lands. Left to whoever merges second.
  • ConstChartVc, a suspected corruption-class defect in a third view constructor. Separate investigation.

Verification. ComplexConcPatternVcDirectEditTests 19/19, ITextDllTests 226 passed / 1 pre-existing skip, MorphologyEditorDllTests 7/7, LexTextControlsTests 353 passed / 3 pre-existing skips. Draft pending the LT-22710 merge order.


Reading this a year from now — start here

The investigation notes, the seed probe and the architecture review were deliberately deleted from this branch rather than merged; their content is synthesised into these sections.

The short version: ComplexConcControl and the phonological rule formula editor are the only two consumers of PatternView, and the only two subclasses of PatternVcBase. Both are chooser-driven surfaces where the user is never meant to type. Both lacked a structural guarantee of that, and each expressed the failure differently — the rule editor renamed shared project data, this one crashed — because the rule editor binds real domain fields and this one does not.

Why it crashes here but corrupts there

ComplexConcPatternVc renders synthetic content: ComplexConcPatternSda serves in-memory values against negative sentinel ids, and a grep for AddStringAltMember in that file returns zero. So no edit can rename a shared phoneme, natural class or part of speech — verified additionally by tests asserting a real IPartOfSpeech and ICmPossibility are untouched after an edit attempt.

RuleFormulaVcBase has always overridden UpdateProp, so its edits were absorbed rather than throwing — and landed on real Name and Abbreviation fields. Same missing invariant, opposite symptom. Loud here, silent there.

Decisions, and why

Four layers, each doing separate work. UpdateProp prevents the throw. ktptEditable prevents the edit reaching the point where the display and the model diverge. ReadOnlyView closes the input-channel registration. AllowDisplaySelection restores the selection highlight that a read-only rootsite suppresses, which a chooser-driven pane needs so the user can see what insert and delete will act on.

ReadOnlyView was kept despite not fixing the crash. It is the only thing that addresses the originally reported trigger, and dropping it to avoid a one-line merge conflict would trade a real protection for a trivial convenience. The PR says plainly that it does not prevent the crash, so nobody later mistakes it for the fix.

The boundary-run marking went into PatternVcBase, not into this subclass, because the gap belongs to the shared base and the sibling editor has it too.

Surprising findings

The test helper was inert. MakeSelOnChild passed its tag into MakeTextSelInObj with fWholeObj: true, where the documented contract says the end arguments are unused. Nine of eleven "fragment angle" tests were constructing one identical selection anchored at the left bracket glyph. Selecting ktagForm and ktagGloss on the same node returned byte-identical results. Rebuilt on MakeTextSelection, every fragment is now genuinely targeted and a standing test proves three tags on one node give three distinct selections.

SetNotEditable does not persist across AddProp calls. Found only because fixing the helper made the difference observable. Several markings had been correct by accident, and the feature lines and multi-line pile hooks were not protected at all.

The zero-width-space boundary run was never marked, for either subclass. It was assessed as harmless by reasoning; mutation testing showed it is exactly why UpdateProp is load-bearing rather than a backstop for the OR and word-boundary fragments.

A Tab regression was hypothesised and disproved. AcceptsTab was already false, set by the Designer independently of ReadOnlyView. The real change is AcceptsReturn.

Evidence

Ablation on the 18-test suite, run rather than argued:

Configuration Result
UpdateProp removed, ktptEditable complete 18/18 pass
UpdateProp intact, ktptEditable neutralised 16/18 — only the IsEditable assertions fail
UpdateProp removed, ktptEditable complete, boundary run unmarked OR and word-boundary fragments throw
ktptEditable neutralised, direct probe model original, display HACKEDForm: original

All eleven fragments — Type, Form, Gloss, Category, Entry, Infl, Tag, OR, word boundary, min, max — crash against unfixed code, each individually targeted after the helper fix.

Deferred, and what would unblock it
  • Live IME verification. Needs a running FLEx with an active IME. The mechanism is proven; the user path is inferred. A field report of the sibling defect exists but is unattributed to a version.
  • ConstChartVc. ApplyFormatting does vwenv.Props = ttp with a format-only property set, which the IVwEnv.Props contract describes as applied in one operation — apparently displacing the cell-level ktptEditable that MakeCellsMethod set, immediately before a real shared ICmPossibility is bound via AddStringAltMember. The Constituent Chart body is editable (ForEditing = true). That is the rule editor's corruption shape in a third view constructor. Unconfirmed: building a valid multi-level selection through the chart's custom cell rendering failed with an ArgumentException. Needs someone fluent in Views selection plumbing, or a pixel-based MakeSelAt.
  • PatternEditingHelper.CanCut/CanPaste. ReadOnlyView's setter drives EditingHelper.Editable, which both base implementations gate on, and no other assignment touches this control's editing helper. Dead once both consumers are read-only; removable in a follow-up.

This change is Reviewable

ComplexConcPatternVc has no UpdateProp override, so any edit that reaches
the view engine without going through PatternView.OnKeyPress (IME
composition, drag-and-drop, or a direct IVwSelection.ReplaceWithTsString
call) falls through to VwBaseVc.UpdateProp, which throws
NotImplementedException. These tests drive a real PatternView/
ComplexConcPatternVc pair and confirm the crash reproduces across every
fragment tried: ktagType, ktagForm, ktagGloss, ktagCategory, ktagEntry,
ktagTag, ktagInfl, kfragOR, kfragHash, and both min/max quantifier lines
(11 of 11 fail today). Two contrast tests pass already: a plain keystroke
never reaches the engine (PatternView.OnKeyPress swallows it), and Delete
still raises RemoveItemsRequested.

Also seed the probe/bug docs from the prior investigation.
Root cause: ComplexConcPatternVc renders every fragment (feature lines,
OR/word-boundary literals, brackets, min/max quantifiers) via AddProp +
DisplayVariant -- a computed display over the synthetic pattern node,
never a real bound field (zero AddStringAltMember calls, confirmed by
inspection). With no UpdateProp override, any edit that reaches the
engine without going through PatternView.OnKeyPress fell through to
VwBaseVc.UpdateProp, which throws NotImplementedException.

Fix, in the layers actually shown to be needed by ablation:

- UpdateProp override (necessary and sufficient to stop the crash --
  verified by re-running the full failing suite with only this change
  added: 13/13 pass). It absorbs the edit the same way
  RuleFormulaVcBase.UpdateProp does; there is nothing to write back to,
  so the next redraw simply shows the live node state again.
- ktptNotEditable on every fragment ComplexConcPatternVc renders
  (feature lines, Infl Features lines, OR/#, brackets/parens and their
  pile glyphs, min/max), so an edit is rejected at the selection layer
  instead of silently reaching UpdateProp. Verified separately: a
  selection over a marked fragment now reports IsEditable == false.
- ReadOnlyView = true on ComplexConcControl's PatternView, the
  categorical fix for the IME-composition/drag-and-drop path named in
  the bug report (it unregisters the keyboard/IME controller hook).
  This needs PatternView.AllowDisplaySelection overridden to stay true,
  or the chooser-driven selection highlight disappears once
  ReadOnlyView is set -- SimpleRootSite suppresses Activate() by
  default for read-only views. PatternView is shared with
  RuleFormulaControl (Morphology), which still runs with
  ReadOnlyView = false on this branch, so AllowDisplaySelection is a
  no-op there today.

Delete still raises RemoveItemsRequested with ReadOnlyView = true
(verified).

All 16 tests in ComplexConcPatternVcDirectEditTests.cs pass, including
the 11 that reproduced the crash pre-fix and the 3 new ablation checks.
Documents: this is a different failure mode than the sibling rule-formula
bug (UpdateProp alone is necessary and sufficient here, confirmed by
ablation, because no fragment binds a real field); what was deliberately
not hoisted to PatternVcBase and why; why PatternEditingHelper.CanCut/
CanPaste were left alone (branch-topology reason, noted for whoever
integrates both branches); and what still needs manual verification in
a running FLEx (IME, drag-and-drop, selection visibility, Delete).
Docs/bugs/*.md are working documents that get evicted from the branch
when the PR is written; a source comment pointing at one would dangle.
Rewrite the ComplexConcControl.Designer.cs comment and the test file
header to state the reasoning inline instead of citing the doc.
…e bug it exposed

Adversarial review found MakeSelOnChild inert: it passed the requested tag
into MakeTextSelInObj's unused cvsliEnd slot with fWholeObj: true, so 9 of
11 "fragment angle" tests were all the same whole-object range selection
in disguise (anchored at the outermost boundary glyph, not the named
fragment). Replaced it with MakeSelOnFragment, built on
IVwRootBox.MakeTextSelection with the tag passed as tagTextProp -- the
same API PatternView.SelectLeftBoundary/SelectRightBoundary already use
to target one fake-tag property on an object. Every fragment test now
carries an AssertSelectionTargets(sel, expectedTag) check via
IVwSelection.TextSelInfo, so a future regression in targeting fails
loudly instead of silently passing. Added
MakeSelOnFragment_DiscriminatesBetweenFragments_OnTheSameNode as a
standing proof that three different tags on one node resolve to three
different selections.

Fixing the helper exposed a real production bug: ComplexConcPatternVc's
SetNotEditable(vwenv) calls do not persist across multiple AddProp calls
the way vwenv.Props = someBuilder does -- ktptEditable must be
re-asserted immediately before *every* AddProp, matching the convention
PatternVcBase.AddExtraLines and RuleFormulaVcBase already use. Most of
the fix's ktptEditable markings were only accidentally correct for
single-AddProp call sites (OR, #, min/max); the feature lines
(Form/Entry/Category/Gloss/Infl) and the multi-line bracket/paren
pile-hook sequences were not actually protected. Fixed by re-asserting
SetNotEditable before each individual AddProp in DisplayFeatures,
DisplayInflFeatureLines, DisplayInflFeatures, and the four pile-hook
sequences.

Also fixed a second, genuinely unmarked gap found by mutation testing:
PatternVcBase.OpenSingleLinePile/CloseSingleLinePile's zero-width-space
boundary run (shared by both PatternVcBase subclasses) had no
ktptEditable marking at all. With UpdateProp removed, an edit on this
run still threw NotImplementedException. Marked it NotEditable and
pinned it with SelectionOverZeroWidthBoundaryRun_IsNotEditable, verified
red (fails without the marking, confirmed by temporarily reverting it)
and green (passes with it).

Corrected ablation with the fixed helper (18-test suite): UpdateProp
alone (ktptEditable neutralized) = 16/18 (only the two IsEditable-assertion
tests fail; every crash/content test still passes). ktptEditable alone
(UpdateProp removed) = 18/18. Both are independently sufficient to stop
the crash, unlike the sibling bug where ktptEditable was the only
load-bearing layer. But a direct probe (UpdateProp intact, ktptEditable
neutralized) showed UpdateProp's no-op leaves the *displayed* text
stale/corrupted ("HACKEDForm: original" instead of "Form: original")
until an explicit Reconstruct -- ktptEditable prevents this because the
edit never reaches that point. Both layers are needed, not for
redundant crash-prevention, but because they prevent two different
failure modes.

Also added ComplexConcControl_AcceptsTabUnchanged_AcceptsReturnNowFalse,
pinning the actual behaviour change from ReadOnlyView = true:
AcceptsTab was already false before this fix (unchanged); AcceptsReturn
flips from true to false (new).
…eturn

Rewrites the ablation conclusion (ktptEditable is independently
sufficient too, and prevents a display artifact UpdateProp's no-op does
not), states plainly that ReadOnlyView does not gate ReplaceWithTsString
(mutation-tested) and its only proven value is closing the IME-controller
registration channel (unverified live), documents the AcceptsReturn/
AcceptsTab behaviour change (AcceptsTab unchanged, AcceptsReturn now
false), and updates the ConstChartVc status per the coordinator's
adversarial-review note (escalated to a concrete corruption suspicion,
explicitly out of scope for this task).
The bug analysis, the seed probe and the architecture self-review were
working documents for this fix. Their conclusions are now carried by the
code and its tests; the reasoning, decisions and paths not taken live in
the pull request body so they inform review without merging into the tree.
Move the file-header explanation into the fixture's own doc comment,
replace decorative section banners with one-line statements, drop a
pointer to a working document, and state the AcceptsReturn contract as
current behaviour rather than as a before-and-after.
@github-actions

Copy link
Copy Markdown

⚠️ Commit Message Format Issues ⚠️
commit d91c7d85a5:
1: T1 Title exceeds max length (74>72): "Correct review doc: ablation findings, ReadOnlyView framing, AcceptsReturn"

commit 9fa6dac187:
1: T1 Title exceeds max length (85>72): "Fix inert fragment-selection test helper; fix ktptEditable persistence bug it exposed"

@github-actions

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   10m 31s ⏱️ -18s
5 815 tests +19  5 734 ✅ +19  81 💤 ±0  0 ❌ ±0 
5 824 runs  +19  5 743 ✅ +19  81 💤 ±0  0 ❌ ±0 

Results for commit a8d7f32. ± Comparison against base commit c4160cb.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.81395% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.43%. Comparing base (6c6f9eb) to head (a8d7f32).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
Src/LexText/Interlinear/ComplexConcPatternVc.cs 53.65% 38 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1088      +/-   ##
==========================================
+ Coverage   38.07%   38.43%   +0.36%     
==========================================
  Files        1499     1499              
  Lines      350141   350314     +173     
  Branches    40238    40256      +18     
==========================================
+ Hits       133304   134656    +1352     
+ Misses     187558   186489    -1069     
+ Partials    29279    29169     -110     
Files with missing lines Coverage Δ
...LexText/Interlinear/ComplexConcControl.Designer.cs 92.72% <100.00%> (+92.72%) ⬆️
Src/LexText/LexTextControls/PatternVcBase.cs 76.61% <100.00%> (+76.61%) ⬆️
Src/LexText/LexTextControls/PatternView.cs 21.88% <100.00%> (+21.88%) ⬆️
Src/LexText/Interlinear/ComplexConcPatternVc.cs 68.34% <53.65%> (+68.34%) ⬆️

... and 30 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.

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.

2 participants