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

Autocomplete for users and issues with '@' and '#' #2156

Merged
merged 95 commits into from Apr 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
13ecbb2
Pulling the AutoCompleteBox from the original GitHub Desktop for Windows
StanleyGoldman Jan 2, 2019
6d1dab1
Importing tests (set to not compile)
StanleyGoldman Jan 3, 2019
83cdda5
Adding AutoCompleteSuggestionView
StanleyGoldman Jan 3, 2019
446a080
Adding missing advisors
StanleyGoldman Jan 3, 2019
c2c4a1b
Adding more tests (not compiled)
StanleyGoldman Jan 3, 2019
9f83539
Other items that need to be imported
StanleyGoldman Jan 3, 2019
c3fa22d
Changing services to use GraphQL library
StanleyGoldman Jan 4, 2019
9e4d7f9
Starting to implement autocomplete functionality
StanleyGoldman Jan 4, 2019
6e83023
Moving some files
StanleyGoldman Jan 4, 2019
edca5b7
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Jan 4, 2019
fcd503d
Moving files
StanleyGoldman Jan 4, 2019
f5809f2
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Jan 4, 2019
653cc79
Undoing changes to PullRequestCreationViewModel
StanleyGoldman Jan 4, 2019
2911b35
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Jan 4, 2019
ac4aab9
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Jan 4, 2019
8906c54
Revert "Undoing changes to PullRequestCreationViewModel"
StanleyGoldman Jan 4, 2019
bfa18c6
Undoing some changes
StanleyGoldman Jan 4, 2019
2c94748
Trying to display the control correctly
StanleyGoldman Jan 5, 2019
027b322
Exposing Advisor as a dependency property
StanleyGoldman Jan 5, 2019
ba9773f
Merge branch 'master' into autocompletebox
StanleyGoldman Jan 24, 2019
1d133f9
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Jan 24, 2019
3dba1d9
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Jan 24, 2019
4779ac1
Add missing control styles
StanleyGoldman Jan 24, 2019
7aadd19
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Jan 24, 2019
3ff1300
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Jan 24, 2019
9d2ee84
Including missing themes
StanleyGoldman Jan 24, 2019
161b85e
Commenting out event subscriptions
StanleyGoldman Jan 24, 2019
4abdd2e
Merge branch 'master' into autocompletebox
StanleyGoldman Feb 26, 2019
995321b
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Feb 26, 2019
0649605
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Feb 26, 2019
01a5a5b
Including missing themes
StanleyGoldman Jan 24, 2019
8e5cb66
Commenting out event subscriptions
StanleyGoldman Jan 24, 2019
e1d75b7
Trying to simplify the observable
StanleyGoldman Feb 26, 2019
9fd2cac
Use FromEventPattern instead of FromEvent
StanleyGoldman Feb 26, 2019
a9cf0c2
Tweaking ImportMany command
StanleyGoldman Feb 26, 2019
62b71c2
Fixing view
StanleyGoldman Feb 26, 2019
92fb304
Importing ITeamExplorerContext
StanleyGoldman Feb 26, 2019
b355b6c
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Feb 26, 2019
0dbabcd
Removing extra control
StanleyGoldman Feb 26, 2019
c201404
Merge branch 'master' into autocompletebox
StanleyGoldman Mar 11, 2019
db11e61
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Mar 11, 2019
4a2657e
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 11, 2019
b746b8a
Merge branch 'feature/graphql-caching' into autocompletebox-fix
StanleyGoldman Mar 11, 2019
8b0dde8
Cleaning up graphql query
StanleyGoldman Mar 11, 2019
dd5ee9c
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 11, 2019
2c11ff3
Merge branch 'master' into autocompletebox
StanleyGoldman Mar 15, 2019
6817bfd
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Mar 15, 2019
c1855d9
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 15, 2019
2f4482a
Restoring AutoCompleteBoxTests
StanleyGoldman Mar 15, 2019
a1fdeb2
Restoring AutoCompleteSuggestionTests
StanleyGoldman Mar 15, 2019
1509020
Enabling more tests
StanleyGoldman Mar 15, 2019
943b3ab
Deleting tests
StanleyGoldman Mar 15, 2019
bf381a8
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 15, 2019
de8da0d
Restoring more functionality
StanleyGoldman Mar 15, 2019
df979af
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 15, 2019
d9de8f6
More tweaks and fixes
StanleyGoldman Mar 15, 2019
ea8f2db
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 15, 2019
df16579
Merge branch 'fixes/dialog-style-guide' into autocompletebox-implemen…
jcansdale Mar 25, 2019
3a84e78
Use VS theme colors for AutoCompleteBox
jcansdale Mar 26, 2019
bd77de9
Fixing logic
StanleyGoldman Mar 27, 2019
dff9d67
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 27, 2019
0eae0cf
Merge branch 'master' into autocompletebox
StanleyGoldman Mar 27, 2019
939e31a
Merge branch 'feature/graphql-caching' into autocompletebox-fix
StanleyGoldman Mar 27, 2019
504fdc1
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Mar 27, 2019
b228f02
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 27, 2019
ff19239
Merge remote-tracking branch 'origin/autocompletebox-implementation' …
jcansdale Mar 27, 2019
4e9a6dd
Merge branch 'feature/graphql-caching' into autocompletebox-fix
StanleyGoldman Mar 28, 2019
c40b757
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Mar 28, 2019
d0a7965
Merge branch 'autocompletebox-implementation' into jcansdale/autocomp…
StanleyGoldman Mar 28, 2019
f6f233a
Merge branch 'master' into autocompletebox
StanleyGoldman Apr 5, 2019
4c97d48
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Apr 5, 2019
3d539c8
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Apr 5, 2019
28fc2f1
Merge branch 'autocompletebox-implementation' into jcansdale/autocomp…
StanleyGoldman Apr 5, 2019
84c4dd1
Merge pull request #2292 from github/jcansdale/autocompletebox-theme
StanleyGoldman Apr 5, 2019
2244660
Merge branch 'master' into autocompletebox
StanleyGoldman Apr 8, 2019
280ec5f
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Apr 8, 2019
0bd1903
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Apr 8, 2019
2027b5c
Undoing change
StanleyGoldman Apr 8, 2019
7ab429a
Some code cleanup
StanleyGoldman Apr 8, 2019
5a6e48b
Removing some changes
StanleyGoldman Apr 8, 2019
70e33d4
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Apr 8, 2019
40a452a
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Apr 8, 2019
2011888
Fixing xml comments all around
StanleyGoldman Jan 5, 2019
aa65fcc
Merge branch 'autocompletebox-fix' into autocompletebox-implementation
StanleyGoldman Apr 8, 2019
be1ab80
Merge pull request #2159 from github/autocompletebox-implementation
StanleyGoldman Apr 8, 2019
25fbc2f
Adding autocomplete functionality to inline comment view, pull reques…
StanleyGoldman Apr 8, 2019
48bf070
Merge branch 'master' into autocompletebox
StanleyGoldman Apr 8, 2019
122f767
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Apr 8, 2019
72a82e5
Fixing tests
StanleyGoldman Apr 9, 2019
8a6737c
Using the GraphQL Search for Issues and Pull Requests
StanleyGoldman Apr 9, 2019
3995d0d
Reverting submodule update
StanleyGoldman Apr 10, 2019
f1b538a
Allowing all issues closed or open
StanleyGoldman Apr 10, 2019
f8cfdae
Merge branch 'master' into autocompletebox
StanleyGoldman Apr 10, 2019
76ebd75
Merge branch 'autocompletebox' into autocompletebox-fix
StanleyGoldman Apr 10, 2019
0b5badb
Merge pull request #2158 from github/autocompletebox-fix
StanleyGoldman Apr 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/GitHub.App/Models/SuggestionItem.cs
@@ -0,0 +1,51 @@
using System;
using GitHub.Extensions;
using GitHub.Helpers;

namespace GitHub.Models
{
/// <summary>
/// Represents a single auto completion suggestion (mentions, emojis, issues) in a generic format that can be
/// easily cached.
/// </summary>
public class SuggestionItem
{
public SuggestionItem(string name, string description)
{
Guard.ArgumentNotEmptyString(name, "name");
Guard.ArgumentNotEmptyString(description, "description");

Name = name;
Description = description;
}

public SuggestionItem(string name, string description, string imageUrl)
{
Guard.ArgumentNotEmptyString(name, "name");

Name = name;
Description = description;
ImageUrl = imageUrl;
}

/// <summary>
/// The name to display for this entry
/// </summary>
public string Name { get; set; }

/// <summary>
/// Additional details about the entry
/// </summary>
public string Description { get; set; }

/// <summary>
/// An image url for this entry
/// </summary>
public string ImageUrl { get; set; }

/// <summary>
/// The date this suggestion was last modified according to the API.
/// </summary>
public DateTimeOffset? LastModifiedDate { get; set; }
}
}
2 changes: 2 additions & 0 deletions src/GitHub.App/SampleData/CommentViewModelDesigner.cs
Expand Up @@ -3,6 +3,7 @@
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using GitHub.ViewModels;
using ReactiveUI;

Expand Down Expand Up @@ -37,6 +38,7 @@ public CommentViewModelDesigner()
public ReactiveCommand<Unit, Unit> CommitEdit { get; }
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; } = ReactiveCommand.Create(() => { });
public ReactiveCommand<Unit, Unit> Delete { get; }
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }

public Task InitializeAsync(ICommentThreadViewModel thread, ActorModel currentUser, CommentModel comment, CommentEditState state)
{
Expand Down
Expand Up @@ -4,6 +4,7 @@
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using GitHub.Validation;
using GitHub.ViewModels.GitHubPane;
using ReactiveUI;
Expand Down Expand Up @@ -53,6 +54,7 @@ public PullRequestCreationViewModelDesigner()
public string PRTitle { get; set; }

public ReactivePropertyValidator TitleValidator { get; }
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }

public ReactivePropertyValidator BranchValidator { get; }

Expand Down
Expand Up @@ -3,6 +3,7 @@
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using GitHub.ViewModels.GitHubPane;
using ReactiveUI;

Expand Down Expand Up @@ -53,6 +54,7 @@ public PullRequestReviewAuthoringViewModelDesigner()
public ReactiveCommand<Unit, Unit> Comment { get; }
public ReactiveCommand<Unit, Unit> RequestChanges { get; }
public ReactiveCommand<Unit, Unit> Cancel { get; }
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }

public Task InitializeAsync(
LocalRepositoryModel localRepository,
Expand Down
121 changes: 121 additions & 0 deletions src/GitHub.App/Services/AutoCompleteAdvisor.cs
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using GitHub.Extensions;
using GitHub.Logging;
using GitHub.Models;
using Serilog;

namespace GitHub.Services
{
[Export(typeof(IAutoCompleteAdvisor))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class AutoCompleteAdvisor : IAutoCompleteAdvisor
{
const int SuggestionCount = 5; // The number of suggestions we'll provide. github.com does 5.

static readonly ILogger log = LogManager.ForContext<AutoCompleteAdvisor>();
readonly Lazy<Dictionary<string, IAutoCompleteSource>> prefixSourceMap;

[ImportingConstructor]
public AutoCompleteAdvisor([ImportMany(typeof(IAutoCompleteSource))]IEnumerable<IAutoCompleteSource> autocompleteSources)
{
prefixSourceMap = new Lazy<Dictionary<string, IAutoCompleteSource>>(
() => autocompleteSources.ToDictionary(s => s.Prefix, s => s));
}

public IObservable<AutoCompleteResult> GetAutoCompletionSuggestions(string text, int caretPosition)
{
Guard.ArgumentNotNull("text", text);

if (caretPosition < 0 || caretPosition > text.Length)
{
string error = String.Format(CultureInfo.InvariantCulture,
"The CaretPosition '{0}', is not in the range of '0' and the text length '{1}' for the text '{2}'",
caretPosition,
text.Length,
text);

// We need to be alerted when this happens because it should never happen.
// But it apparently did happen in production.
Debug.Fail(error);
log.Error(error);
return Observable.Empty<AutoCompleteResult>();
}
var tokenAndSource = PrefixSourceMap
.Select(kvp => new {Source = kvp.Value, Token = ParseAutoCompletionToken(text, caretPosition, kvp.Key)})
.FirstOrDefault(s => s.Token != null);

if (tokenAndSource == null)
{
return Observable.Return(AutoCompleteResult.Empty);
}

return tokenAndSource.Source.GetSuggestions()
.Select(suggestion => new
{
suggestion,
rank = suggestion.GetSortRank(tokenAndSource.Token.SearchSearchPrefix)
})
.Where(suggestion => suggestion.rank > -1)
.ToList()
.Select(suggestions => suggestions
.OrderByDescending(s => s.rank)
.ThenBy(s => s.suggestion.Name)
.Take(SuggestionCount)
.Select(s => s.suggestion)
.ToList())
.Select(suggestions => new AutoCompleteResult(tokenAndSource.Token.Offset,
new ReadOnlyCollection<AutoCompleteSuggestion>(suggestions)))
.Catch<AutoCompleteResult, Exception>(e =>
{
log.Error(e, "Error Getting AutoCompleteResult");
return Observable.Return(AutoCompleteResult.Empty);
});
}

[SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "caretPosition-1"
, Justification = "We ensure the argument is greater than -1 so it can't overflow")]
public static AutoCompletionToken ParseAutoCompletionToken(string text, int caretPosition, string triggerPrefix)
{
Guard.ArgumentNotNull("text", text);
Guard.ArgumentInRange(caretPosition, 0, text.Length, "caretPosition");
if (caretPosition == 0 || text.Length == 0) return null;

// :th : 1
//:th : 0
//Hi :th : 3
int beginningOfWord = text.LastIndexOfAny(new[] { ' ', '\n' }, caretPosition - 1) + 1;
string word = text.Substring(beginningOfWord, caretPosition - beginningOfWord);
if (!word.StartsWith(triggerPrefix, StringComparison.Ordinal)) return null;

return new AutoCompletionToken(word.Substring(1), beginningOfWord);
}

Dictionary<string, IAutoCompleteSource> PrefixSourceMap { get { return prefixSourceMap.Value; } }
}

public class AutoCompletionToken
{
public AutoCompletionToken(string searchPrefix, int offset)
{
Guard.ArgumentNotNull(searchPrefix, "searchPrefix");
Guard.ArgumentNonNegative(offset, "offset");

SearchSearchPrefix = searchPrefix;
Offset = offset;
}

/// <summary>
/// Used to filter the list of auto complete suggestions to what the user has typed in.
/// </summary>
public string SearchSearchPrefix { get; private set; }
public int Offset { get; private set; }
}
}
13 changes: 13 additions & 0 deletions src/GitHub.App/Services/IAutoCompleteSource.cs
@@ -0,0 +1,13 @@
using System;
using GitHub.Models;

namespace GitHub.Services
{
public interface IAutoCompleteSource
{
IObservable<AutoCompleteSuggestion> GetSuggestions();

// The prefix used to trigger auto completion.
string Prefix { get; }
}
}
136 changes: 136 additions & 0 deletions src/GitHub.App/Services/IssuesAutoCompleteSource.cs
@@ -0,0 +1,136 @@
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.Primitives;
using Octokit.GraphQL;
using Octokit.GraphQL.Model;
using static Octokit.GraphQL.Variable;

namespace GitHub.Services
{
[Export(typeof(IAutoCompleteSource))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssuesAutoCompleteSource : IAutoCompleteSource
{
readonly ITeamExplorerContext teamExplorerContext;
readonly IGraphQLClientFactory graphqlFactory;
ICompiledQuery<Page<SuggestionItem>> query;

[ImportingConstructor]
public IssuesAutoCompleteSource(ITeamExplorerContext teamExplorerContext, IGraphQLClientFactory graphqlFactory)
{
Guard.ArgumentNotNull(teamExplorerContext, nameof(teamExplorerContext));
Guard.ArgumentNotNull(graphqlFactory, nameof(graphqlFactory));

this.teamExplorerContext = teamExplorerContext;
this.graphqlFactory = graphqlFactory;
}

public IObservable<AutoCompleteSuggestion> GetSuggestions()
{
var localRepositoryModel = teamExplorerContext.ActiveRepository;

var hostAddress = HostAddress.Create(localRepositoryModel.CloneUrl.Host);
var owner = localRepositoryModel.Owner;
var name = localRepositoryModel.Name;

string filter;
string after;

if (query == null)
{
query = new Query().Search(query: Var(nameof(filter)), SearchType.Issue, 100, after: Var(nameof(after)))
.Select(item => new Page<SuggestionItem>
{
Items = item.Nodes.Select(searchResultItem =>
searchResultItem.Switch<SuggestionItem>(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();
}

filter = $"repo:{owner}/{name}";

return Observable.FromAsync(async () =>
{
var results = new List<SuggestionItem>();

var variables = new Dictionary<string, object>
{
{nameof(filter), filter },
};

var connection = await graphqlFactory.CreateConnection(hostAddress);
var searchResults = await connection.Run(query, variables);

results.AddRange(searchResults.Items);

while (searchResults.HasNextPage)
{
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
{
get { return "#"; }
}

class IssueAutoCompleteSuggestion : AutoCompleteSuggestion
{
// 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));

readonly SuggestionItem suggestion;
public IssueAutoCompleteSuggestion(SuggestionItem suggestion, string prefix)
: base(suggestion.Name, suggestion.Description, prefix)
{
this.suggestion = suggestion;
}

public override int GetSortRank(string text)
{
// We need to override the sort rank behavior because when we display issues, we include the prefix
// unlike mentions. So we need to account for that in how we do filtering.
if (text.Length == 0)
{
return (int) ((suggestion.LastModifiedDate ?? lowerBound) - lowerBound).TotalSeconds;
}
// Name is always "#" followed by issue number.
return Name.StartsWith("#" + text, StringComparison.OrdinalIgnoreCase)
? 1
: DescriptionWords.Any(word => word.StartsWith(text, StringComparison.OrdinalIgnoreCase))
? 0
: -1;
}

// This is what gets "completed" when you tab.
public override string ToString()
{
return Name;
}
}
}
}