Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 446a080

Browse files
Adding missing advisors
1 parent 83cdda5 commit 446a080

File tree

5 files changed

+338
-0
lines changed

5 files changed

+338
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.ComponentModel.Composition;
5+
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Globalization;
8+
using System.Linq;
9+
using System.Reactive.Linq;
10+
using GitHub.UI;
11+
using NLog;
12+
13+
namespace GitHub.Helpers
14+
{
15+
[Export(typeof(IAutoCompleteAdvisor))]
16+
[PartCreationPolicy(CreationPolicy.Shared)]
17+
public class AutoCompleteAdvisor : IAutoCompleteAdvisor
18+
{
19+
const int SuggestionCount = 5; // The number of suggestions we'll provide. github.com does 5.
20+
21+
static readonly Logger log = LogManager.GetCurrentClassLogger();
22+
readonly Lazy<Dictionary<string, IAutoCompleteSource>> prefixSourceMap;
23+
24+
[ImportingConstructor]
25+
public AutoCompleteAdvisor([ImportMany]IEnumerable<IAutoCompleteSource> autocompleteSources)
26+
{
27+
prefixSourceMap = new Lazy<Dictionary<string, IAutoCompleteSource>>(
28+
() => autocompleteSources.ToDictionary(s => s.Prefix, s => s));
29+
}
30+
31+
public IObservable<AutoCompleteResult> GetAutoCompletionSuggestions(string text, int caretPosition)
32+
{
33+
Ensure.ArgumentNotNull("text", text);
34+
35+
if (caretPosition < 0 || caretPosition > text.Length)
36+
{
37+
string error = String.Format(CultureInfo.InvariantCulture,
38+
"The CaretPosition '{0}', is not in the range of '0' and the text length '{1}' for the text '{2}'",
39+
caretPosition,
40+
text.Length,
41+
text);
42+
43+
// We need to be alerted when this happens because it should never happen.
44+
// But it apparently did happen in production.
45+
Debug.Fail(error);
46+
log.Error(error);
47+
return Observable.Empty<AutoCompleteResult>();
48+
}
49+
var tokenAndSource = PrefixSourceMap
50+
.Select(kvp => new {Source = kvp.Value, Token = ParseAutoCompletionToken(text, caretPosition, kvp.Key)})
51+
.FirstOrDefault(s => s.Token != null);
52+
53+
if (tokenAndSource == null)
54+
{
55+
return Observable.Return(AutoCompleteResult.Empty);
56+
}
57+
58+
return tokenAndSource.Source.GetSuggestions()
59+
.Select(suggestion => new
60+
{
61+
suggestion,
62+
rank = suggestion.GetSortRank(tokenAndSource.Token.SearchSearchPrefix)
63+
})
64+
.Where(suggestion => suggestion.rank > -1)
65+
.ToList()
66+
.Select(suggestions => suggestions
67+
.OrderByDescending(s => s.rank)
68+
.ThenBy(s => s.suggestion.Name)
69+
.Take(SuggestionCount)
70+
.Select(s => s.suggestion)
71+
.ToList())
72+
.Select(suggestions => new AutoCompleteResult(tokenAndSource.Token.Offset,
73+
new ReadOnlyCollection<AutoCompleteSuggestion>(suggestions)))
74+
.Catch<AutoCompleteResult, Exception>(e =>
75+
{
76+
log.Info(e);
77+
return Observable.Return(AutoCompleteResult.Empty);
78+
});
79+
}
80+
81+
[SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "caretPosition-1"
82+
, Justification = "We ensure the argument is greater than -1 so it can't overflow")]
83+
public static AutoCompletionToken ParseAutoCompletionToken(string text, int caretPosition, string triggerPrefix)
84+
{
85+
Ensure.ArgumentNotNull("text", text);
86+
Ensure.ArgumentInRange(caretPosition, 0, text.Length, "caretPosition");
87+
if (caretPosition == 0 || text.Length == 0) return null;
88+
89+
// :th : 1
90+
//:th : 0
91+
//Hi :th : 3
92+
int beginningOfWord = text.LastIndexOfAny(new[] { ' ', '\n' }, caretPosition - 1) + 1;
93+
string word = text.Substring(beginningOfWord, caretPosition - beginningOfWord);
94+
if (!word.StartsWith(triggerPrefix, StringComparison.Ordinal)) return null;
95+
96+
return new AutoCompletionToken(word.Substring(1), beginningOfWord);
97+
}
98+
99+
Dictionary<string, IAutoCompleteSource> PrefixSourceMap { get { return prefixSourceMap.Value; } }
100+
}
101+
102+
public class AutoCompletionToken
103+
{
104+
public AutoCompletionToken(string searchPrefix, int offset)
105+
{
106+
Ensure.ArgumentNotNull(searchPrefix, "searchPrefix");
107+
Ensure.ArgumentNonNegative(offset, "offset");
108+
109+
SearchSearchPrefix = searchPrefix;
110+
Offset = offset;
111+
}
112+
113+
/// <summary>
114+
/// Used to filter the list of auto complete suggestions to what the user has typed in.
115+
/// </summary>
116+
public string SearchSearchPrefix { get; private set; }
117+
public int Offset { get; private set; }
118+
}
119+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.ComponentModel.Composition;
3+
using System.Linq;
4+
using System.Reactive.Linq;
5+
using System.Windows.Media.Imaging;
6+
using GitHub.UI;
7+
8+
namespace GitHub.Helpers
9+
{
10+
[Export(typeof(IAutoCompleteSource))]
11+
[PartCreationPolicy(CreationPolicy.Shared)]
12+
public class EmojiAutoCompleteSource : IAutoCompleteSource
13+
{
14+
readonly IEmojiCache emojiCache;
15+
16+
[ImportingConstructor]
17+
public EmojiAutoCompleteSource(IEmojiCache emojiCache)
18+
{
19+
Ensure.ArgumentNotNull(emojiCache, "emojiCache");
20+
21+
this.emojiCache = emojiCache;
22+
}
23+
24+
public IObservable<AutoCompleteSuggestion> GetSuggestions()
25+
{
26+
Func<Uri, IObservable<BitmapSource>> resolveImage = uri =>
27+
Observable.Defer(() =>
28+
{
29+
var resourcePath = "pack://application:,,,/GitHub;component/" + uri;
30+
return Observable.Return(App.CreateBitmapImage(resourcePath));
31+
});
32+
33+
return emojiCache.GetEmojis()
34+
.Where(emoji => !String.IsNullOrEmpty(emoji.Name)) // Just being extra cautious.
35+
.Select(emoji => new AutoCompleteSuggestion(emoji.Name, resolveImage(emoji.IconKey), ":", ":"))
36+
.ToObservable();
37+
}
38+
39+
public string Prefix { get { return ":"; } }
40+
}
41+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using GitHub.UI;
3+
4+
namespace GitHub.Helpers
5+
{
6+
public interface IAutoCompleteSource
7+
{
8+
IObservable<AutoCompleteSuggestion> GetSuggestions();
9+
10+
// The prefix used to trigger auto completion.
11+
string Prefix { get; }
12+
}
13+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.Composition;
4+
using System.Linq;
5+
using System.Reactive.Linq;
6+
using GitHub.Cache;
7+
using GitHub.Models;
8+
using GitHub.UI;
9+
using GitHub.ViewModels;
10+
11+
namespace GitHub.Helpers
12+
{
13+
[Export(typeof(IAutoCompleteSource))]
14+
[PartCreationPolicy(CreationPolicy.Shared)]
15+
public class IssuesAutoCompleteSource : IAutoCompleteSource
16+
{
17+
readonly Lazy<IIssuesCache> issuesCache;
18+
readonly Lazy<ISourceListViewModel> currentRepositoryState;
19+
20+
[ImportingConstructor]
21+
public IssuesAutoCompleteSource(
22+
Lazy<IIssuesCache> issuesCache,
23+
Lazy<ISourceListViewModel> currentRepositoryState)
24+
{
25+
Ensure.ArgumentNotNull(issuesCache, "issuesCache");
26+
Ensure.ArgumentNotNull(currentRepositoryState, "currentRepositoryState");
27+
28+
this.issuesCache = issuesCache;
29+
this.currentRepositoryState = currentRepositoryState;
30+
}
31+
32+
public IObservable<AutoCompleteSuggestion> GetSuggestions()
33+
{
34+
if (CurrentRepository.RepositoryHost == null)
35+
{
36+
return Observable.Empty<AutoCompleteSuggestion>();
37+
}
38+
39+
return IssuesCache.RetrieveSuggestions(CurrentRepository)
40+
.Catch<IReadOnlyList<SuggestionItem>, Exception>(_ => Observable.Empty<IReadOnlyList<SuggestionItem>>())
41+
.SelectMany(x => x.ToObservable())
42+
.Where(suggestion => !String.IsNullOrEmpty(suggestion.Name)) // Just being extra cautious
43+
.Select(suggestion => new IssueAutoCompleteSuggestion(suggestion, Prefix));
44+
}
45+
46+
public string Prefix
47+
{
48+
get { return "#"; }
49+
}
50+
51+
IIssuesCache IssuesCache { get { return issuesCache.Value; } }
52+
53+
IRepositoryModel CurrentRepository { get { return currentRepositoryState.Value.SelectedRepository; } }
54+
55+
class IssueAutoCompleteSuggestion : AutoCompleteSuggestion
56+
{
57+
// Just needs to be some value before GitHub stored its first issue.
58+
static readonly DateTimeOffset lowerBound = new DateTimeOffset(2000, 1, 1, 12, 0, 0, TimeSpan.FromSeconds(0));
59+
60+
readonly SuggestionItem suggestion;
61+
public IssueAutoCompleteSuggestion(SuggestionItem suggestion, string prefix)
62+
: base(suggestion.Name, suggestion.Description, prefix)
63+
{
64+
this.suggestion = suggestion;
65+
}
66+
67+
public override int GetSortRank(string text)
68+
{
69+
// We need to override the sort rank behavior because when we display issues, we include the prefix
70+
// unlike mentions. So we need to account for that in how we do filtering.
71+
if (text.Length == 0)
72+
{
73+
return (int) ((suggestion.LastModifiedDate ?? lowerBound) - lowerBound).TotalSeconds;
74+
}
75+
// Name is always "#" followed by issue number.
76+
return Name.StartsWith("#" + text, StringComparison.OrdinalIgnoreCase)
77+
? 1
78+
: DescriptionWords.Any(word => word.StartsWith(text, StringComparison.OrdinalIgnoreCase))
79+
? 0
80+
: -1;
81+
}
82+
83+
// This is what gets "completed" when you tab.
84+
public override string ToString()
85+
{
86+
return Name;
87+
}
88+
}
89+
}
90+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.Composition;
4+
using System.Reactive.Linq;
5+
using System.Windows.Media.Imaging;
6+
using GitHub.Cache;
7+
using GitHub.Models;
8+
using GitHub.UI;
9+
using GitHub.ViewModels;
10+
11+
namespace GitHub.Helpers
12+
{
13+
/// <summary>
14+
/// Supplies @mentions auto complete suggestions.
15+
/// </summary>
16+
[Export(typeof(IAutoCompleteSource))]
17+
[PartCreationPolicy(CreationPolicy.Shared)]
18+
public class MentionsAutoCompleteSource : IAutoCompleteSource
19+
{
20+
readonly Lazy<IMentionsCache> mentionsCache;
21+
readonly Lazy<ISourceListViewModel> currentRepositoryState;
22+
readonly Lazy<IImageCache> imageCache;
23+
readonly IHostAvatarProvider hostAvatarProvider;
24+
25+
[ImportingConstructor]
26+
public MentionsAutoCompleteSource(
27+
Lazy<IMentionsCache> mentionsCache,
28+
Lazy<ISourceListViewModel> currentRepositoryState,
29+
Lazy<IImageCache> imageCache,
30+
IHostAvatarProvider hostAvatarProvider)
31+
{
32+
Ensure.ArgumentNotNull(mentionsCache, "mentionsCache");
33+
Ensure.ArgumentNotNull(currentRepositoryState, "currentRepositoryState");
34+
Ensure.ArgumentNotNull(imageCache, "imageCache");
35+
Ensure.ArgumentNotNull(hostAvatarProvider, "hostAvatarProvider");
36+
37+
this.mentionsCache = mentionsCache;
38+
this.currentRepositoryState = currentRepositoryState;
39+
this.imageCache = imageCache;
40+
this.hostAvatarProvider = hostAvatarProvider;
41+
}
42+
43+
public IObservable<AutoCompleteSuggestion> GetSuggestions()
44+
{
45+
if (CurrentRepository.RepositoryHost == null)
46+
{
47+
return Observable.Empty<AutoCompleteSuggestion>();
48+
}
49+
50+
var avatarProviderKey = CurrentRepository.RepositoryHost.Address.WebUri.ToString();
51+
var avatarProvider = hostAvatarProvider.Get(avatarProviderKey);
52+
53+
Func<Uri, IObservable<BitmapSource>> resolveImage = uri =>
54+
Observable.Defer(() => ImageCache
55+
.GetImage(uri)
56+
.Catch<BitmapSource, Exception>(_ => Observable.Return(avatarProvider.DefaultUserBitmapImage))
57+
.StartWith(avatarProvider.DefaultUserBitmapImage));
58+
59+
return MentionsCache.RetrieveSuggestions(CurrentRepository)
60+
.Catch<IReadOnlyList<SuggestionItem>, Exception>(_ => Observable.Empty<IReadOnlyList<SuggestionItem>>())
61+
.SelectMany(x => x.ToObservable())
62+
.Where(suggestion => !String.IsNullOrEmpty(suggestion.Name)) // Just being extra cautious
63+
.Select(suggestion =>
64+
new AutoCompleteSuggestion(suggestion.Name, suggestion.Description, resolveImage(suggestion.IconKey), Prefix));
65+
}
66+
67+
public string Prefix { get { return "@"; } }
68+
69+
IImageCache ImageCache { get { return imageCache.Value; } }
70+
71+
IMentionsCache MentionsCache { get { return mentionsCache.Value; } }
72+
73+
IRepositoryModel CurrentRepository { get { return currentRepositoryState.Value.SelectedRepository; } }
74+
}
75+
}

0 commit comments

Comments
 (0)