From fa66aa8e7ba13a0cb6d61e48a789d2d412374a8f Mon Sep 17 00:00:00 2001 From: Zachary Burnham Date: Fri, 14 Aug 2026 11:28:51 -0400 Subject: [PATCH 1/4] LT-22524: Add substring search to Find Lexical Entry Find Lexical Entry now matches typed characters anywhere in a word for queries of 3+ characters, using liblcm's new SearchType.Substring; shorter queries keep the original full-text search. The mode decision lives in a unit-tested SubstringSearchPolicy, and results retain the existing exact -> starts-with -> anywhere ordering. --- .../XMLViews/MatchingObjectsBrowser.cs | 15 +++++ Src/LexText/LexTextControls/EntryGoDlg.cs | 53 ++++++++++++++++- .../LexTextControls/EntryGoSearchEngine.cs | 4 +- .../LexTextControls.Designer.cs | 2 +- .../SubstringSearchPolicyTests.cs | 58 +++++++++++++++++++ .../LexTextControls/SubstringSearchPolicy.cs | 36 ++++++++++++ 6 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs create mode 100644 Src/LexText/LexTextControls/SubstringSearchPolicy.cs diff --git a/Src/Common/Controls/XMLViews/MatchingObjectsBrowser.cs b/Src/Common/Controls/XMLViews/MatchingObjectsBrowser.cs index 3f9627c211..89772c5428 100644 --- a/Src/Common/Controls/XMLViews/MatchingObjectsBrowser.cs +++ b/Src/Common/Controls/XMLViews/MatchingObjectsBrowser.cs @@ -205,6 +205,21 @@ public void Initialize(LcmCache cache, IVwStylesheet stylesheet, Mediator mediat ResumeLayout(false); } + /// + /// Swap the active search engine (e.g. when toggling substring-match mode). The caller + /// owns the lifetime of both engines; this only re-hooks the SearchCompleted event. + /// + public void SetSearchEngine(SearchEngine searchEngine) + { + CheckDisposed(); + if (ReferenceEquals(m_searchEngine, searchEngine)) + return; + if (m_searchEngine != null) + m_searchEngine.SearchCompleted -= m_searchEngine_SearchCompleted; + m_searchEngine = searchEngine; + m_searchEngine.SearchCompleted += m_searchEngine_SearchCompleted; + } + private void m_searchEngine_SearchCompleted(object sender, SearchCompletedEventArgs e) { UpdateResults(e.Fields.FirstOrDefault(), e.Results); diff --git a/Src/LexText/LexTextControls/EntryGoDlg.cs b/Src/LexText/LexTextControls/EntryGoDlg.cs index 1edac295ad..ba1041f511 100644 --- a/Src/LexText/LexTextControls/EntryGoDlg.cs +++ b/Src/LexText/LexTextControls/EntryGoDlg.cs @@ -41,6 +41,51 @@ protected override string PersistenceLabel get { return "EntryGo"; } } + /// + /// The default engine: full-text (word-prefix) matching. Cached in the property table. + /// + private SearchEngine FullTextSearchEngine + { + get + { + return SearchEngine.Get(m_mediator, m_propertyTable, "EntryGoSearchEngine", + () => new EntryGoSearchEngine(m_cache, SearchType.FullText)); + } + } + + /// + /// The substring (match-anywhere) engine. Built lazily the first time a query is long enough + /// to use substring matching (see ). + /// + private SearchEngine SubstringSearchEngine + { + get + { + return SearchEngine.Get(m_mediator, m_propertyTable, "EntryGoSubstringSearchEngine", + () => new EntryGoSearchEngine(m_cache, SearchType.Substring)); + } + } + + /// + /// True when this query should use substring matching. The decision (minimum query length) + /// lives in . (Kept separate from + /// so callers can test the mode without instantiating the substring engine.) + /// + private bool UseSubstringFor(string searchKey) + { + return SubstringSearchPolicy.UseSubstring(searchKey); + } + + /// + /// Choose the engine for a given search key: the substring engine only when + /// is true; otherwise the default full-text engine + /// (which still returns its normal results for short keys). + /// + private SearchEngine SearchEngineFor(string searchKey) + { + return UseSubstringFor(searchKey) ? SubstringSearchEngine : FullTextSearchEngine; + } + /// /// Get/Set the starting entry object. This will not be displayed in the list of /// matching entries. @@ -85,10 +130,8 @@ protected override void InitializeMatchingObjects(LcmCache cache) var xnWindow = m_propertyTable.GetValue("WindowConfiguration"); XmlNode configNode = xnWindow.SelectSingleNode("controls/parameters/guicontrol[@id=\"matchingEntries\"]/parameters"); - SearchEngine searchEngine = SearchEngine.Get(m_mediator, m_propertyTable, "EntryGoSearchEngine", () => new EntryGoSearchEngine(cache)); - m_matchingObjectsBrowser.Initialize(cache, FontHeightAdjuster.StyleSheetFromPropertyTable(m_propertyTable), m_mediator, m_propertyTable, configNode, - searchEngine); + SearchEngineFor(string.Empty)); m_matchingObjectsBrowser.ColumnsChanged += m_matchingObjectsBrowser_ColumnsChanged; @@ -97,6 +140,7 @@ protected override void InitializeMatchingObjects(LcmCache cache) if (selectedWs != null) m_matchingObjectsBrowser.SearchAsync(GetFields(string.Empty, selectedWs.Handle)); } + #endregion Construction and Destruction #region Other methods @@ -158,6 +202,9 @@ protected override void ResetMatches(string searchKey) m_oldSearchKey = searchKey; m_oldSearchWs = wsSelHvo; + // Select the engine for this query: substring once the key is long enough (>= MinQueryLength); + // otherwise the default full-text engine, so short keys behave like the original search. + m_matchingObjectsBrowser.SetSearchEngine(SearchEngineFor(searchKey)); m_matchingObjectsBrowser.SearchAsync(GetFields(searchKey, wsSelHvo)); } diff --git a/Src/LexText/LexTextControls/EntryGoSearchEngine.cs b/Src/LexText/LexTextControls/EntryGoSearchEngine.cs index a47d66ad00..3a4a9cce66 100644 --- a/Src/LexText/LexTextControls/EntryGoSearchEngine.cs +++ b/Src/LexText/LexTextControls/EntryGoSearchEngine.cs @@ -20,8 +20,8 @@ internal class EntryGoSearchEngine : SearchEngine { private readonly Virtuals m_virtuals; - public EntryGoSearchEngine(LcmCache cache) - : base(cache, SearchType.FullText) + public EntryGoSearchEngine(LcmCache cache, SearchType searchType = SearchType.FullText) + : base(cache, searchType) { m_virtuals = Cache.ServiceLocator.GetInstance(); } diff --git a/Src/LexText/LexTextControls/LexTextControls.Designer.cs b/Src/LexText/LexTextControls/LexTextControls.Designer.cs index 8611971f6b..e581cf3f2f 100644 --- a/Src/LexText/LexTextControls/LexTextControls.Designer.cs +++ b/Src/LexText/LexTextControls/LexTextControls.Designer.cs @@ -1149,7 +1149,7 @@ internal static string ksFindLexEntry { return ResourceManager.GetString("ksFindLexEntry", resourceCulture); } } - + /// /// Looks up a localized string similar to Find Record. /// diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs new file mode 100644 index 0000000000..b795c4215f --- /dev/null +++ b/Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using NUnit.Framework; +using SIL.FieldWorks.LexText.Controls; + +namespace LexTextControlsTests +{ + /// + /// Tests for the pure Find-Lexical-Entry substring-search decision logic that was extracted from + /// EntryGoDlg so it could be tested without driving the dialog. + /// + [TestFixture] + public class SubstringSearchPolicyTests + { + // A base letter followed by a combining acute accent (U+0301): 2 UTF-16 units that compose to + // a single character under FormC normalization. + private static readonly string ComposedAcuteE = "e" + (char)0x0301; + + [TestCase("", ExpectedResult = false)] + [TestCase("l", ExpectedResult = false)] + [TestCase("la", ExpectedResult = false)] // below MinQueryLength + [TestCase("lan", ExpectedResult = true)] // exactly MinQueryLength + [TestCase("language", ExpectedResult = true)] + public bool UseSubstring_gatesOnLength(string key) + { + return SubstringSearchPolicy.UseSubstring(key); + } + + [Test] + public void UseSubstring_nullKey_isFalse() + { + Assert.That(SubstringSearchPolicy.UseSubstring(null), Is.False); + } + + [Test] + public void UseSubstring_countsComposedCharacters_notUtf16Units() + { + // Each ComposedAcuteE is 2 UTF-16 units but 1 character after FormC. If the policy counted + // raw Length it would see 4 and 6 (both >= 3) and wrongly enable substring; counting composed + // characters it sees 2 and 3. + string twoComposed = ComposedAcuteE + ComposedAcuteE; // raw Length 4 -> 2 + string threeComposed = ComposedAcuteE + ComposedAcuteE + ComposedAcuteE; // raw Length 6 -> 3 + + Assert.That(SubstringSearchPolicy.UseSubstring(twoComposed), Is.False, + "two composed characters should count as length 2, below the threshold"); + Assert.That(SubstringSearchPolicy.UseSubstring(threeComposed), Is.True, + "three composed characters should count as length 3, at the threshold"); + } + + [Test] + public void MinQueryLength_hasExpectedDefault() + { + Assert.That(SubstringSearchPolicy.MinQueryLength, Is.EqualTo(3)); + } + } +} diff --git a/Src/LexText/LexTextControls/SubstringSearchPolicy.cs b/Src/LexText/LexTextControls/SubstringSearchPolicy.cs new file mode 100644 index 0000000000..ccca72cbff --- /dev/null +++ b/Src/LexText/LexTextControls/SubstringSearchPolicy.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System.Text; + +namespace SIL.FieldWorks.LexText.Controls +{ + /// + /// UI-independent decision logic for the Find Lexical Entry "match anywhere" (substring) search + /// mode: when substring matching applies, plus its tuning knobs. Kept out of + /// so it can be unit-tested without driving the dialog. + /// + internal static class SubstringSearchPolicy + { + /// + /// Substring matching engages only once the query is at least this many characters; shorter + /// keys fall back to the default full-text engine, so short queries behave like the original + /// search (a 1-2 char substring in a large project would otherwise match most entries). + /// + public const int MinQueryLength = 3; + + /// + /// True when a query should use substring matching: the key is at least + /// characters. Length is counted after FormC normalization, so a + /// base character plus a combining diacritic counts as one character. Shorter keys fall back to + /// the default full-text search. + /// + /// The (already trimmed) search key. + public static bool UseSubstring(string searchKey) + { + return !string.IsNullOrEmpty(searchKey) + && searchKey.Normalize(NormalizationForm.FormC).Length >= MinQueryLength; + } + } +} From 6a56fb8ceffd2411f8f428e4e3337acaa8127a99 Mon Sep 17 00:00:00 2001 From: Zachary Burnham Date: Tue, 25 Aug 2026 17:41:20 -0400 Subject: [PATCH 2/4] Comment cleanup --- Src/LexText/LexTextControls/EntryGoDlg.cs | 15 ++++++++------- .../SubstringSearchPolicyTests.cs | 14 +++++++------- .../LexTextControls/SubstringSearchPolicy.cs | 19 ++++++++++--------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Src/LexText/LexTextControls/EntryGoDlg.cs b/Src/LexText/LexTextControls/EntryGoDlg.cs index ba1041f511..3335005c6e 100644 --- a/Src/LexText/LexTextControls/EntryGoDlg.cs +++ b/Src/LexText/LexTextControls/EntryGoDlg.cs @@ -54,8 +54,8 @@ private SearchEngine FullTextSearchEngine } /// - /// The substring (match-anywhere) engine. Built lazily the first time a query is long enough - /// to use substring matching (see ). + /// The substring (match-anywhere) engine. Built lazily the first time a query is long + /// enough to use substring matching (see ). /// private SearchEngine SubstringSearchEngine { @@ -67,9 +67,10 @@ private SearchEngine SubstringSearchEngine } /// - /// True when this query should use substring matching. The decision (minimum query length) - /// lives in . (Kept separate from - /// so callers can test the mode without instantiating the substring engine.) + /// True when this query should use substring matching. The decision (minimum query + /// length) lives in . Kept separate from + /// so callers can test the mode without instantiating the + /// substring engine. /// private bool UseSubstringFor(string searchKey) { @@ -202,8 +203,8 @@ protected override void ResetMatches(string searchKey) m_oldSearchKey = searchKey; m_oldSearchWs = wsSelHvo; - // Select the engine for this query: substring once the key is long enough (>= MinQueryLength); - // otherwise the default full-text engine, so short keys behave like the original search. + // Select the engine for this query: substring once the key is long enough + // (>= MinQueryLength), otherwise full-text so short keys behave like the original. m_matchingObjectsBrowser.SetSearchEngine(SearchEngineFor(searchKey)); m_matchingObjectsBrowser.SearchAsync(GetFields(searchKey, wsSelHvo)); } diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs index b795c4215f..1e870e076a 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/SubstringSearchPolicyTests.cs @@ -8,14 +8,14 @@ namespace LexTextControlsTests { /// - /// Tests for the pure Find-Lexical-Entry substring-search decision logic that was extracted from - /// EntryGoDlg so it could be tested without driving the dialog. + /// Verifies the query-length gate that decides when Find Lexical Entry uses substring + /// (match-anywhere) matching. /// [TestFixture] public class SubstringSearchPolicyTests { - // A base letter followed by a combining acute accent (U+0301): 2 UTF-16 units that compose to - // a single character under FormC normalization. + // A base letter followed by a combining acute accent (U+0301): 2 UTF-16 units that + // compose to a single character under FormC normalization. private static readonly string ComposedAcuteE = "e" + (char)0x0301; [TestCase("", ExpectedResult = false)] @@ -37,9 +37,9 @@ public void UseSubstring_nullKey_isFalse() [Test] public void UseSubstring_countsComposedCharacters_notUtf16Units() { - // Each ComposedAcuteE is 2 UTF-16 units but 1 character after FormC. If the policy counted - // raw Length it would see 4 and 6 (both >= 3) and wrongly enable substring; counting composed - // characters it sees 2 and 3. + // Each ComposedAcuteE is 2 UTF-16 units but 1 character after FormC. Counting raw + // Length would see 4 and 6 (both >= 3) and wrongly enable substring; composed + // characters count as 2 and 3. string twoComposed = ComposedAcuteE + ComposedAcuteE; // raw Length 4 -> 2 string threeComposed = ComposedAcuteE + ComposedAcuteE + ComposedAcuteE; // raw Length 6 -> 3 diff --git a/Src/LexText/LexTextControls/SubstringSearchPolicy.cs b/Src/LexText/LexTextControls/SubstringSearchPolicy.cs index ccca72cbff..22fb7cfef8 100644 --- a/Src/LexText/LexTextControls/SubstringSearchPolicy.cs +++ b/Src/LexText/LexTextControls/SubstringSearchPolicy.cs @@ -7,24 +7,25 @@ namespace SIL.FieldWorks.LexText.Controls { /// - /// UI-independent decision logic for the Find Lexical Entry "match anywhere" (substring) search - /// mode: when substring matching applies, plus its tuning knobs. Kept out of - /// so it can be unit-tested without driving the dialog. + /// UI-independent decision logic for the Find Lexical Entry "match anywhere" (substring) + /// search mode: when substring matching applies, plus its tuning knobs. Kept out of + /// so it can be unit-tested without driving the dialog. /// internal static class SubstringSearchPolicy { /// - /// Substring matching engages only once the query is at least this many characters; shorter - /// keys fall back to the default full-text engine, so short queries behave like the original - /// search (a 1-2 char substring in a large project would otherwise match most entries). + /// Substring matching engages only once the query is at least this many characters; + /// shorter keys fall back to the default full-text engine, so short queries behave like + /// the original search (a 1-2 char substring in a large project would otherwise match + /// most entries). /// public const int MinQueryLength = 3; /// /// True when a query should use substring matching: the key is at least - /// characters. Length is counted after FormC normalization, so a - /// base character plus a combining diacritic counts as one character. Shorter keys fall back to - /// the default full-text search. + /// characters. Length is counted after FormC normalization, + /// so a base character plus a combining diacritic counts as one character. Shorter keys + /// fall back to the default full-text search. /// /// The (already trimmed) search key. public static bool UseSubstring(string searchKey) From f30f2a469d04ae8870decb81cac3e8c8656df74a Mon Sep 17 00:00:00 2001 From: Zachary Burnham Date: Tue, 25 Aug 2026 18:15:55 -0400 Subject: [PATCH 3/4] Liblcm version dependency --- Build/SilVersions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/SilVersions.props b/Build/SilVersions.props index 551972abe1..3de6c4fb0d 100644 --- a/Build/SilVersions.props +++ b/Build/SilVersions.props @@ -12,7 +12,7 @@ ============================================================= --> - 11.0.0-beta0178 + 11.0.0-beta0180 18.0.0-beta0030 18.0.0-beta0012 6.0.0-beta0065 From ff11df9f056db794a1c2b39fbceaefcf79282bac Mon Sep 17 00:00:00 2001 From: Zachary Burnham Date: Wed, 26 Aug 2026 15:02:10 -0400 Subject: [PATCH 4/4] LT-22524: Keep Merge Entry from matching an entry with itself ResetMatches now swaps the matching browser's search engine on each query to move between full-text and substring modes. That swap always installed an EntryGoDlg-owned engine, replacing the MergeEntryDlg engine that excludes the starting entry. As soon as the Merge Entry dialog set its form text (or the user typed), the starting entry reappeared in the list and could be merged into itself. Make the full-text and substring engine accessors virtual on EntryGoDlg so MergeEntryDlg can supply engines that exclude the starting entry in both modes, and drop MergeEntryDlg's InitializeMatchingObjects override now that the base engine selection handles it. Add tests covering the exclusion in full-text and substring modes. Co-Authored-By: Claude --- Src/LexText/LexTextControls/EntryGoDlg.cs | 11 ++- .../MergeEntrySearchEngineTests.cs | 94 +++++++++++++++++++ Src/LexText/LexTextControls/MergeEntryDlg.cs | 41 ++++---- 3 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 Src/LexText/LexTextControls/LexTextControlsTests/MergeEntrySearchEngineTests.cs diff --git a/Src/LexText/LexTextControls/EntryGoDlg.cs b/Src/LexText/LexTextControls/EntryGoDlg.cs index 3335005c6e..fae6c87271 100644 --- a/Src/LexText/LexTextControls/EntryGoDlg.cs +++ b/Src/LexText/LexTextControls/EntryGoDlg.cs @@ -43,8 +43,10 @@ protected override string PersistenceLabel /// /// The default engine: full-text (word-prefix) matching. Cached in the property table. + /// Overridable so a subclass can supply a specialized engine (for example one that + /// excludes a starting entry). /// - private SearchEngine FullTextSearchEngine + protected virtual SearchEngine FullTextSearchEngine { get { @@ -54,10 +56,11 @@ private SearchEngine FullTextSearchEngine } /// - /// The substring (match-anywhere) engine. Built lazily the first time a query is long - /// enough to use substring matching (see ). + /// The substring (match-anywhere) engine, used once a query is long enough + /// (see ). A subclass can override it to supply a + /// specialized engine that also filters substring results. /// - private SearchEngine SubstringSearchEngine + protected virtual SearchEngine SubstringSearchEngine { get { diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/MergeEntrySearchEngineTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/MergeEntrySearchEngineTests.cs new file mode 100644 index 0000000000..bb2d3484d7 --- /dev/null +++ b/Src/LexText/LexTextControls/LexTextControlsTests/MergeEntrySearchEngineTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using SIL.FieldWorks.Common.Controls; +using SIL.FieldWorks.LexText.Controls; +using SIL.LCModel; +using SIL.LCModel.Core.Text; +using SIL.LCModel.DomainServices; + +namespace LexTextControlsTests +{ + /// + /// The Merge Entry search engine must exclude the starting entry from its matches in BOTH + /// full-text and substring modes, so an entry can never be offered as a target for merging + /// into itself. The substring case matters because a typical multi-character merge-target + /// query runs under substring matching (see ). + /// + [TestFixture] + public class MergeEntrySearchEngineTests : MemoryOnlyBackendProviderRestoredForEachTestTestBase + { + private ILexEntry _language; + private ILexEntry _languor; + + // The base opens a UOW in TestSetup and calls CreateTestData() inside it, so data is + // created directly here with no UOW wrapper (a nested task would throw). + protected override void CreateTestData() + { + base.CreateTestData(); + _language = MakeEntry("language", "speech"); + _languor = MakeEntry("languor", "weariness"); + } + + private ILexEntry MakeEntry(string lexemeForm, string gloss) + { + var components = new LexEntryComponents + { + MorphType = Cache.ServiceLocator.GetInstance() + .GetObject(MoMorphTypeTags.kguidMorphStem) + }; + components.LexemeFormAlternatives.Add(TsStringUtils.MakeString(lexemeForm, Cache.DefaultVernWs)); + components.GlossAlternatives.Add(TsStringUtils.MakeString(gloss, Cache.DefaultAnalWs)); + return Cache.ServiceLocator.GetInstance().Create(components); + } + + private IEnumerable LexemeFormQuery(string query) + { + var tss = TsStringUtils.MakeString(query, Cache.DefaultVernWs); + return new[] { new SearchField(LexEntryTags.kflidLexemeForm, tss) }; + } + + [Test] + public void FullTextEngine_ExcludesTheStartingEntry() + { + using (var engine = new MergeEntryDlg.MergeEntrySearchEngine(Cache, SearchType.FullText)) + { + // "langu" is a word-prefix shared by both entries, so full-text matches both. + var withoutExclusion = engine.Search(LexemeFormQuery("langu")).ToList(); + Assert.That(withoutExclusion, Does.Contain(_language.Hvo).And.Contain(_languor.Hvo), + "both entries match the shared prefix before any entry is excluded"); + + engine.CurrentEntryHvo = _language.Hvo; + var withExclusion = engine.Search(LexemeFormQuery("langu")).ToList(); + Assert.That(withExclusion, Does.Not.Contain(_language.Hvo), + "the starting entry is never offered as a target for merging into itself"); + Assert.That(withExclusion, Does.Contain(_languor.Hvo), + "the other matching entry still appears"); + } + } + + [Test] + public void SubstringEngine_ExcludesTheStartingEntry() + { + using (var engine = new MergeEntryDlg.MergeEntrySearchEngine(Cache, SearchType.Substring)) + { + // "angu" is an interior substring of both entries and a prefix of neither, so it + // exercises the substring (match-anywhere) path specifically. + var withoutExclusion = engine.Search(LexemeFormQuery("angu")).ToList(); + Assert.That(withoutExclusion, Does.Contain(_language.Hvo).And.Contain(_languor.Hvo), + "both entries match the interior substring before any entry is excluded"); + + engine.CurrentEntryHvo = _language.Hvo; + var withExclusion = engine.Search(LexemeFormQuery("angu")).ToList(); + Assert.That(withExclusion, Does.Not.Contain(_language.Hvo), + "substring results also exclude the starting entry (the self-merge guard)"); + Assert.That(withExclusion, Does.Contain(_languor.Hvo), + "the other interior-substring match still appears"); + } + } + } +} diff --git a/Src/LexText/LexTextControls/MergeEntryDlg.cs b/Src/LexText/LexTextControls/MergeEntryDlg.cs index 3fb55eadb2..0661121691 100644 --- a/Src/LexText/LexTextControls/MergeEntryDlg.cs +++ b/Src/LexText/LexTextControls/MergeEntryDlg.cs @@ -187,35 +187,44 @@ protected override void SetBottomMessage() m_fwTextBoxBottomMsg.Tss = tsb.GetString(); } - protected override void InitializeMatchingObjects(LcmCache cache) + /// + /// The full-text engine, specialized to drop the starting entry so an entry can never be + /// offered as a merge target for itself. + /// + protected override SearchEngine FullTextSearchEngine { - var xnWindow = m_propertyTable.GetValue("WindowConfiguration"); - XmlNode configNode = xnWindow.SelectSingleNode("controls/parameters/guicontrol[@id=\"matchingEntries\"]/parameters"); - - var searchEngine = (MergeEntrySearchEngine)SearchEngine.Get(m_mediator, m_propertyTable, "MergeEntrySearchEngine", () => new MergeEntrySearchEngine(cache)); - searchEngine.CurrentEntryHvo = m_startingEntry.Hvo; + get { return MergeSearchEngine("MergeEntrySearchEngine", SearchType.FullText); } + } - m_matchingObjectsBrowser.Initialize(cache, FontHeightAdjuster.StyleSheetFromPropertyTable(m_propertyTable), m_mediator, m_propertyTable, configNode, - searchEngine); + /// + /// The substring engine, specialized to drop the starting entry. + /// + protected override SearchEngine SubstringSearchEngine + { + get { return MergeSearchEngine("MergeEntrySubstringSearchEngine", SearchType.Substring); } + } - // start building index - var selectedWs = (CoreWritingSystemDefinition) m_cbWritingSystems.SelectedItem; - if(selectedWs != null) - m_matchingObjectsBrowser.SearchAsync(GetFields(string.Empty, selectedWs.Handle)); + private SearchEngine MergeSearchEngine(string cacheKey, SearchType searchType) + { + var searchEngine = (MergeEntrySearchEngine)SearchEngine.Get(m_mediator, m_propertyTable, + cacheKey, () => new MergeEntrySearchEngine(m_cache, searchType)); + searchEngine.CurrentEntryHvo = m_startingEntry.Hvo; + return searchEngine; } /// - /// A search engine that excludes the current entry (you can't merge an entry with its self + /// A search engine that excludes the current entry, since you cannot merge an entry + /// with itself. /// - private class MergeEntrySearchEngine : EntryGoSearchEngine + internal class MergeEntrySearchEngine : EntryGoSearchEngine { public int CurrentEntryHvo { private get; set; } - public MergeEntrySearchEngine(LcmCache cache) : base(cache) + public MergeEntrySearchEngine(LcmCache cache, SearchType searchType) : base(cache, searchType) { } - protected override IEnumerable FilterResults(IEnumerable results) + protected override IEnumerable FilterResults(IEnumerable results) { return results == null ? null : results.Where(hvo => hvo != CurrentEntryHvo); }