From c3fa22d739d44bf22f2d8ea1f401b5824dcf49ff Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 Jan 2019 10:46:42 -0500 Subject: [PATCH 01/33] Changing services to use GraphQL library --- .../Caches/AutoCompleteSourceCache.cs | 147 ------------------ .../Caches/IAutoCompleteSourceCache.cs | 17 -- src/GitHub.App/Caches/IIssuesCache.cs | 12 -- src/GitHub.App/Caches/IMentionsCache.cs | 14 -- src/GitHub.App/Caches/IssuesCache.cs | 80 ---------- src/GitHub.App/Caches/MentionsCache.cs | 50 ------ src/GitHub.App/Models/SuggestionItem.cs | 13 +- .../Services}/AutoCompleteAdvisor.cs | 21 +-- .../Services}/IAutoCompleteSource.cs | 4 +- .../Services}/IssuesAutoCompleteSource.cs | 51 +++--- .../Services/MentionsAutoCompleteSource.cs | 75 +++++++++ .../Helpers/EmojiAutoCompleteSource.cs | 41 ----- .../Helpers/MentionsAutoCompleteSource.cs | 77 --------- .../Views/AutoCompleteSuggestionView.xaml.cs | 1 + 14 files changed, 123 insertions(+), 480 deletions(-) delete mode 100644 src/GitHub.App/Caches/AutoCompleteSourceCache.cs delete mode 100644 src/GitHub.App/Caches/IAutoCompleteSourceCache.cs delete mode 100644 src/GitHub.App/Caches/IIssuesCache.cs delete mode 100644 src/GitHub.App/Caches/IMentionsCache.cs delete mode 100644 src/GitHub.App/Caches/IssuesCache.cs delete mode 100644 src/GitHub.App/Caches/MentionsCache.cs rename src/{GitHub.UI/Helpers => GitHub.App/Services}/AutoCompleteAdvisor.cs (88%) rename src/{GitHub.UI/Helpers => GitHub.App/Services}/IAutoCompleteSource.cs (75%) rename src/{GitHub.UI/Helpers => GitHub.App/Services}/IssuesAutoCompleteSource.cs (58%) create mode 100644 src/GitHub.App/Services/MentionsAutoCompleteSource.cs delete mode 100644 src/GitHub.UI/Helpers/EmojiAutoCompleteSource.cs delete mode 100644 src/GitHub.UI/Helpers/MentionsAutoCompleteSource.cs diff --git a/src/GitHub.App/Caches/AutoCompleteSourceCache.cs b/src/GitHub.App/Caches/AutoCompleteSourceCache.cs deleted file mode 100644 index 543ceef1b7..0000000000 --- a/src/GitHub.App/Caches/AutoCompleteSourceCache.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using GitHub.Api; -using GitHub.Extensions; -using GitHub.Helpers; -using GitHub.Models; -using GitHub.Services; -using ReactiveUI; - -namespace GitHub.Cache -{ - public abstract class AutoCompleteSourceCache : IAutoCompleteSourceCache - { - static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger(); - - readonly SerializedObservableProvider> serializedSuggestions; - readonly TimeSpan cacheDuration; - readonly TimeSpan maxCacheDuration; - - protected AutoCompleteSourceCache(TimeSpan cacheDuration, TimeSpan maxCacheDuration) - { - this.cacheDuration = cacheDuration; - this.maxCacheDuration = maxCacheDuration; - - serializedSuggestions = new SerializedObservableProvider>( - GetAndFetchCachedSourceItemsImpl); - } - - // We append this to the cache key to differentiate the various auto completion caches. - protected abstract string CacheSuffix { get; } - - /// - /// Retrieves suggestions from the cache for the specified repository. If not there, it makes an API - /// call to retrieve them. - /// - /// The repository that contains the suggestion items. - /// An observable containing a readonly list of auto complete suggestions - public IObservable> RetrieveSuggestions(IRepositoryModel repository) - { - Guard.ArgumentNotNull(repository, "repository"); - - return serializedSuggestions.Get(repository); - } - - /// - /// Calls the API to fetch mentionables, issues, or whatever. - /// - /// Existing items in the cache. Useful for incremental cache updates - /// The repository containing the items to fetch - /// The API client to use to make the request - protected abstract IObservable FetchSuggestionsSourceItems( - CachedData> existingCachedItems, - IRepositoryModel repository, - IApiClient apiClient); - - IObservable> GetAndFetchExistingCachedSourceItems( - IHostCache hostCache, - IRepositoryModel repository, - Func>, IObservable>> fetchFunc) - { - Debug.Assert(repository != null, "Repository cannot be null because we validated it at the callsite"); - Debug.Assert(hostCache != null, "HostCache cannot be null because we validated it at the callsite"); - - return GetOrFetchCachedItems(hostCache, repository, fetchFunc); - } - - IObservable> GetAndFetchCachedSourceItemsImpl(IRepositoryModel repository) - { - Debug.Assert(repository != null, "Repository cannot be null because we validated it at the callsite"); - - return Observable.Defer(() => - { - var hostCache = repository.RepositoryHost != null - ? repository.RepositoryHost.Cache - : null; - - if (hostCache == null) - { - return Observable.Empty>(); - } - - var ret = new ReplaySubject>(1); - - GetAndFetchExistingCachedSourceItems(hostCache, repository, GetCacheableSuggestions) - .Catch, Exception>(_ => Observable.Return(new List())) - .Multicast(ret) - .PermaRef(); - - // If GetAndFetchExistingCachedSourceItems finds that the cache item is stale it produces the - // stale value and then fetches and produces a fresh one. It can thus produce - // 1, 2 or no values (if cache miss and fetch fails). While I'd ideally want - // to expose that through this method so that the suggestions list in the UI get updated - // as soon as we have a fresh value this method has historically only produced - // one value so in an effort to reduce scope I'm keeping it that way. We - // unfortunately still need to maintain the subscription to GetAndRefresh though - // so that we don't cancel the refresh as soon as we get the stale object. - return ret.Take(1); - }); - } - - IObservable> GetCacheableSuggestions( - IRepositoryModel repository, - CachedData> existingCachedItems) - { - Debug.Assert(repository != null, "Repository cannot be null because we validated it at the callsite"); - - var apiClient = repository.RepositoryHost != null ? repository.RepositoryHost.ApiClient : null; - if (apiClient == null) - { - return Observable.Empty>(); - } - - // Our current serializer can't handle deserializing IReadOnlyList. That's why we need a concrete list - // here. - return FetchSuggestionsSourceItems(existingCachedItems, repository, apiClient) - .ToConcreteList(); - } - - IObservable> GetOrFetchCachedItems( - IHostCache hostCache, - IRepositoryModel repositoryModel, - Func>, IObservable>> fetchFunc) - where T : class - { - Ensure.ArgumentNotNull(repositoryModel, "repositoryModel"); - Ensure.ArgumentNotNull(fetchFunc, "fetchFunc"); - - string cacheKey = repositoryModel.NameWithOwner + ":" + CacheSuffix; - return hostCache.LocalMachine.GetCachedValueThenFetchForNextTime>( - cacheKey, - cacheData => fetchFunc(repositoryModel, cacheData), - cacheDuration, - maxCacheDuration) - .Catch, Exception>(ex => - { - log.Info(String.Format(CultureInfo.InvariantCulture, - "Exception occurred attempting to get a cached value and then fetch '{0}'", cacheKey), ex); - return Observable.Return(new List()); - }) - .Select(result => result ?? new List()); - } - } -} diff --git a/src/GitHub.App/Caches/IAutoCompleteSourceCache.cs b/src/GitHub.App/Caches/IAutoCompleteSourceCache.cs deleted file mode 100644 index ad50aa00ef..0000000000 --- a/src/GitHub.App/Caches/IAutoCompleteSourceCache.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using GitHub.Models; - -namespace GitHub.Cache -{ - public interface IAutoCompleteSourceCache - { - /// - /// Retrieves suggestions from the cache for the specified repository. If not there, it makes an API - /// call to retrieve them. - /// - /// The repository that contains the users - /// An observable containing a readonly list of issue suggestions - IObservable> RetrieveSuggestions(IRepositoryModel repositoryModel); - } -} diff --git a/src/GitHub.App/Caches/IIssuesCache.cs b/src/GitHub.App/Caches/IIssuesCache.cs deleted file mode 100644 index d99bcc64e9..0000000000 --- a/src/GitHub.App/Caches/IIssuesCache.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -namespace GitHub.Cache -{ - /// - /// Used to cache and supply #issues in the autocomplete control. - /// - [SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", - Justification = "Yeah, it's empty, but it makes it easy to import the correct one.")] - public interface IIssuesCache : IAutoCompleteSourceCache - { - } -} \ No newline at end of file diff --git a/src/GitHub.App/Caches/IMentionsCache.cs b/src/GitHub.App/Caches/IMentionsCache.cs deleted file mode 100644 index b0ef8a0391..0000000000 --- a/src/GitHub.App/Caches/IMentionsCache.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using GitHub.Cache; - -namespace GitHub.Helpers -{ - /// - /// Used to cache and supply @mentions in the autocomplete control. - /// - [SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", - Justification = "Yeah, it's empty, but it makes it easy to import the correct one.")] - public interface IMentionsCache : IAutoCompleteSourceCache - { - } -} \ No newline at end of file diff --git a/src/GitHub.App/Caches/IssuesCache.cs b/src/GitHub.App/Caches/IssuesCache.cs deleted file mode 100644 index 96dd3407e9..0000000000 --- a/src/GitHub.App/Caches/IssuesCache.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Linq; -using System.Reactive.Linq; -using GitHub.Api; -using GitHub.Extensions; -using GitHub.Models; -using GitHub.ViewModels; -using Octokit; - -namespace GitHub.Cache -{ - [Export(typeof(IIssuesCache))] - [Export(typeof(IAutoCompleteSourceCache))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class IssuesCache : AutoCompleteSourceCache, IIssuesCache - { - // Just needs to be some value before GitHub stored its first issue. - static readonly DateTimeOffset lowerBound = new DateTimeOffset(2000, 1, 1, 12, 0, 0, TimeSpan.FromSeconds(0)); - - [ImportingConstructor] - public IssuesCache() : base(TimeSpan.FromSeconds(10), TimeSpan.FromDays(7)) - { - } - - protected override string CacheSuffix - { - get { return "issues"; } - } - - protected override IObservable FetchSuggestionsSourceItems( - CachedData> existingCachedItems, - IRepositoryModel repository, - IApiClient apiClient) - { - var data = (existingCachedItems.Data ?? new List()) - .Where(item => !String.IsNullOrEmpty(item.Name)) // Helps handle cache corruption - .ToList(); - - if (data.IsEmpty()) - { - return apiClient.GetIssuesForRepository(repository.Owner, repository.Name) - .Select(ConvertToSuggestionItem); - } - - // Update cache with changes - var since = data.Max(issue => issue.LastModifiedDate ?? lowerBound).ToUniversalTime(); - var existingIssues = data.ToDictionary(i => i.Name, i => i); - return apiClient.GetIssuesChangedSince(repository.Owner, repository.Name, since) - .WhereNotNull() - .Do(issue => - { - var suggestionItem = ConvertToSuggestionItem(issue); - // Remove closed ones. - if (issue.State == ItemState.Closed) - { - existingIssues.Remove(suggestionItem.Name); - } - else - { - // Adds new ones (this is basically a noop for existing ones) - existingIssues[suggestionItem.Name] = suggestionItem; - } - }) - .ToList() // We always want to return existing issues. - .SelectMany(_ => existingIssues.Values.ToObservable()); - } - - static SuggestionItem ConvertToSuggestionItem(Issue issue) - { - return new SuggestionItem("#" + issue.Number, issue.Title) - { - // Just in case CreatedAt isn't set, we'll use UTCNow. - LastModifiedDate = issue.UpdatedAt - ?? (issue.CreatedAt == DateTimeOffset.MinValue ? DateTimeOffset.UtcNow : issue.CreatedAt) - }; - } - } -} diff --git a/src/GitHub.App/Caches/MentionsCache.cs b/src/GitHub.App/Caches/MentionsCache.cs deleted file mode 100644 index 1c2872bae6..0000000000 --- a/src/GitHub.App/Caches/MentionsCache.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Reactive.Linq; -using GitHub.Api; -using GitHub.Helpers; -using GitHub.Models; -using Octokit; - -namespace GitHub.Cache -{ - /// - /// Used to cache and supply @mentions in the autocomplete control. - /// - [Export(typeof(IMentionsCache))] - [Export(typeof(IAutoCompleteSourceCache))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class MentionsCache : AutoCompleteSourceCache, IMentionsCache - { - public MentionsCache() : base(TimeSpan.FromHours(12), TimeSpan.FromDays(7)) - { - } - - protected override string CacheSuffix - { - get { return "mentions"; } - } - - protected override IObservable FetchSuggestionsSourceItems( - CachedData> existingCachedItems, - IRepositoryModel repository, - IApiClient apiClient) - { - return apiClient.GetMentionables(repository.Owner, repository.Name) - .Select(ConvertToSuggestionItem); - } - - static SuggestionItem ConvertToSuggestionItem(AccountMention sourceItem) - { - return new SuggestionItem(sourceItem.Login, sourceItem.Name ?? "(unknown)", GetUrlSafe(sourceItem.AvatarUrl)); - } - - static Uri GetUrlSafe(string url) - { - Uri uri; - Uri.TryCreate(url, UriKind.Absolute, out uri); - return uri; - } - } -} diff --git a/src/GitHub.App/Models/SuggestionItem.cs b/src/GitHub.App/Models/SuggestionItem.cs index 0b8bd59644..17bdfa2525 100644 --- a/src/GitHub.App/Models/SuggestionItem.cs +++ b/src/GitHub.App/Models/SuggestionItem.cs @@ -1,4 +1,5 @@ using System; +using GitHub.Extensions; using GitHub.Helpers; namespace GitHub.Models @@ -15,8 +16,8 @@ public SuggestionItem() // So this can be deserialized from cache public SuggestionItem(string name, Uri iconCacheKey) { - Ensure.ArgumentNotNullOrEmptyString(name, "name"); - Ensure.ArgumentNotNull(iconCacheKey, "iconCacheKey"); + Guard.ArgumentNotEmptyString(name, "name"); + Guard.ArgumentNotNull(iconCacheKey, "iconCacheKey"); Name = name; IconKey = iconCacheKey; @@ -24,8 +25,8 @@ public SuggestionItem(string name, Uri iconCacheKey) public SuggestionItem(string name, string description) { - Ensure.ArgumentNotNullOrEmptyString(name, "name"); - Ensure.ArgumentNotNullOrEmptyString(description, "description"); + Guard.ArgumentNotEmptyString(name, "name"); + Guard.ArgumentNotEmptyString(description, "description"); Name = name; Description = description; @@ -33,8 +34,8 @@ public SuggestionItem(string name, string description) public SuggestionItem(string name, string description, Uri iconCacheKey) { - Ensure.ArgumentNotNullOrEmptyString(name, "name"); - Ensure.ArgumentNotNull(iconCacheKey, "iconCacheKey"); + Guard.ArgumentNotEmptyString(name, "name"); + Guard.ArgumentNotNull(iconCacheKey, "iconCacheKey"); Name = name; Description = description; diff --git a/src/GitHub.UI/Helpers/AutoCompleteAdvisor.cs b/src/GitHub.App/Services/AutoCompleteAdvisor.cs similarity index 88% rename from src/GitHub.UI/Helpers/AutoCompleteAdvisor.cs rename to src/GitHub.App/Services/AutoCompleteAdvisor.cs index 1c06d0ac96..031392d581 100644 --- a/src/GitHub.UI/Helpers/AutoCompleteAdvisor.cs +++ b/src/GitHub.App/Services/AutoCompleteAdvisor.cs @@ -7,10 +7,13 @@ using System.Globalization; using System.Linq; using System.Reactive.Linq; +using GitHub.Extensions; +using GitHub.Logging; +using GitHub.Models; using GitHub.UI; -using NLog; +using Serilog; -namespace GitHub.Helpers +namespace GitHub.Services { [Export(typeof(IAutoCompleteAdvisor))] [PartCreationPolicy(CreationPolicy.Shared)] @@ -18,7 +21,7 @@ public class AutoCompleteAdvisor : IAutoCompleteAdvisor { const int SuggestionCount = 5; // The number of suggestions we'll provide. github.com does 5. - static readonly Logger log = LogManager.GetCurrentClassLogger(); + static readonly ILogger log = LogManager.ForContext(); readonly Lazy> prefixSourceMap; [ImportingConstructor] @@ -30,7 +33,7 @@ public AutoCompleteAdvisor([ImportMany]IEnumerable autocomp public IObservable GetAutoCompletionSuggestions(string text, int caretPosition) { - Ensure.ArgumentNotNull("text", text); + Guard.ArgumentNotNull("text", text); if (caretPosition < 0 || caretPosition > text.Length) { @@ -73,7 +76,7 @@ public IObservable GetAutoCompletionSuggestions(string text, new ReadOnlyCollection(suggestions))) .Catch(e => { - log.Info(e); + log.Error(e, "Error Getting AutoCompleteResult"); return Observable.Return(AutoCompleteResult.Empty); }); } @@ -82,8 +85,8 @@ public IObservable GetAutoCompletionSuggestions(string text, , Justification = "We ensure the argument is greater than -1 so it can't overflow")] public static AutoCompletionToken ParseAutoCompletionToken(string text, int caretPosition, string triggerPrefix) { - Ensure.ArgumentNotNull("text", text); - Ensure.ArgumentInRange(caretPosition, 0, text.Length, "caretPosition"); + Guard.ArgumentNotNull("text", text); + Guard.ArgumentInRange(caretPosition, 0, text.Length, "caretPosition"); if (caretPosition == 0 || text.Length == 0) return null; // :th : 1 @@ -103,8 +106,8 @@ public class AutoCompletionToken { public AutoCompletionToken(string searchPrefix, int offset) { - Ensure.ArgumentNotNull(searchPrefix, "searchPrefix"); - Ensure.ArgumentNonNegative(offset, "offset"); + Guard.ArgumentNotNull(searchPrefix, "searchPrefix"); + Guard.ArgumentNonNegative(offset, "offset"); SearchSearchPrefix = searchPrefix; Offset = offset; diff --git a/src/GitHub.UI/Helpers/IAutoCompleteSource.cs b/src/GitHub.App/Services/IAutoCompleteSource.cs similarity index 75% rename from src/GitHub.UI/Helpers/IAutoCompleteSource.cs rename to src/GitHub.App/Services/IAutoCompleteSource.cs index 2b7b80688e..8d54799c6e 100644 --- a/src/GitHub.UI/Helpers/IAutoCompleteSource.cs +++ b/src/GitHub.App/Services/IAutoCompleteSource.cs @@ -1,7 +1,9 @@ using System; +using System.Threading.Tasks; +using GitHub.Models; using GitHub.UI; -namespace GitHub.Helpers +namespace GitHub.Services { public interface IAutoCompleteSource { diff --git a/src/GitHub.UI/Helpers/IssuesAutoCompleteSource.cs b/src/GitHub.App/Services/IssuesAutoCompleteSource.cs similarity index 58% rename from src/GitHub.UI/Helpers/IssuesAutoCompleteSource.cs rename to src/GitHub.App/Services/IssuesAutoCompleteSource.cs index 1e67271309..41c311d79b 100644 --- a/src/GitHub.UI/Helpers/IssuesAutoCompleteSource.cs +++ b/src/GitHub.App/Services/IssuesAutoCompleteSource.cs @@ -3,44 +3,47 @@ using System.ComponentModel.Composition; using System.Linq; using System.Reactive.Linq; -using GitHub.Cache; +using System.Threading.Tasks; +using GitHub.Api; +using GitHub.Extensions; using GitHub.Models; -using GitHub.UI; -using GitHub.ViewModels; +using GitHub.Primitives; +using Octokit.GraphQL; -namespace GitHub.Helpers +namespace GitHub.Services { [Export(typeof(IAutoCompleteSource))] [PartCreationPolicy(CreationPolicy.Shared)] public class IssuesAutoCompleteSource : IAutoCompleteSource { - readonly Lazy issuesCache; - readonly Lazy currentRepositoryState; + readonly LocalRepositoryModel localRepositoryModel; + readonly IGraphQLClientFactory graphqlFactory; [ImportingConstructor] - public IssuesAutoCompleteSource( - Lazy issuesCache, - Lazy currentRepositoryState) + public IssuesAutoCompleteSource(LocalRepositoryModel localRepositoryModel, IGraphQLClientFactory graphqlFactory) { - Ensure.ArgumentNotNull(issuesCache, "issuesCache"); - Ensure.ArgumentNotNull(currentRepositoryState, "currentRepositoryState"); + Guard.ArgumentNotNull(localRepositoryModel, nameof(localRepositoryModel)); + Guard.ArgumentNotNull(graphqlFactory, nameof(graphqlFactory)); - this.issuesCache = issuesCache; - this.currentRepositoryState = currentRepositoryState; + this.localRepositoryModel = localRepositoryModel; + this.graphqlFactory = graphqlFactory; } public IObservable GetSuggestions() { - if (CurrentRepository.RepositoryHost == null) - { - return Observable.Empty(); - } + var query = new Query().Repository(owner: localRepositoryModel.Owner, name: localRepositoryModel.Name) + .Select(repository => + repository.Issues(null, null, null, null, null, null, null) + .AllPages() + .Select(issue => new SuggestionItem("#" + issue.Number, issue.Title)) + .ToList()); - return IssuesCache.RetrieveSuggestions(CurrentRepository) - .Catch, Exception>(_ => Observable.Empty>()) - .SelectMany(x => x.ToObservable()) - .Where(suggestion => !String.IsNullOrEmpty(suggestion.Name)) // Just being extra cautious - .Select(suggestion => new IssueAutoCompleteSuggestion(suggestion, Prefix)); + return Observable.FromAsync(async () => + { + var connection = await graphqlFactory.CreateConnection(HostAddress.Create(localRepositoryModel.CloneUrl.Host)); + var suggestions = await connection.Run(query); + return suggestions.Select(suggestion => new IssueAutoCompleteSuggestion(suggestion, Prefix)); + }).SelectMany(enumerable => enumerable); } public string Prefix @@ -48,10 +51,6 @@ public string Prefix get { return "#"; } } - IIssuesCache IssuesCache { get { return issuesCache.Value; } } - - IRepositoryModel CurrentRepository { get { return currentRepositoryState.Value.SelectedRepository; } } - class IssueAutoCompleteSuggestion : AutoCompleteSuggestion { // Just needs to be some value before GitHub stored its first issue. diff --git a/src/GitHub.App/Services/MentionsAutoCompleteSource.cs b/src/GitHub.App/Services/MentionsAutoCompleteSource.cs new file mode 100644 index 0000000000..2a95c39741 --- /dev/null +++ b/src/GitHub.App/Services/MentionsAutoCompleteSource.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using GitHub.Api; +using GitHub.Caches; +using GitHub.Extensions; +using GitHub.Models; +using GitHub.Primitives; +using Octokit.GraphQL; + +namespace GitHub.Services +{ + /// + /// Supplies @mentions auto complete suggestions. + /// + [Export(typeof(IAutoCompleteSource))] + [PartCreationPolicy(CreationPolicy.Shared)] + public class MentionsAutoCompleteSource : IAutoCompleteSource + { + readonly LocalRepositoryModel localRepositoryModel; + readonly IGraphQLClientFactory graphqlFactory; + readonly IAvatarProvider avatarProvider; + + [ImportingConstructor] + public MentionsAutoCompleteSource(LocalRepositoryModel localRepositoryModel, + IGraphQLClientFactory graphqlFactory, + IAvatarProvider avatarProvider) + { + Guard.ArgumentNotNull(localRepositoryModel, nameof(localRepositoryModel)); + Guard.ArgumentNotNull(graphqlFactory, nameof(graphqlFactory)); + Guard.ArgumentNotNull(avatarProvider, nameof(avatarProvider)); + + this.localRepositoryModel = localRepositoryModel; + this.graphqlFactory = graphqlFactory; + this.avatarProvider = avatarProvider; + } + + public IObservable GetSuggestions() + { + var query = new Query().Repository(owner: localRepositoryModel.Owner, name: localRepositoryModel.Name) + .Select(repository => + repository.MentionableUsers(null, null, null, null) + .AllPages() + .Select(sourceItem => + new SuggestionItem(sourceItem.Login, + sourceItem.Name ?? "(unknown)", + GetUrlSafe(sourceItem.AvatarUrl(null)))) + .ToList()); + + return Observable.FromAsync(async () => + { + var connection = await graphqlFactory.CreateConnection(HostAddress.Create(localRepositoryModel.CloneUrl.Host)); + var suggestions = await connection.Run(query); + return suggestions.Select(suggestion => new AutoCompleteSuggestion(suggestion.Name, + suggestion.Description, + ResolveImage(suggestion.IconKey.ToString()), + Prefix)); + }).SelectMany(enumerable => enumerable); + } + + private IObservable ResolveImage(string uri) => avatarProvider.GetAvatar(uri); + + public string Prefix => "@"; + + static Uri GetUrlSafe(string url) + { + Uri uri; + Uri.TryCreate(url, UriKind.Absolute, out uri); + return uri; + } + } +} diff --git a/src/GitHub.UI/Helpers/EmojiAutoCompleteSource.cs b/src/GitHub.UI/Helpers/EmojiAutoCompleteSource.cs deleted file mode 100644 index 2dbf7d0750..0000000000 --- a/src/GitHub.UI/Helpers/EmojiAutoCompleteSource.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.ComponentModel.Composition; -using System.Linq; -using System.Reactive.Linq; -using System.Windows.Media.Imaging; -using GitHub.UI; - -namespace GitHub.Helpers -{ - [Export(typeof(IAutoCompleteSource))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class EmojiAutoCompleteSource : IAutoCompleteSource - { - readonly IEmojiCache emojiCache; - - [ImportingConstructor] - public EmojiAutoCompleteSource(IEmojiCache emojiCache) - { - Ensure.ArgumentNotNull(emojiCache, "emojiCache"); - - this.emojiCache = emojiCache; - } - - public IObservable GetSuggestions() - { - Func> resolveImage = uri => - Observable.Defer(() => - { - var resourcePath = "pack://application:,,,/GitHub;component/" + uri; - return Observable.Return(App.CreateBitmapImage(resourcePath)); - }); - - return emojiCache.GetEmojis() - .Where(emoji => !String.IsNullOrEmpty(emoji.Name)) // Just being extra cautious. - .Select(emoji => new AutoCompleteSuggestion(emoji.Name, resolveImage(emoji.IconKey), ":", ":")) - .ToObservable(); - } - - public string Prefix { get { return ":"; } } - } -} diff --git a/src/GitHub.UI/Helpers/MentionsAutoCompleteSource.cs b/src/GitHub.UI/Helpers/MentionsAutoCompleteSource.cs deleted file mode 100644 index c1453759ce..0000000000 --- a/src/GitHub.UI/Helpers/MentionsAutoCompleteSource.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Reactive.Linq; -using System.Windows.Media.Imaging; -using GitHub.Cache; -using GitHub.Caches; -using GitHub.Extensions; -using GitHub.Models; -using GitHub.Services; -using GitHub.UI; -using GitHub.ViewModels; - -namespace GitHub.Helpers -{ - /// - /// Supplies @mentions auto complete suggestions. - /// - [Export(typeof(IAutoCompleteSource))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class MentionsAutoCompleteSource : IAutoCompleteSource - { - readonly Lazy mentionsCache; - readonly Lazy currentRepositoryState; - readonly Lazy imageCache; - readonly IAvatarProvider hostAvatarProvider; - - [ImportingConstructor] - public MentionsAutoCompleteSource( - Lazy mentionsCache, - Lazy imageCache, - IAvatarProvider hostAvatarProvider) - { - Guard.ArgumentNotNull(mentionsCache, "mentionsCache"); - Guard.ArgumentNotNull(currentRepositoryState, "currentRepositoryState"); - Guard.ArgumentNotNull(imageCache, "imageCache"); - Guard.ArgumentNotNull(hostAvatarProvider, "hostAvatarProvider"); - - this.mentionsCache = mentionsCache; - this.currentRepositoryState = currentRepositoryState; - this.imageCache = imageCache; - this.hostAvatarProvider = hostAvatarProvider; - } - - public IObservable GetSuggestions() - { - if (CurrentRepository.RepositoryHost == null) - { - return Observable.Empty(); - } - - var avatarProviderKey = CurrentRepository.RepositoryHost.Address.WebUri.ToString(); - var avatarProvider = hostAvatarProvider.Get(avatarProviderKey); - - Func> resolveImage = uri => - Observable.Defer(() => ImageCache - .GetImage(uri) - .Catch(_ => Observable.Return(avatarProvider.DefaultUserBitmapImage)) - .StartWith(avatarProvider.DefaultUserBitmapImage)); - - return MentionsCache.RetrieveSuggestions(CurrentRepository) - .Catch, Exception>(_ => Observable.Empty>()) - .SelectMany(x => x.ToObservable()) - .Where(suggestion => !String.IsNullOrEmpty(suggestion.Name)) // Just being extra cautious - .Select(suggestion => - new AutoCompleteSuggestion(suggestion.Name, suggestion.Description, resolveImage(suggestion.IconKey), Prefix)); - } - - public string Prefix { get { return "@"; } } - - IImageCache ImageCache { get { return imageCache.Value; } } - - IMentionsCache MentionsCache { get { return mentionsCache.Value; } } - - RepositoryModel CurrentRepository { get { return currentRepositoryState.Value.SelectedRepository; } } - } -} diff --git a/src/GitHub.UI/Views/AutoCompleteSuggestionView.xaml.cs b/src/GitHub.UI/Views/AutoCompleteSuggestionView.xaml.cs index d0c52b33f7..d76f4fb316 100644 --- a/src/GitHub.UI/Views/AutoCompleteSuggestionView.xaml.cs +++ b/src/GitHub.UI/Views/AutoCompleteSuggestionView.xaml.cs @@ -2,6 +2,7 @@ using System.Windows; using GitHub.Extensions; using GitHub.Extensions.Reactive; +using GitHub.Models; using GitHub.UI; using ReactiveUI; From 9e4d7f963de1410aec2c62c4383a1a8f5675feb7 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 Jan 2019 10:47:51 -0500 Subject: [PATCH 02/33] Starting to implement autocomplete functionality --- .../GitHubPane/PullRequestCreationView.xaml | 41 +++++++++++++------ .../PullRequestCreationViewModelTests.cs | 21 ++++++---- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml index ba748d1eef..e3d1c4b224 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml @@ -149,18 +149,35 @@ SpellCheck.IsEnabled="True" AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.PullRequestCreationTitleTextBox}"/> - - + + + + + + + + + + + + + (); var api = Substitute.For(); var ms = Substitute.For(); + var autoCompleteAdvisor = Substitute.For(); connection.HostAddress.Returns(HostAddress.Create("https://github.com")); @@ -121,7 +123,8 @@ static TestData PrepareTestData( NotificationService = notifications, Connection = connection, ApiClient = api, - ModelService = ms + ModelService = ms, + AutoCompleteAdvisor = autoCompleteAdvisor }; } @@ -147,7 +150,7 @@ public async Task TargetBranchDisplayNameIncludesRepoOwnerWhenForkAsync() var prservice = new PullRequestService(data.GitClient, data.GitService, Substitute.For(), Substitute.For(), data.ServiceProvider.GetOperatingSystem(), Substitute.For()); prservice.GetPullRequestTemplate(data.ActiveRepo).Returns(Observable.Empty()); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, - Substitute.For(), data.GitService); + Substitute.For(), data.GitService, data.AutoCompleteAdvisor); await vm.InitializeAsync(data.ActiveRepo, data.Connection); Assert.That("octokit/master", Is.EqualTo(vm.TargetBranch.DisplayName)); } @@ -183,7 +186,7 @@ public async Task CreatingPRsAsync( var prservice = new PullRequestService(data.GitClient, data.GitService, Substitute.For(), Substitute.For(), data.ServiceProvider.GetOperatingSystem(), Substitute.For()); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, - Substitute.For(), data.GitService); + Substitute.For(), data.GitService, data.AutoCompleteAdvisor); await vm.InitializeAsync(data.ActiveRepo, data.Connection); // the TargetBranch property gets set to whatever the repo default is (we assume master here), @@ -226,7 +229,7 @@ public async Task TemplateIsUsedIfPresentAsync() prservice.GetPullRequestTemplate(data.ActiveRepo).Returns(Observable.Return("Test PR template")); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, - Substitute.For(), data.GitService); + Substitute.For(), data.GitService, data.AutoCompleteAdvisor); await vm.InitializeAsync(data.ActiveRepo, data.Connection); Assert.That("Test PR template", Is.EqualTo(vm.Description)); @@ -246,7 +249,7 @@ public async Task LoadsDraft() var prservice = Substitute.For(); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, - draftStore, data.GitService); + draftStore, data.GitService, data.AutoCompleteAdvisor); await vm.InitializeAsync(data.ActiveRepo, data.Connection); Assert.That(vm.PRTitle, Is.EqualTo("This is a Title.")); @@ -261,7 +264,7 @@ public async Task UpdatesDraftWhenDescriptionChanges() var draftStore = Substitute.For(); var prservice = Substitute.For(); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, - draftStore, data.GitService, scheduler); + draftStore, data.GitService, data.AutoCompleteAdvisor, scheduler); await vm.InitializeAsync(data.ActiveRepo, data.Connection); vm.Description = "Body changed."; @@ -284,7 +287,7 @@ public async Task UpdatesDraftWhenTitleChanges() var draftStore = Substitute.For(); var prservice = Substitute.For(); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, - draftStore, data.GitService, scheduler); + draftStore, data.GitService, data.AutoCompleteAdvisor, scheduler); await vm.InitializeAsync(data.ActiveRepo, data.Connection); vm.PRTitle = "Title changed."; @@ -307,7 +310,7 @@ public async Task DeletesDraftWhenPullRequestSubmitted() var draftStore = Substitute.For(); var prservice = Substitute.For(); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, draftStore, - data.GitService, scheduler); + data.GitService, data.AutoCompleteAdvisor, scheduler); await vm.InitializeAsync(data.ActiveRepo, data.Connection); await vm.CreatePullRequest.Execute(); @@ -323,7 +326,7 @@ public async Task DeletesDraftWhenCanceled() var draftStore = Substitute.For(); var prservice = Substitute.For(); var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, draftStore, - data.GitService, scheduler); + data.GitService, data.AutoCompleteAdvisor, scheduler); await vm.InitializeAsync(data.ActiveRepo, data.Connection); await vm.Cancel.Execute(); From 8906c54b4faf2a10d15cf782881d5617f42f98e3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 Jan 2019 10:59:41 -0500 Subject: [PATCH 03/33] Revert "Undoing changes to PullRequestCreationViewModel" This reverts commit 653cc79c6cb098417f69b608af19edea82aaf4a0. --- .../GitHubPane/PullRequestCreationViewModel.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs index 89f8e37c50..5450fb970e 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs @@ -18,6 +18,7 @@ using GitHub.Models.Drafts; using GitHub.Primitives; using GitHub.Services; +using GitHub.UI; using GitHub.Validation; using Octokit; using ReactiveUI; @@ -51,8 +52,9 @@ public PullRequestCreationViewModel( IPullRequestService service, INotificationService notifications, IMessageDraftStore draftStore, - IGitService gitService) - : this(modelServiceFactory, service, notifications, draftStore, gitService, DefaultScheduler.Instance) + IGitService gitService, + IAutoCompleteAdvisor autoCompleteAdvisor) + : this(modelServiceFactory, service, notifications, draftStore, gitService, autoCompleteAdvisor, DefaultScheduler.Instance) { } @@ -62,6 +64,7 @@ public PullRequestCreationViewModel( INotificationService notifications, IMessageDraftStore draftStore, IGitService gitService, + IAutoCompleteAdvisor autoCompleteAdvisor, IScheduler timerScheduler) { Guard.ArgumentNotNull(modelServiceFactory, nameof(modelServiceFactory)); @@ -69,12 +72,14 @@ public PullRequestCreationViewModel( Guard.ArgumentNotNull(notifications, nameof(notifications)); Guard.ArgumentNotNull(draftStore, nameof(draftStore)); Guard.ArgumentNotNull(gitService, nameof(gitService)); + Guard.ArgumentNotNull(autoCompleteAdvisor, nameof(autoCompleteAdvisor)); Guard.ArgumentNotNull(timerScheduler, nameof(timerScheduler)); this.service = service; this.modelServiceFactory = modelServiceFactory; this.draftStore = draftStore; this.gitService = gitService; + this.AutoCompleteAdvisor = autoCompleteAdvisor; this.timerScheduler = timerScheduler; this.WhenAnyValue(x => x.Branches) @@ -334,8 +339,9 @@ protected string GetDraftKey() SourceBranch.Name); } - public RemoteRepositoryModel GitHubRepository { get { return githubRepository?.Value; } } - bool IsExecuting { get { return isExecuting.Value; } } + public RemoteRepositoryModel GitHubRepository => githubRepository?.Value; + bool IsExecuting => isExecuting.Value; + public IAutoCompleteAdvisor AutoCompleteAdvisor { get; } bool initialized; bool Initialized From bfa18c619ec6a02b9689b6b31a0c40672a7324c0 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 Jan 2019 11:09:03 -0500 Subject: [PATCH 04/33] Undoing some changes --- .../ViewModels/GitHubPane/PullRequestCreationViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs index 5450fb970e..bd25c85c52 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs @@ -339,8 +339,8 @@ protected string GetDraftKey() SourceBranch.Name); } - public RemoteRepositoryModel GitHubRepository => githubRepository?.Value; - bool IsExecuting => isExecuting.Value; + public RemoteRepositoryModel GitHubRepository { get { return githubRepository?.Value; } } + bool IsExecuting { get { return isExecuting.Value; } } public IAutoCompleteAdvisor AutoCompleteAdvisor { get; } bool initialized; From 2c947487771d7038b02013b342785462b9269626 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Sat, 5 Jan 2019 10:43:14 -0500 Subject: [PATCH 05/33] Trying to display the control correctly --- .../GitHubPane/PullRequestCreationView.xaml | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml index e3d1c4b224..9b1c35db1f 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCreationView.xaml @@ -149,13 +149,24 @@ SpellCheck.IsEnabled="True" AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.PullRequestCreationTitleTextBox}"/> + + + Grid.Row="3" + MinHeight="100" + Margin="10,5" + Advisor="{Binding AutoCompleteAdvisor}" + > @@ -163,17 +174,13 @@ - + SpellCheck.IsEnabled="True"/> From 027b32254c08caa1544ade927dbc13c6286bd694 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Sat, 5 Jan 2019 10:43:40 -0500 Subject: [PATCH 06/33] Exposing Advisor as a dependency property --- .../AutoCompleteBox/AutoCompleteBox.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/GitHub.UI/Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/GitHub.UI/Controls/AutoCompleteBox/AutoCompleteBox.cs index 7c6075caa0..c96b73c2ee 100644 --- a/src/GitHub.UI/Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/GitHub.UI/Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -740,6 +740,34 @@ public IAutoCompleteAdvisor Advisor set; } + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DependencyProperty AdvisorProperty = + DependencyProperty.Register( + "Advisor", + typeof(IAutoCompleteAdvisor), + typeof(AutoCompleteBox), + new PropertyMetadata(null, OnAdvisorPropertyChanged)); + + /// + /// AdvisorProperty property changed handler. + /// + /// AutoCompleteBox that changed its Advisor. + /// Event arguments. + private static void OnAdvisorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var source = d as AutoCompleteBox; + if (source == null) return; + + source.Advisor = (IAutoCompleteAdvisor)e.NewValue; + } + /// /// Builds the visual tree for the /// control From 9d2ee84e80dd415819719bd1db6c79d7a92b2be5 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 Jan 2019 11:06:34 -0500 Subject: [PATCH 07/33] Including missing themes --- .../Assets/Controls/AutoCompleteBox.xaml | 20 +++---- .../Styles/ThemeBlue.xaml | 52 +++++++++++++++++++ .../Styles/ThemeDark.xaml | 52 +++++++++++++++++++ .../Styles/ThemeLight.xaml | 52 +++++++++++++++++++ 4 files changed, 166 insertions(+), 10 deletions(-) diff --git a/src/GitHub.UI/Assets/Controls/AutoCompleteBox.xaml b/src/GitHub.UI/Assets/Controls/AutoCompleteBox.xaml index 1a1a950e6b..2283b4b9a1 100644 --- a/src/GitHub.UI/Assets/Controls/AutoCompleteBox.xaml +++ b/src/GitHub.UI/Assets/Controls/AutoCompleteBox.xaml @@ -65,11 +65,11 @@ - - + + - + @@ -81,23 +81,23 @@ - + - - + + - - + + @@ -113,8 +113,8 @@ - - - - - + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + Submit review diff --git a/src/GitHub.VisualStudio.UI/Views/PullRequestReviewCommentView.xaml b/src/GitHub.VisualStudio.UI/Views/PullRequestReviewCommentView.xaml index 41597a44dc..eab0a78381 100644 --- a/src/GitHub.VisualStudio.UI/Views/PullRequestReviewCommentView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/PullRequestReviewCommentView.xaml @@ -161,39 +161,51 @@ - - - - - - - + + + + + + + + + (); + autoCompleteAdvisor = autoCompleteAdvisor ?? Substitute.For(); - return new IssueishCommentViewModel(commentService); + return new IssueishCommentViewModel(commentService, autoCompleteAdvisor); } } } diff --git a/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentThreadViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentThreadViewModelTests.cs index 72e2c88536..2420120dea 100644 --- a/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentThreadViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentThreadViewModelTests.cs @@ -172,8 +172,9 @@ static IViewViewModelFactory CreateFactory() { var result = Substitute.For(); var commentService = Substitute.For(); + var autoCompleteAdvisor = Substitute.For(); result.CreateViewModel().Returns(_ => - new PullRequestReviewCommentViewModel(commentService)); + new PullRequestReviewCommentViewModel(commentService, autoCompleteAdvisor)); return result; } diff --git a/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs index 838d6fc8ca..d6d2fe8043 100644 --- a/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs @@ -54,7 +54,8 @@ public async Task CanBeExecutedForPlaceholders() var thread = CreateThread(); var currentUser = Substitute.For(); var commentService = Substitute.For(); - var target = new PullRequestReviewCommentViewModel(commentService); + var autoCompleteAdvisor = Substitute.For(); + var target = new PullRequestReviewCommentViewModel(commentService, autoCompleteAdvisor); await target.InitializeAsPlaceholderAsync(session, thread, false, false); @@ -98,7 +99,8 @@ public async Task CannotBeExecutedForPlaceholders() var thread = CreateThread(); var currentUser = Substitute.For(); var commentService = Substitute.For(); - var target = new PullRequestReviewCommentViewModel(commentService); + var autoCompleteAdvisor = Substitute.For(); + var target = new PullRequestReviewCommentViewModel(commentService, autoCompleteAdvisor); await target.InitializeAsPlaceholderAsync(session, thread, false, false); @@ -218,16 +220,18 @@ static async Task CreateTarget( ICommentThreadViewModel thread = null, ActorModel currentUser = null, PullRequestReviewModel review = null, - PullRequestReviewCommentModel comment = null) + PullRequestReviewCommentModel comment = null, + IAutoCompleteAdvisor autoCompleteAdvisor = null) { session = session ?? CreateSession(); commentService = commentService ?? Substitute.For(); + autoCompleteAdvisor = autoCompleteAdvisor ?? Substitute.For(); thread = thread ?? CreateThread(); currentUser = currentUser ?? new ActorModel { Login = "CurrentUser" }; comment = comment ?? new PullRequestReviewCommentModel(); review = review ?? CreateReview(PullRequestReviewState.Approved, comment); - var result = new PullRequestReviewCommentViewModel(commentService); + var result = new PullRequestReviewCommentViewModel(commentService, autoCompleteAdvisor); await result.InitializeAsync(session, thread, review, comment, CommentEditState.None); return result; } diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs index bbdf4668c2..5e9ac069e6 100644 --- a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs @@ -272,8 +272,9 @@ static IViewViewModelFactory CreateFactory() var draftStore = Substitute.For(); var commentService = Substitute.For(); var result = Substitute.For(); + var autoCompleteAdvisor = Substitute.For(); result.CreateViewModel().Returns(_ => - new PullRequestReviewCommentViewModel(commentService)); + new PullRequestReviewCommentViewModel(commentService, autoCompleteAdvisor)); result.CreateViewModel().Returns(_ => new PullRequestReviewCommentThreadViewModel(draftStore, result)); return result; From 72a82e5f91ec58e5045c67c2397bd81e1b3e2ebf Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 8 Apr 2019 20:38:06 -0400 Subject: [PATCH 30/33] Fixing tests --- .../ViewModels/CommentThreadViewModelTests.cs | 2 +- .../ViewModels/CommentViewModelTests.cs | 7 +++++-- .../GitHubPane/PullRequestReviewAuthoringViewModelTests.cs | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/GitHub.App.UnitTests/ViewModels/CommentThreadViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/CommentThreadViewModelTests.cs index 41744b5780..562522d252 100644 --- a/test/GitHub.App.UnitTests/ViewModels/CommentThreadViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/CommentThreadViewModelTests.cs @@ -113,7 +113,7 @@ public async Task AddPlaceholder(bool isEditing) class TestComment : CommentViewModel { public TestComment() - : base(Substitute.For()) + : base(Substitute.For(), Substitute.For()) { } diff --git a/test/GitHub.App.UnitTests/ViewModels/CommentViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/CommentViewModelTests.cs index 6fa4e42632..786719d517 100644 --- a/test/GitHub.App.UnitTests/ViewModels/CommentViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/CommentViewModelTests.cs @@ -65,11 +65,14 @@ await target.InitializeAsync( } CommentViewModel CreateTarget( - ICommentService commentService = null) + ICommentService commentService = null, + IAutoCompleteAdvisor autoCompleteAdvisor = null + ) { commentService = commentService ?? Substitute.For(); + autoCompleteAdvisor = autoCompleteAdvisor ?? Substitute.For(); - return new CommentViewModel(commentService); + return new CommentViewModel(commentService, autoCompleteAdvisor); } } } diff --git a/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs index 42c876cb35..f2d4d2925a 100644 --- a/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs @@ -487,12 +487,15 @@ static PullRequestReviewAuthoringViewModel CreateTarget( IPullRequestSessionManager sessionManager = null, IMessageDraftStore draftStore = null, IPullRequestFilesViewModel files = null, - IScheduler timerScheduler = null) + IScheduler timerScheduler = null, + IAutoCompleteAdvisor autoCompleteAdvisor = null + ) { editorService = editorService ?? Substitute.For(); sessionManager = sessionManager ?? CreateSessionManager(); draftStore = draftStore ?? Substitute.For(); files = files ?? Substitute.For(); + autoCompleteAdvisor = autoCompleteAdvisor ?? Substitute.For(); timerScheduler = timerScheduler ?? DefaultScheduler.Instance; return new PullRequestReviewAuthoringViewModel( @@ -501,6 +504,7 @@ static PullRequestReviewAuthoringViewModel CreateTarget( sessionManager, draftStore, files, + autoCompleteAdvisor, timerScheduler); } From 8a6737c9e207851fdcc77ae1635312adf2bb49e3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 9 Apr 2019 14:06:23 -0400 Subject: [PATCH 31/33] Using the GraphQL Search for Issues and Pull Requests --- .../Services/IssuesAutoCompleteSource.cs | 67 +++++++++++++------ submodules/octokit.graphql.net | 2 +- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/GitHub.App/Services/IssuesAutoCompleteSource.cs b/src/GitHub.App/Services/IssuesAutoCompleteSource.cs index bc05f4469e..4889879841 100644 --- a/src/GitHub.App/Services/IssuesAutoCompleteSource.cs +++ b/src/GitHub.App/Services/IssuesAutoCompleteSource.cs @@ -8,6 +8,7 @@ using GitHub.Models; using GitHub.Primitives; using Octokit.GraphQL; +using Octokit.GraphQL.Model; using static Octokit.GraphQL.Variable; namespace GitHub.Services @@ -18,7 +19,7 @@ public class IssuesAutoCompleteSource : IAutoCompleteSource { readonly ITeamExplorerContext teamExplorerContext; readonly IGraphQLClientFactory graphqlFactory; - ICompiledQuery> query; + ICompiledQuery> query; [ImportingConstructor] public IssuesAutoCompleteSource(ITeamExplorerContext teamExplorerContext, IGraphQLClientFactory graphqlFactory) @@ -38,32 +39,58 @@ public IObservable GetSuggestions() var owner = localRepositoryModel.Owner; var name = localRepositoryModel.Name; + string filter; + string after; + if (query == null) { - query = new Query().Repository(owner: Var(nameof(owner)), name: Var(nameof(name))) - .Select(repository => - repository.Issues(null, null, null, null, null, null, null) - .AllPages() - .Select(issue => new SuggestionItem("#" + issue.Number, issue.Title) - { - LastModifiedDate = issue.LastEditedAt - }) - .ToList()) - .Compile(); + query = new Query().Search(query: Var(nameof(filter)), SearchType.Issue, 100, after: Var(nameof(after))) + .Select(item => new Page + { + Items = item.Nodes.Select(searchResultItem => + searchResultItem.Switch(selector => selector + .Issue(i => new SuggestionItem("#" + i.Number, i.Title) { LastModifiedDate = i.LastEditedAt }) + .PullRequest(p => new SuggestionItem("#" + p.Number, p.Title) { LastModifiedDate = p.LastEditedAt })) + ).ToList(), + EndCursor = item.PageInfo.EndCursor, + HasNextPage = item.PageInfo.HasNextPage, + TotalCount = item.IssueCount + }) + .Compile(); } - var variables = new Dictionary - { - {nameof(owner), owner }, - {nameof(name), name }, - }; + filter = $"repo:{owner}/{name} is:open"; return Observable.FromAsync(async () => + { + var results = new List(); + + var variables = new Dictionary + { + {nameof(filter), filter }, + }; + + var connection = await graphqlFactory.CreateConnection(hostAddress); + var searchResults = await connection.Run(query, variables); + + results.AddRange(searchResults.Items); + + while (searchResults.HasNextPage) { - var connection = await graphqlFactory.CreateConnection(hostAddress); - var suggestions = await connection.Run(query, variables); - return suggestions.Select(suggestion => new IssueAutoCompleteSuggestion(suggestion, Prefix)); - }).SelectMany(enumerable => enumerable); + variables[nameof(after)] = searchResults.EndCursor; + searchResults = await connection.Run(query, variables); + + results.AddRange(searchResults.Items); + } + + return results.Select(item => new IssueAutoCompleteSuggestion(item, Prefix)); + + }).SelectMany(observable => observable); + } + + class SearchResult + { + public SuggestionItem SuggestionItem { get; set; } } public string Prefix diff --git a/submodules/octokit.graphql.net b/submodules/octokit.graphql.net index ff20b6e9de..f12917ae36 160000 --- a/submodules/octokit.graphql.net +++ b/submodules/octokit.graphql.net @@ -1 +1 @@ -Subproject commit ff20b6e9de3d016112de8787ec8ade080214db2b +Subproject commit f12917ae365cc98b4f4a48b1aaefc6dc8c2593aa From 3995d0d841c6553c77f5d45531790b61d71937d6 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 10 Apr 2019 07:13:19 -0400 Subject: [PATCH 32/33] Reverting submodule update --- submodules/octokit.graphql.net | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/octokit.graphql.net b/submodules/octokit.graphql.net index f12917ae36..ff20b6e9de 160000 --- a/submodules/octokit.graphql.net +++ b/submodules/octokit.graphql.net @@ -1 +1 @@ -Subproject commit f12917ae365cc98b4f4a48b1aaefc6dc8c2593aa +Subproject commit ff20b6e9de3d016112de8787ec8ade080214db2b From f1b538a4be4c146e3f41294cfe1da0bfd730582f Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 10 Apr 2019 12:23:40 -0400 Subject: [PATCH 33/33] Allowing all issues closed or open --- src/GitHub.App/Services/IssuesAutoCompleteSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.App/Services/IssuesAutoCompleteSource.cs b/src/GitHub.App/Services/IssuesAutoCompleteSource.cs index 4889879841..d2c64e671c 100644 --- a/src/GitHub.App/Services/IssuesAutoCompleteSource.cs +++ b/src/GitHub.App/Services/IssuesAutoCompleteSource.cs @@ -59,7 +59,7 @@ public IObservable GetSuggestions() .Compile(); } - filter = $"repo:{owner}/{name} is:open"; + filter = $"repo:{owner}/{name}"; return Observable.FromAsync(async () => {