/ src / modules / cmdpal / Microsoft.CmdPal.UI.ViewModels / Commands / MainListPageResultFactory.cs
MainListPageResultFactory.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  #pragma warning disable IDE0007 // Use implicit type
  6  
  7  using Microsoft.CommandPalette.Extensions;
  8  using Microsoft.CommandPalette.Extensions.Toolkit;
  9  
 10  namespace Microsoft.CmdPal.UI.ViewModels.Commands;
 11  
 12  internal static class MainListPageResultFactory
 13  {
 14      /// <summary>
 15      /// Creates a merged and ordered array of results from multiple scored input lists,
 16      /// applying an application result limit and filtering fallback items as needed.
 17      /// </summary>
 18      public static IListItem[] Create(
 19          IList<Scored<IListItem>>? filteredItems,
 20          IList<Scored<IListItem>>? scoredFallbackItems,
 21          IList<Scored<IListItem>>? filteredApps,
 22          IList<Scored<IListItem>>? fallbackItems,
 23          int appResultLimit)
 24      {
 25          if (appResultLimit < 0)
 26          {
 27              throw new ArgumentOutOfRangeException(
 28                  nameof(appResultLimit), "App result limit must be non-negative.");
 29          }
 30  
 31          int len1 = filteredItems?.Count ?? 0;
 32  
 33          // Empty fallbacks are removed prior to this merge.
 34          int len2 = scoredFallbackItems?.Count ?? 0;
 35  
 36          // Apps are pre-sorted, so we just need to take the top N, limited by appResultLimit.
 37          int len3 = Math.Min(filteredApps?.Count ?? 0, appResultLimit);
 38  
 39          int nonEmptyFallbackCount = fallbackItems?.Count ?? 0;
 40  
 41          // Allocate the exact size of the result array.
 42          // We'll add an extra slot for the fallbacks section header if needed.
 43          int totalCount = len1 + len2 + len3 + nonEmptyFallbackCount + (nonEmptyFallbackCount > 0 ? 1 : 0);
 44  
 45          var result = new IListItem[totalCount];
 46  
 47          // Three-way stable merge of already-sorted lists.
 48          int idx1 = 0, idx2 = 0, idx3 = 0;
 49          int writePos = 0;
 50  
 51          // Merge while all three lists have items. To maintain a stable sort, the
 52          // priority is: list1 > list2 > list3 when scores are equal.
 53          while (idx1 < len1 && idx2 < len2 && idx3 < len3)
 54          {
 55              // Using null-forgiving operator as we have already checked against lengths.
 56              int score1 = filteredItems![idx1].Score;
 57              int score2 = scoredFallbackItems![idx2].Score;
 58              int score3 = filteredApps![idx3].Score;
 59  
 60              if (score1 >= score2 && score1 >= score3)
 61              {
 62                  result[writePos++] = filteredItems[idx1++].Item;
 63              }
 64              else if (score2 >= score3)
 65              {
 66                  result[writePos++] = scoredFallbackItems[idx2++].Item;
 67              }
 68              else
 69              {
 70                  result[writePos++] = filteredApps[idx3++].Item;
 71              }
 72          }
 73  
 74          // Two-way merges for remaining pairs.
 75          while (idx1 < len1 && idx2 < len2)
 76          {
 77              if (filteredItems![idx1].Score >= scoredFallbackItems![idx2].Score)
 78              {
 79                  result[writePos++] = filteredItems[idx1++].Item;
 80              }
 81              else
 82              {
 83                  result[writePos++] = scoredFallbackItems[idx2++].Item;
 84              }
 85          }
 86  
 87          while (idx1 < len1 && idx3 < len3)
 88          {
 89              if (filteredItems![idx1].Score >= filteredApps![idx3].Score)
 90              {
 91                  result[writePos++] = filteredItems[idx1++].Item;
 92              }
 93              else
 94              {
 95                  result[writePos++] = filteredApps[idx3++].Item;
 96              }
 97          }
 98  
 99          while (idx2 < len2 && idx3 < len3)
100          {
101              if (scoredFallbackItems![idx2].Score >= filteredApps![idx3].Score)
102              {
103                  result[writePos++] = scoredFallbackItems[idx2++].Item;
104              }
105              else
106              {
107                  result[writePos++] = filteredApps[idx3++].Item;
108              }
109          }
110  
111          // Drain remaining items from a non-empty list.
112          while (idx1 < len1)
113          {
114              result[writePos++] = filteredItems![idx1++].Item;
115          }
116  
117          while (idx2 < len2)
118          {
119              result[writePos++] = scoredFallbackItems![idx2++].Item;
120          }
121  
122          while (idx3 < len3)
123          {
124              result[writePos++] = filteredApps![idx3++].Item;
125          }
126  
127          // Append filtered fallback items. Fallback items are added post-sort so they are
128          // always at the end of the list and are sorted by user settings.
129          if (fallbackItems is not null)
130          {
131              // Create the fallbacks section header
132              if (fallbackItems.Count > 0)
133              {
134                  result[writePos++] = new Separator(Properties.Resources.fallbacks);
135              }
136  
137              for (int i = 0; i < fallbackItems.Count; i++)
138              {
139                  var item = fallbackItems[i].Item;
140                  if (!string.IsNullOrEmpty(item.Title))
141                  {
142                      result[writePos++] = item;
143                  }
144              }
145          }
146  
147          return result;
148      }
149  
150      private static int GetNonEmptyFallbackItemsCount(IList<Scored<IListItem>>? fallbackItems)
151      {
152          int fallbackItemsCount = 0;
153  
154          if (fallbackItems is not null)
155          {
156              for (int i = 0; i < fallbackItems.Count; i++)
157              {
158                  if (!string.IsNullOrWhiteSpace(fallbackItems[i].Item.Title))
159                  {
160                      fallbackItemsCount++;
161                  }
162              }
163          }
164  
165          return fallbackItemsCount;
166      }
167  }
168  #pragma warning restore IDE0007 // Use implicit type