diff --git a/src/.editorconfig b/src/.editorconfig
index cfb7e4f786..cce712f5f9 100644
--- a/src/.editorconfig
+++ b/src/.editorconfig
@@ -9,6 +9,7 @@ spelling_languages = en-US
spelling_checkable_types = strings,identifiers,comments
spelling_error_severity = information
spelling_exclusion_path = spellcheckerexclusion.dic
+spelling_use_default_exclusion_dictionary = true
# Use CRLF for end of line
[*]
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 5daad4f971..40bedea1b8 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -7,7 +7,6 @@
8.1.07.1.27.0.0
- 2.0.14.7.377.1.1008.0.0-dev.65
@@ -30,7 +29,6 @@
-
diff --git a/src/app/dev/DevToys.Api/Core/ObservableHashSet.cs b/src/app/dev/DevToys.Api/Core/ObservableHashSet.cs
new file mode 100644
index 0000000000..12aa580dff
--- /dev/null
+++ b/src/app/dev/DevToys.Api/Core/ObservableHashSet.cs
@@ -0,0 +1,486 @@
+using System.Collections;
+using System.Collections.Specialized;
+
+namespace DevToys.Api.Core;
+
+///
+/// COPY FROM https://github.com/dotnet/efcore/blob/2d7c4a407e3ae8b1fab38500465e37619786d362/src/EFCore/ChangeTracking/ObservableHashSet.cs
+///
+///
+/// See Local views of tracked entities in EF Core for more information and
+/// examples.
+///
+/// The type of elements in the hash set.
+public class ObservableHashSet
+ : ISet, IReadOnlyCollection, INotifyCollectionChanged, INotifyPropertyChanged, INotifyPropertyChanging
+{
+ private HashSet _set;
+
+ ///
+ /// Initializes a new instance of the class
+ /// that is empty and uses the default equality comparer for the set type.
+ ///
+ public ObservableHashSet()
+ : this(EqualityComparer.Default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// that is empty and uses the specified equality comparer for the set type.
+ ///
+ ///
+ /// The implementation to use when
+ /// comparing values in the set, or null to use the default
+ /// implementation for the set type.
+ ///
+ public ObservableHashSet(IEqualityComparer comparer)
+ {
+ _set = new HashSet(comparer);
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// that uses the default equality comparer for the set type, contains elements copied
+ /// from the specified collection, and has sufficient capacity to accommodate the
+ /// number of elements copied.
+ ///
+ /// The collection whose elements are copied to the new set.
+ public ObservableHashSet(IEnumerable collection)
+ : this(collection, EqualityComparer.Default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// that uses the specified equality comparer for the set type, contains elements
+ /// copied from the specified collection, and has sufficient capacity to accommodate
+ /// the number of elements copied.
+ ///
+ /// The collection whose elements are copied to the new set.
+ ///
+ /// The implementation to use when
+ /// comparing values in the set, or null to use the default
+ /// implementation for the set type.
+ ///
+ public ObservableHashSet(IEnumerable collection, IEqualityComparer comparer)
+ {
+ _set = new HashSet(collection, comparer);
+ }
+
+ ///
+ /// Occurs when a property of this hash set (such as ) changes.
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ /// Occurs when a property of this hash set (such as ) is changing.
+ ///
+ public event PropertyChangingEventHandler? PropertyChanging;
+
+ ///
+ /// Occurs when the contents of the hash set changes.
+ ///
+ public event NotifyCollectionChangedEventHandler? CollectionChanged;
+
+ void ICollection.Add(T item)
+ => Add(item);
+
+ ///
+ /// Removes all elements from the hash set.
+ ///
+ public virtual void Clear()
+ {
+ if (_set.Count == 0)
+ {
+ return;
+ }
+
+ OnCountPropertyChanging();
+
+ var removed = this.ToList();
+
+ _set.Clear();
+
+ OnCollectionChanged(ObservableHashSetSingletons.NoItems, removed);
+
+ OnCountPropertyChanged();
+ }
+
+ ///
+ /// Determines whether the hash set object contains the
+ /// specified element.
+ ///
+ /// The element to locate in the hash set.
+ ///
+ /// if the hash set contains the specified element; otherwise, .
+ ///
+ public virtual bool Contains(T item)
+ => _set.Contains(item);
+
+ ///
+ /// Copies the elements of the hash set to an array, starting at the specified array index.
+ ///
+ ///
+ /// The one-dimensional array that is the destination of the elements copied from
+ /// the hash set. The array must have zero-based indexing.
+ ///
+ /// The zero-based index in array at which copying begins.
+ public virtual void CopyTo(T[] array, int arrayIndex)
+ => _set.CopyTo(array, arrayIndex);
+
+ ///
+ /// Removes the specified element from the hash set.
+ ///
+ /// The element to remove.
+ ///
+ /// if the element is successfully found and removed; otherwise, .
+ ///
+ public virtual bool Remove(T item)
+ {
+ if (!_set.Contains(item))
+ {
+ return false;
+ }
+
+ OnCountPropertyChanging();
+
+ _set.Remove(item);
+
+ OnCollectionChanged(NotifyCollectionChangedAction.Remove, item);
+
+ OnCountPropertyChanged();
+
+ return true;
+ }
+
+ ///
+ /// Gets the number of elements that are contained in the hash set.
+ ///
+ public virtual int Count
+ => _set.Count;
+
+ ///
+ /// Gets a value indicating whether the hash set is read-only.
+ ///
+ public virtual bool IsReadOnly
+ => ((ICollection)_set).IsReadOnly;
+
+ ///
+ /// Returns an enumerator that iterates through the hash set.
+ ///
+ ///
+ /// An enumerator for the hash set.
+ ///
+ public virtual HashSet.Enumerator GetEnumerator()
+ => _set.GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+
+ ///
+ /// Adds the specified element to the hash set.
+ ///
+ /// The element to add to the set.
+ ///
+ /// if the element is added to the hash set; if the element is already present.
+ ///
+ public virtual bool Add(T item)
+ {
+ if (_set.Contains(item))
+ {
+ return false;
+ }
+
+ OnCountPropertyChanging();
+
+ _set.Add(item);
+
+ OnCollectionChanged(NotifyCollectionChangedAction.Add, item);
+
+ OnCountPropertyChanged();
+
+ return true;
+ }
+
+ ///
+ /// Modifies the hash set to contain all elements that are present in itself, the specified collection, or both.
+ ///
+ /// The collection to compare to the current hash set.
+ public virtual void UnionWith(IEnumerable other)
+ {
+ var copy = new HashSet(_set, _set.Comparer);
+
+ copy.UnionWith(other);
+
+ if (copy.Count == _set.Count)
+ {
+ return;
+ }
+
+ var added = copy.Where(i => !_set.Contains(i)).ToList();
+
+ OnCountPropertyChanging();
+
+ _set = copy;
+
+ OnCollectionChanged(added, ObservableHashSetSingletons.NoItems);
+
+ OnCountPropertyChanged();
+ }
+
+ ///
+ /// Modifies the current hash set to contain only
+ /// elements that are present in that object and in the specified collection.
+ ///
+ /// The collection to compare to the current hash set.
+ public virtual void IntersectWith(IEnumerable other)
+ {
+ var copy = new HashSet(_set, _set.Comparer);
+
+ copy.IntersectWith(other);
+
+ if (copy.Count == _set.Count)
+ {
+ return;
+ }
+
+ var removed = _set.Where(i => !copy.Contains(i)).ToList();
+
+ OnCountPropertyChanging();
+
+ _set = copy;
+
+ OnCollectionChanged(ObservableHashSetSingletons.NoItems, removed);
+
+ OnCountPropertyChanged();
+ }
+
+ ///
+ /// Removes all elements in the specified collection from the hash set.
+ ///
+ /// The collection of items to remove from the current hash set.
+ public virtual void ExceptWith(IEnumerable other)
+ {
+ var copy = new HashSet(_set, _set.Comparer);
+
+ copy.ExceptWith(other);
+
+ if (copy.Count == _set.Count)
+ {
+ return;
+ }
+
+ var removed = _set.Where(i => !copy.Contains(i)).ToList();
+
+ OnCountPropertyChanging();
+
+ _set = copy;
+
+ OnCollectionChanged(ObservableHashSetSingletons.NoItems, removed);
+
+ OnCountPropertyChanged();
+ }
+
+ ///
+ /// Modifies the current hash set to contain only elements that are present either in that
+ /// object or in the specified collection, but not both.
+ ///
+ /// The collection to compare to the current hash set.
+ public virtual void SymmetricExceptWith(IEnumerable other)
+ {
+ var copy = new HashSet(_set, _set.Comparer);
+
+ copy.SymmetricExceptWith(other);
+
+ var removed = _set.Where(i => !copy.Contains(i)).ToList();
+ var added = copy.Where(i => !_set.Contains(i)).ToList();
+
+ if (removed.Count == 0
+ && added.Count == 0)
+ {
+ return;
+ }
+
+ OnCountPropertyChanging();
+
+ _set = copy;
+
+ OnCollectionChanged(added, removed);
+
+ OnCountPropertyChanged();
+ }
+
+ ///
+ /// Determines whether the hash set is a subset of the specified collection.
+ ///
+ /// The collection to compare to the current hash set.
+ ///
+ /// if the hash set is a subset of other; otherwise, .
+ ///
+ public virtual bool IsSubsetOf(IEnumerable other)
+ => _set.IsSubsetOf(other);
+
+ ///
+ /// Determines whether the hash set is a proper subset of the specified collection.
+ ///
+ /// The collection to compare to the current hash set.
+ ///
+ /// if the hash set is a proper subset of other; otherwise, .
+ ///
+ public virtual bool IsProperSubsetOf(IEnumerable other)
+ => _set.IsProperSubsetOf(other);
+
+ ///
+ /// Determines whether the hash set is a superset of the specified collection.
+ ///
+ /// The collection to compare to the current hash set.
+ ///
+ /// if the hash set is a superset of other; otherwise, .
+ ///
+ public virtual bool IsSupersetOf(IEnumerable other)
+ => _set.IsSupersetOf(other);
+
+ ///
+ /// Determines whether the hash set is a proper superset of the specified collection.
+ ///
+ /// The collection to compare to the current hash set.
+ ///
+ /// if the hash set is a proper superset of other; otherwise, .
+ ///
+ public virtual bool IsProperSupersetOf(IEnumerable other)
+ => _set.IsProperSupersetOf(other);
+
+ ///
+ /// Determines whether the current System.Collections.Generic.HashSet`1 object and a specified collection share common elements.
+ ///
+ /// The collection to compare to the current hash set.
+ ///
+ /// if the hash set and other share at least one common element; otherwise, .
+ ///
+ public virtual bool Overlaps(IEnumerable other)
+ => _set.Overlaps(other);
+
+ ///
+ /// Determines whether the hash set and the specified collection contain the same elements.
+ ///
+ /// The collection to compare to the current hash set.
+ ///
+ /// if the hash set is equal to other; otherwise, .
+ ///
+ public virtual bool SetEquals(IEnumerable other)
+ => _set.SetEquals(other);
+
+ ///
+ /// Copies the elements of the hash set to an array.
+ ///
+ ///
+ /// The one-dimensional array that is the destination of the elements copied from
+ /// the hash set. The array must have zero-based indexing.
+ ///
+ public virtual void CopyTo(T[] array)
+ => _set.CopyTo(array);
+
+ ///
+ /// Copies the specified number of elements of the hash set to an array, starting at the specified array index.
+ ///
+ ///
+ /// The one-dimensional array that is the destination of the elements copied from
+ /// the hash set. The array must have zero-based indexing.
+ ///
+ /// The zero-based index in array at which copying begins.
+ /// The number of elements to copy to array.
+ public virtual void CopyTo(T[] array, int arrayIndex, int count)
+ => _set.CopyTo(array, arrayIndex, count);
+
+ ///
+ /// Removes all elements that match the conditions defined by the specified predicate
+ /// from the hash set.
+ ///
+ ///
+ /// The delegate that defines the conditions of the elements to remove.
+ ///
+ /// The number of elements that were removed from the hash set.
+ public virtual int RemoveWhere(Predicate match)
+ {
+ var copy = new HashSet(_set, _set.Comparer);
+
+ int removedCount = copy.RemoveWhere(match);
+
+ if (removedCount == 0)
+ {
+ return 0;
+ }
+
+ var removed = _set.Where(i => !copy.Contains(i)).ToList();
+
+ OnCountPropertyChanging();
+
+ _set = copy;
+
+ OnCollectionChanged(ObservableHashSetSingletons.NoItems, removed);
+
+ OnCountPropertyChanged();
+
+ return removedCount;
+ }
+
+ ///
+ /// Gets the object that is used to determine equality for the values in the set.
+ ///
+ public virtual IEqualityComparer Comparer
+ => _set.Comparer;
+
+ ///
+ /// Sets the capacity of the hash set to the actual number of elements it contains, rounded up to a nearby,
+ /// implementation-specific value.
+ ///
+ public virtual void TrimExcess()
+ => _set.TrimExcess();
+
+ ///
+ /// Raises the event.
+ ///
+ /// Details of the property that changed.
+ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+ => PropertyChanged?.Invoke(this, e);
+
+ ///
+ /// Raises the event.
+ ///
+ /// Details of the property that is changing.
+ protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
+ => PropertyChanging?.Invoke(this, e);
+
+ private void OnCountPropertyChanged()
+ => OnPropertyChanged(ObservableHashSetSingletons.CountPropertyChanged);
+
+ private void OnCountPropertyChanging()
+ => OnPropertyChanging(ObservableHashSetSingletons.CountPropertyChanging);
+
+ private void OnCollectionChanged(NotifyCollectionChangedAction action, object? item)
+ => OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item));
+
+ private void OnCollectionChanged(IList newItems, IList oldItems)
+ => OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, oldItems));
+
+ ///
+ /// Raises the event.
+ ///
+ /// Details of the change.
+ protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+ => CollectionChanged?.Invoke(this, e);
+}
+
+internal static class ObservableHashSetSingletons
+{
+ public static readonly PropertyChangedEventArgs CountPropertyChanged = new("Count");
+ public static readonly PropertyChangingEventArgs CountPropertyChanging = new("Count");
+
+ public static readonly object[] NoItems = Array.Empty