|
7 | 7 | using System.Text.Json; |
8 | 8 | using Flow.Launcher.Infrastructure.UserSettings; |
9 | 9 | using Flow.Launcher.Plugin.SharedCommands; |
| 10 | +using Version = SemanticVersioning.Version; |
10 | 11 |
|
11 | 12 | namespace Flow.Launcher.Core.Plugin |
12 | 13 | { |
@@ -79,37 +80,101 @@ internal static (List<PluginMetadata>, List<PluginMetadata>) GetUniqueLatestPlug |
79 | 80 |
|
80 | 81 | var duplicateGroups = allPluginMetadata.GroupBy(x => x.ID).Where(g => g.Count() > 1).Select(y => y).ToList(); |
81 | 82 |
|
82 | | - foreach (var metadata in allPluginMetadata) |
| 83 | + foreach (var group in duplicateGroups) |
83 | 84 | { |
84 | | - var duplicatesExist = false; |
85 | | - foreach (var group in duplicateGroups) |
| 85 | + // Use a single consistent comparison strategy for the entire group |
| 86 | + // to avoid cycles when mixing semantic and non-semantic versions. |
| 87 | + var allSemantic = group.All(x => TryParseSemanticVersion(x.Version, out _)); |
| 88 | + |
| 89 | + // Use the same comparison strategy for both the sort and the tie check so |
| 90 | + // that equal semantic precedence expressed with different text (e.g. |
| 91 | + // "1.0" vs "1.0.0") is detected as a tie. |
| 92 | + IOrderedEnumerable<PluginMetadata> sorted; |
| 93 | + if (allSemantic) |
86 | 94 | { |
87 | | - if (metadata.ID == group.Key) |
| 95 | + sorted = group.OrderByDescending(x => |
88 | 96 | { |
89 | | - duplicatesExist = true; |
| 97 | + TryParseSemanticVersion(x.Version, out var v); |
| 98 | + return v; |
| 99 | + }); |
| 100 | + } |
| 101 | + else |
| 102 | + { |
| 103 | + sorted = group.OrderByDescending(x => x.Version, StringComparer.InvariantCulture); |
| 104 | + } |
90 | 105 |
|
91 | | - // If metadata's version greater than each duplicate's version, CompareTo > 0 |
92 | | - var count = group.Where(x => metadata.Version.CompareTo(x.Version) > 0).Count(); |
93 | | - |
94 | | - // Only add if the meatadata's version is the highest of all duplicates in the group |
95 | | - if (count == group.Count() - 1) |
96 | | - { |
97 | | - unique_list.Add(metadata); |
98 | | - } |
99 | | - else |
100 | | - { |
101 | | - duplicate_list.Add(metadata); |
102 | | - } |
103 | | - } |
| 106 | + var ordered = sorted.ToList(); |
| 107 | + |
| 108 | + // If the top two versions are tied, no single copy is uniquely highest, |
| 109 | + // so treat all as duplicates (preserves original behavior). |
| 110 | + bool isTie; |
| 111 | + if (ordered.Count < 2) |
| 112 | + { |
| 113 | + isTie = false; |
| 114 | + } |
| 115 | + else if (allSemantic) |
| 116 | + { |
| 117 | + TryParseSemanticVersion(ordered[0].Version, out var v0); |
| 118 | + TryParseSemanticVersion(ordered[1].Version, out var v1); |
| 119 | + isTie = v0.Equals(v1); |
| 120 | + } |
| 121 | + else |
| 122 | + { |
| 123 | + isTie = StringComparer.InvariantCulture.Equals(ordered[0].Version, ordered[1].Version); |
104 | 124 | } |
105 | | - |
106 | | - if (!duplicatesExist) |
| 125 | + |
| 126 | + if (!isTie) |
| 127 | + { |
| 128 | + unique_list.Add(ordered[0]); |
| 129 | + duplicate_list.AddRange(ordered.Skip(1)); |
| 130 | + } |
| 131 | + else |
| 132 | + { |
| 133 | + duplicate_list.AddRange(ordered); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + // Add plugins that have no duplicates |
| 138 | + foreach (var metadata in allPluginMetadata) |
| 139 | + { |
| 140 | + if (!duplicateGroups.Any(g => g.Key == metadata.ID)) |
| 141 | + { |
107 | 142 | unique_list.Add(metadata); |
| 143 | + } |
108 | 144 | } |
109 | 145 |
|
110 | 146 | return (unique_list, duplicate_list); |
111 | 147 | } |
112 | 148 |
|
| 149 | + private static bool TryParseSemanticVersion(string value, out Version version) |
| 150 | + { |
| 151 | + if (Version.TryParse(value, out version)) |
| 152 | + { |
| 153 | + return true; |
| 154 | + } |
| 155 | + |
| 156 | + if (string.IsNullOrEmpty(value)) |
| 157 | + { |
| 158 | + return false; |
| 159 | + } |
| 160 | + |
| 161 | + var suffixIndex = value.IndexOfAny(new[] { '-', '+' }); |
| 162 | + var coreLength = suffixIndex >= 0 ? suffixIndex : value.Length; |
| 163 | + var componentCount = value[..coreLength].Split('.').Length; |
| 164 | + |
| 165 | + if (componentCount is not (1 or 2)) |
| 166 | + { |
| 167 | + return false; |
| 168 | + } |
| 169 | + |
| 170 | + var missingComponents = componentCount == 1 ? ".0.0" : ".0"; |
| 171 | + var normalized = suffixIndex >= 0 |
| 172 | + ? value.Insert(suffixIndex, missingComponents) |
| 173 | + : value + missingComponents; |
| 174 | + |
| 175 | + return Version.TryParse(normalized, out version); |
| 176 | + } |
| 177 | + |
113 | 178 | private static PluginMetadata GetPluginMetadata(string pluginDirectory) |
114 | 179 | { |
115 | 180 | string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName); |
|
0 commit comments