Merge remote-tracking branch 'origin/master' into fixes/726-markdig

This commit is contained in:
Steven Kirk 2017-07-07 10:42:01 +02:00
Родитель fe86aec18c bf27bc6ee3
Коммит 2113a98f0d
174 изменённых файлов: 10928 добавлений и 197 удалений

Просмотреть файл

@ -0,0 +1,6 @@
<ProjectConfiguration>
<Settings>
<CopyReferencedAssembliesToWorkspace>True</CopyReferencedAssembliesToWorkspace>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

Просмотреть файл

@ -0,0 +1,7 @@
<ProjectConfiguration>
<Settings>
<InstrumentOutputAssembly>True</InstrumentOutputAssembly>
<PreventSigningOfAssembly>True</PreventSigningOfAssembly>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

Просмотреть файл

@ -4,6 +4,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio", "src\GitHub.VisualStudio\GitHub.VisualStudio.csproj", "{11569514-5AE5-4B5B-92A2-F10B0967DE5F}"
ProjectSection(ProjectDependencies) = postProject
{7F5ED78B-74A3-4406-A299-70CFB5885B8B} = {7F5ED78B-74A3-4406-A299-70CFB5885B8B}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{72036B62-2FA6-4A22-8B33-69F698A18CF1}"
ProjectSection(SolutionItems) = preProject
@ -109,6 +112,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Markdig", "Markdig", "{C5964392-2BEF-41DD-8D36-9E306FD67137}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdig.Wpf", "submodules\markdig-wpf\src\Markdig.Wpf\Markdig.Wpf.csproj", "{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.InlineReviews", "src\GitHub.InlineReviews\GitHub.InlineReviews.csproj", "{7F5ED78B-74A3-4406-A299-70CFB5885B8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.InlineReviews.UnitTests", "test\GitHub.InlineReviews.UnitTests\GitHub.InlineReviews.UnitTests.csproj", "{17EB676B-BB91-48B5-AA59-C67695C647C2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -132,7 +138,6 @@ Global
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|x86.ActiveCfg = Release|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|x86.Build.0 = Release|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|x86.ActiveCfg = Debug|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|x86.Build.0 = Debug|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Publish|Any CPU.ActiveCfg = Release|Any CPU
@ -242,7 +247,6 @@ Global
{E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Release|x86.ActiveCfg = Release|Any CPU
{E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Release|x86.Build.0 = Release|Any CPU
{B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|x86.ActiveCfg = Debug|Any CPU
{B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|x86.Build.0 = Debug|Any CPU
{B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Publish|Any CPU.ActiveCfg = Release|Any CPU
@ -460,6 +464,30 @@ Global
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Release|Any CPU.Build.0 = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Release|x86.ActiveCfg = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Release|x86.Build.0 = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Debug|x86.ActiveCfg = Debug|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Debug|x86.Build.0 = Debug|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Publish|Any CPU.Build.0 = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Publish|x86.ActiveCfg = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Publish|x86.Build.0 = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Release|Any CPU.Build.0 = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Release|x86.ActiveCfg = Release|Any CPU
{7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Release|x86.Build.0 = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Debug|x86.ActiveCfg = Debug|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Debug|x86.Build.0 = Debug|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Publish|Any CPU.Build.0 = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Publish|x86.ActiveCfg = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Publish|x86.Build.0 = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Release|Any CPU.Build.0 = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Release|x86.ActiveCfg = Release|Any CPU
{17EB676B-BB91-48B5-AA59-C67695C647C2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -486,5 +514,6 @@ Global
{110B206F-8554-4B51-BF86-94DAA32F5E26} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
{C5964392-2BEF-41DD-8D36-9E306FD67137} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7} = {C5964392-2BEF-41DD-8D36-9E306FD67137}
{17EB676B-BB91-48B5-AA59-C67695C647C2} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -11,7 +11,7 @@
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.VisualStudio.Imaging" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.0.0.0" newVersion="15.0.0.0" />
<bindingRedirect oldVersion="0.0.0.0-14.0.0.0" newVersion="14.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.TeamFoundation.Controls" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
@ -33,6 +33,18 @@
<assemblyIdentity name="Microsoft.VisualStudio.CoreUtility" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-14.0.0.0" newVersion="14.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Xml.ReaderWriter" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Просмотреть файл

@ -62,6 +62,30 @@ namespace GitHub.Api
return (isUser ? client.Create(repository) : client.Create(login, repository));
}
public IObservable<PullRequestReviewComment> CreatePullRequestReviewComment(
string owner,
string name,
int number,
string body,
string commitId,
string path,
int position)
{
var comment = new PullRequestReviewCommentCreate(body, commitId, path, position);
return gitHubClient.PullRequest.Comment.Create(owner, name, number, comment);
}
public IObservable<PullRequestReviewComment> CreatePullRequestReviewComment(
string owner,
string name,
int number,
string body,
int inReplyTo)
{
var comment = new PullRequestReviewCommentReplyCreate(body, inReplyTo);
return gitHubClient.PullRequest.Comment.CreateReply(owner, name, number, comment);
}
public IObservable<Gist> CreateGist(NewGist newGist)
{
return gitHubClient.Gist.Create(newGist);
@ -239,6 +263,11 @@ namespace GitHub.Api
return gitHubClient.Authorization.Delete(id, twoFactorAuthorizationCode);
}
public IObservable<IssueComment> GetIssueComments(string owner, string name, int number)
{
return gitHubClient.Issue.Comment.GetAllForIssue(owner, name, number);
}
public IObservable<PullRequest> GetPullRequest(string owner, string name, int number)
{
return gitHubClient.PullRequest.Get(owner, name, number);
@ -249,6 +278,11 @@ namespace GitHub.Api
return gitHubClient.PullRequest.Files(owner, name, number);
}
public IObservable<PullRequestReviewComment> GetPullRequestReviewComments(string owner, string name, int number)
{
return gitHubClient.PullRequest.Comment.GetAll(owner, name, number);
}
public IObservable<PullRequest> GetPullRequestsForRepository(string owner, string name)
{
return gitHubClient.PullRequest.GetAllForRepository(owner, name,

Просмотреть файл

@ -132,6 +132,8 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="Models\IssueCommentModel.cs" />
<Compile Include="Models\PullRequestReviewCommentModel.cs" />
<Compile Include="ViewModels\ViewModelBase.cs" />
<None Include="..\..\script\Key.snk" Condition="$(Buildtype) == 'Internal'">
<Link>Key.snk</Link>

Просмотреть файл

@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Git", Scope = "resource", Target = "GitHub.Resources.resources")]
[assembly: SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "GitHub.Caches.CredentialCache.#InsertObject`1(System.String,!!0,System.Nullable`1<System.DateTimeOffset>)")]
[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Git", Scope = "resource", Target = "GitHub.App.Resources.resources")]
[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object,System.Object)", Scope = "member", Target = "GitHub.Services.PullRequestService.#CreateTempFile(System.String,System.String,System.String)")]
[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object,System.Object)", Scope = "member", Target = "GitHub.Services.PullRequestService.#CreateTempFile(System.String,System.String,System.String,System.Text.Encoding)")]
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given

Просмотреть файл

@ -31,6 +31,7 @@ namespace GitHub.Models
public ReactiveList<IAccount> Accounts { get; private set; }
public string Title { get; private set; }
public IAccount User { get; private set; }
[AllowNull]
public IModelService ModelService { get; private set; }
public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password)

Просмотреть файл

@ -0,0 +1,12 @@
using System;
namespace GitHub.Models
{
public class IssueCommentModel : ICommentModel
{
public string Body { get; set; }
public int Id { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public IAccount User { get; set; }
}
}

Просмотреть файл

@ -11,8 +11,8 @@ namespace GitHub.Models
Status = status;
}
public string FileName { get; set; }
public string Sha { get; set; }
public PullRequestFileStatus Status { get; set; }
public string FileName { get; }
public string Sha { get; }
public PullRequestFileStatus Status { get; }
}
}

Просмотреть файл

@ -160,6 +160,14 @@ namespace GitHub.Models
public DateTimeOffset UpdatedAt { get; set; }
public IAccount Author { get; set; }
public IReadOnlyCollection<IPullRequestFileModel> ChangedFiles { get; set; } = new IPullRequestFileModel[0];
public IReadOnlyCollection<ICommentModel> Comments { get; set; } = new ICommentModel[0];
IReadOnlyCollection<IPullRequestReviewCommentModel> reviewComments = new IPullRequestReviewCommentModel[0];
public IReadOnlyCollection<IPullRequestReviewCommentModel> ReviewComments
{
get { return reviewComments; }
set { reviewComments = value; this.RaisePropertyChange(); }
}
IAccount assignee;
[AllowNull]

Просмотреть файл

@ -0,0 +1,20 @@
using System;
using NullGuard;
namespace GitHub.Models
{
[NullGuard(ValidationFlags.None)]
public class PullRequestReviewCommentModel : IPullRequestReviewCommentModel
{
public int Id { get; set; }
public string Path { get; set; }
public int? Position { get; set; }
public int? OriginalPosition { get; set; }
public string CommitId { get; set; }
public string OriginalCommitId { get; set; }
public string DiffHunk { get; set; }
public IAccount User { get; set; }
public string Body { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
}

Просмотреть файл

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using GitHub.ViewModels;
using ReactiveUI;
@ -71,8 +72,11 @@ This requires that errors be propagated from the viewmodel to the view and from
}
public IPullRequestModel Model { get; }
public IPullRequestSession Session { get; }
public ILocalRepositoryModel Repository { get; }
public string SourceBranchDisplayName { get; set; }
public string TargetBranchDisplayName { get; set; }
public int CommentCount { get; set; }
public bool IsLoading { get; }
public bool IsBusy { get; }
public bool IsCheckedOut { get; }

Просмотреть файл

@ -7,14 +7,14 @@ using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Primitives;
using LibGit2Sharp;
using NullGuard;
using System.Diagnostics;
using NLog;
using NullGuard;
namespace GitHub.Services
{
[Export(typeof(IGitClient))]
[PartCreationPolicy(CreationPolicy.Shared)]
[NullGuard(ValidationFlags.None)]
public class GitClient : IGitClient
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
@ -159,6 +159,58 @@ namespace GitHub.Services
});
}
public Task<Patch> Compare(
IRepository repository,
string sha1,
string sha2,
string path)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(sha1, nameof(sha1));
Guard.ArgumentNotEmptyString(sha2, nameof(sha2));
Guard.ArgumentNotEmptyString(path, nameof(path));
return Task.Factory.StartNew(() =>
{
var commit1 = repository.Lookup<Commit>(sha1);
var commit2 = repository.Lookup<Commit>(sha2);
if (commit1 != null && commit2 != null)
{
return repository.Diff.Compare<Patch>(
commit1.Tree,
commit2.Tree,
new[] { path });
}
else
{
return null;
}
});
}
public Task<ContentChanges> CompareWith(IRepository repository, string sha, string path, [AllowNull] byte[] contents)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(sha, nameof(sha));
Guard.ArgumentNotEmptyString(path, nameof(path));
return Task.Factory.StartNew(() =>
{
var commit = repository.Lookup<Commit>(sha);
if (commit != null)
{
var contentStream = contents != null ? new MemoryStream(contents) : new MemoryStream();
var blob1 = commit[path]?.Target as Blob ?? repository.ObjectDatabase.CreateBlob(new MemoryStream());
var blob2 = repository.ObjectDatabase.CreateBlob(contentStream, path);
return repository.Diff.Compare(blob1, blob2);
}
return null;
});
}
public Task<T> GetConfig<T>(IRepository repository, string key)
{
Guard.ArgumentNotNull(repository, nameof(repository));
@ -239,36 +291,107 @@ namespace GitHub.Services
}
[return: AllowNull]
public async Task<string> ExtractFile(IRepository repository, string commitSha, string fileName)
public Task<string> ExtractFile(IRepository repository, string commitSha, string fileName)
{
var commit = repository.Lookup<Commit>(commitSha);
return Task.Factory.StartNew(() =>
{
var commit = repository.Lookup<Commit>(commitSha);
if(commit == null)
{
throw new FileNotFoundException("Couldn't find '" + fileName + "' at commit " + commitSha + ".");
}
if (commit == null)
var blob = commit[fileName]?.Target as Blob;
return blob?.GetContentText();
});
}
[return: AllowNull]
public Task<byte[]> ExtractFileBinary(IRepository repository, string commitSha, string fileName)
{
return Task.Factory.StartNew(() =>
{
var commit = repository.Lookup<Commit>(commitSha);
if (commit == null)
{
throw new FileNotFoundException("Couldn't find '" + fileName + "' at commit " + commitSha + ".");
}
var blob = commit[fileName]?.Target as Blob;
if (blob != null)
{
using (var m = new MemoryStream())
{
var content = blob.GetContentStream();
content.CopyTo(m);
return m.ToArray();
}
}
return null;
});
}
public Task<bool> IsModified(IRepository repository, string path, [AllowNull] byte[] contents)
{
return Task.Factory.StartNew(() =>
{
if (repository.RetrieveStatus(path) == FileStatus.Unaltered)
{
var head = repository.Head[path];
if (head.TargetType != TreeEntryTargetType.Blob)
{
return false;
}
var blob1 = (Blob)head.Target;
using (var s = contents != null ? new MemoryStream(contents) : new MemoryStream())
{
var blob2 = repository.ObjectDatabase.CreateBlob(s, path);
var diff = repository.Diff.Compare(blob1, blob2);
return diff.LinesAdded != 0 || diff.LinesDeleted != 0;
}
}
return true;
});
}
public async Task<string> GetPullRequestMergeBase(IRepository repo, string remoteName, string baseSha, string headSha, string baseRef, int pullNumber)
{
var mergeBase = GetMergeBase(repo, baseSha, headSha);
if (mergeBase == null)
{
var pullHeadRef = $"refs/pull/{pullNumber}/head";
await Fetch(repo, remoteName, baseRef, pullHeadRef);
mergeBase = GetMergeBase(repo, baseSha, headSha);
}
return mergeBase;
}
static string GetMergeBase(IRepository repo, string a, string b)
{
var aCommit = repo.Lookup<Commit>(a);
var bCommit = repo.Lookup<Commit>(b);
if (aCommit == null || bCommit == null)
{
return null;
}
var blob = commit[fileName]?.Target as Blob;
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var tempFileName = $"{Path.GetFileNameWithoutExtension(fileName)}@{commitSha}{Path.GetExtension(fileName)}";
var tempFile = Path.Combine(tempDir, tempFileName);
var baseCommit = repo.ObjectDatabase.FindMergeBase(aCommit, bCommit);
return baseCommit?.Sha;
}
Directory.CreateDirectory(tempDir);
if (blob != null)
public Task<bool> IsHeadPushed(IRepository repo)
{
return Task.Factory.StartNew(() =>
{
using (var source = blob.GetContentStream(new FilteringOptions(fileName)))
using (var destination = File.OpenWrite(tempFile))
{
await source.CopyToAsync(destination);
}
}
else
{
File.Create(tempFile).Dispose();
}
return tempFile;
return repo.Head.IsTracking && repo.Head.Tip.Sha == repo.Head.TrackedBranch.Tip.Sha;
});
}
static bool IsCanonical(string s)

Просмотреть файл

@ -41,6 +41,11 @@ namespace GitHub.Services
this.avatarProvider = avatarProvider;
}
public IObservable<IAccount> GetCurrentUser()
{
return GetUserFromCache().Select(Create);
}
public IObservable<GitIgnoreItem> GetGitIgnoreTemplates()
{
return Observable.Defer(() =>
@ -138,7 +143,7 @@ namespace GitHub.Services
.Concat(GetAllRepositoriesForAllOrganizations());
}
public IObservable<AccountCacheItem> GetUserFromCache()
IObservable<AccountCacheItem> GetUserFromCache()
{
return Observable.Defer(() => hostCache.GetObject<AccountCacheItem>("user"));
}
@ -192,8 +197,20 @@ namespace GitHub.Services
Observable.CombineLatest(
apiClient.GetPullRequest(repo.CloneUrl.Owner, repo.CloneUrl.RepositoryName, number),
apiClient.GetPullRequestFiles(repo.CloneUrl.Owner, repo.CloneUrl.RepositoryName, number).ToList(),
(pr, files) => new { PullRequest = pr, Files = files })
.Select(x => PullRequestCacheItem.Create(x.PullRequest, (IReadOnlyList<PullRequestFile>)x.Files)),
apiClient.GetIssueComments(repo.CloneUrl.Owner, repo.CloneUrl.RepositoryName, number).ToList(),
apiClient.GetPullRequestReviewComments(repo.CloneUrl.Owner, repo.CloneUrl.RepositoryName, number).ToList(),
(pr, files, comments, reviewComments) => new
{
PullRequest = pr,
Files = files,
Comments = comments,
ReviewComments = reviewComments
})
.Select(x => PullRequestCacheItem.Create(
x.PullRequest,
(IReadOnlyList<PullRequestFile>)x.Files,
(IReadOnlyList<IssueComment>)x.Comments,
(IReadOnlyList<PullRequestReviewComment>)x.ReviewComments)),
TimeSpan.Zero,
TimeSpan.FromDays(7))
.Select(Create);
@ -400,6 +417,28 @@ namespace GitHub.Services
Body = prCacheItem.Body ?? string.Empty,
ChangedFiles = prCacheItem.ChangedFiles.Select(x =>
(IPullRequestFileModel)new PullRequestFileModel(x.FileName, x.Sha, x.Status)).ToList(),
Comments = prCacheItem.Comments.Select(x =>
(ICommentModel)new IssueCommentModel
{
Id = x.Id,
Body = x.Body,
User = Create(x.User),
CreatedAt = x.CreatedAt ?? DateTimeOffset.MinValue,
}).ToList(),
ReviewComments = prCacheItem.ReviewComments.Select(x =>
(IPullRequestReviewCommentModel)new PullRequestReviewCommentModel
{
Id = x.Id,
Path = x.Path,
Position = x.Position,
OriginalPosition = x.OriginalPosition,
CommitId = x.CommitId,
OriginalCommitId = x.OriginalCommitId,
DiffHunk = x.DiffHunk,
User = Create(x.User),
Body = x.Body,
CreatedAt = x.CreatedAt,
}).ToList(),
CommentCount = prCacheItem.CommentCount,
CommitCount = prCacheItem.CommitCount,
CreatedAt = prCacheItem.CreatedAt,
@ -489,22 +528,30 @@ namespace GitHub.Services
{
public static PullRequestCacheItem Create(PullRequest pr)
{
return new PullRequestCacheItem(pr, new PullRequestFile[0]);
return new PullRequestCacheItem(pr, new PullRequestFile[0], new IssueComment[0], new PullRequestReviewComment[0]);
}
public static PullRequestCacheItem Create(PullRequest pr, IReadOnlyList<PullRequestFile> files)
public static PullRequestCacheItem Create(
PullRequest pr,
IReadOnlyList<PullRequestFile> files,
IReadOnlyList<IssueComment> comments,
IReadOnlyList<PullRequestReviewComment> reviewComments)
{
return new PullRequestCacheItem(pr, files);
return new PullRequestCacheItem(pr, files, comments, reviewComments);
}
public PullRequestCacheItem() {}
public PullRequestCacheItem(PullRequest pr)
: this(pr, new PullRequestFile[0])
: this(pr, new PullRequestFile[0], new IssueComment[0], new PullRequestReviewComment[0])
{
}
public PullRequestCacheItem(PullRequest pr, IReadOnlyList<PullRequestFile> files)
public PullRequestCacheItem(
PullRequest pr,
IReadOnlyList<PullRequestFile> files,
IReadOnlyList<IssueComment> comments,
IReadOnlyList<PullRequestReviewComment> reviewComments)
{
Title = pr.Title;
Number = pr.Number;
@ -530,6 +577,8 @@ namespace GitHub.Services
UpdatedAt = pr.UpdatedAt;
Body = pr.Body;
ChangedFiles = files.Select(x => new PullRequestFileCacheItem(x)).ToList();
Comments = comments.Select(x => new IssueCommentCacheItem(x)).ToList();
ReviewComments = reviewComments.Select(x => new PullRequestReviewCommentCacheItem(x)).ToList();
State = GetState(pr);
IsOpen = pr.State == ItemState.Open;
Merged = pr.Merged;
@ -549,6 +598,8 @@ namespace GitHub.Services
public DateTimeOffset UpdatedAt { get; set; }
public string Body { get; set; }
public IList<PullRequestFileCacheItem> ChangedFiles { get; set; } = new PullRequestFileCacheItem[0];
public IList<IssueCommentCacheItem> Comments { get; set; } = new IssueCommentCacheItem[0];
public IList<PullRequestReviewCommentCacheItem> ReviewComments { get; set; } = new PullRequestReviewCommentCacheItem[0];
// Nullable for compatibility with old caches.
public PullRequestStateEnum? State { get; set; }
@ -593,6 +644,60 @@ namespace GitHub.Services
public PullRequestFileStatus Status { get; set; }
}
[NullGuard(ValidationFlags.None)]
public class IssueCommentCacheItem
{
public IssueCommentCacheItem()
{
}
public IssueCommentCacheItem(IssueComment comment)
{
Id = comment.Id;
User = new AccountCacheItem(comment.User);
Body = comment.Body;
CreatedAt = comment.CreatedAt;
}
public int Id { get; }
public AccountCacheItem User { get; set; }
public string Body { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
}
[NullGuard(ValidationFlags.None)]
public class PullRequestReviewCommentCacheItem
{
public PullRequestReviewCommentCacheItem()
{
}
public PullRequestReviewCommentCacheItem(PullRequestReviewComment comment)
{
Id = comment.Id;
Path = comment.Path;
Position = comment.Position;
OriginalPosition = comment.OriginalPosition;
CommitId = comment.CommitId;
OriginalCommitId = comment.OriginalCommitId;
DiffHunk = comment.DiffHunk;
User = new AccountCacheItem(comment.User);
Body = comment.Body;
CreatedAt = comment.CreatedAt;
}
public int Id { get; }
public string Path { get; set; }
public int? Position { get; set; }
public int? OriginalPosition { get; set; }
public string CommitId { get; set; }
public string OriginalCommitId { get; set; }
public string DiffHunk { get; set; }
public AccountCacheItem User { get; set; }
public string Body { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
[NullGuard(ValidationFlags.None)]
public class GitReferenceCacheItem
{

Просмотреть файл

@ -105,10 +105,11 @@ namespace GitHub.Services
public IObservable<Unit> Push(ILocalRepositoryModel repository)
{
return Observable.Defer(() =>
return Observable.Defer(async () =>
{
var repo = gitService.GetRepository(repository.LocalPath);
return gitClient.Push(repo, repo.Head.TrackedBranch.UpstreamBranchCanonicalName, repo.Head.Remote.Name).ToObservable();
var remote = await gitClient.GetHttpRemote(repo, repo.Head.Remote.Name);
return gitClient.Push(repo, repo.Head.TrackedBranch.UpstreamBranchCanonicalName, remote.Name).ToObservable();
});
}
@ -132,16 +133,18 @@ namespace GitHub.Services
else
{
var refSpec = $"{pullRequest.Head.Ref}:{localBranchName}";
var prConfigKey = $"branch.{localBranchName}.{SettingGHfVSPullRequest}";
var remoteName = await CreateRemote(repo, pullRequest.Head.RepositoryCloneUrl);
await gitClient.Fetch(repo, remoteName);
await gitClient.Fetch(repo, remoteName, new[] { refSpec });
await gitClient.Checkout(repo, localBranchName);
await gitClient.SetTrackingBranch(repo, localBranchName, $"refs/remotes/{remoteName}/{pullRequest.Head.Ref}");
await gitClient.SetConfig(repo, prConfigKey, pullRequest.Number.ToString());
}
// Store the PR number in the branch config with the key "ghfvs-pr".
var prConfigKey = $"branch.{localBranchName}.{SettingGHfVSPullRequest}";
await gitClient.SetConfig(repo, prConfigKey, pullRequest.Number.ToString());
return Observable.Return(Unit.Default);
});
}
@ -169,8 +172,13 @@ namespace GitHub.Services
return Observable.Defer(async () =>
{
var repo = gitService.GetRepository(repository.LocalPath);
if (repo.Head.Remote != null)
await gitClient.Fetch(repo, repo.Head.Remote.Name);
{
var remote = await gitClient.GetHttpRemote(repo, repo.Head.Remote.Name);
await gitClient.Fetch(repo, remote.Name);
}
return Observable.Return(repo.Head.TrackingDetails);
});
}
@ -197,6 +205,27 @@ namespace GitHub.Services
});
}
public IObservable<bool> EnsureLocalBranchesAreMarkedAsPullRequests(ILocalRepositoryModel repository, IPullRequestModel pullRequest)
{
return Observable.Defer(async () =>
{
var repo = gitService.GetRepository(repository.LocalPath);
var branches = GetLocalBranchesInternal(repository, repo, pullRequest).Select(x => new BranchModel(x, repository));
var result = false;
foreach (var branch in branches)
{
if (!await IsBranchMarkedAsPullRequest(repo, branch.Name, pullRequest.Number))
{
await MarkBranchAsPullRequest(repo, branch.Name, pullRequest.Number);
result = true;
}
}
return Observable.Return(result);
});
}
public bool IsPullRequestFromFork(ILocalRepositoryModel repository, IPullRequestModel pullRequest)
{
if (pullRequest.Head?.Label != null && pullRequest.Base?.Label != null)
@ -242,93 +271,92 @@ namespace GitHub.Services
}
await gitClient.Checkout(repo, branchName);
await MarkBranchAsPullRequest(repo, branchName, pullRequest.Number);
}
return Observable.Return(Unit.Default);
});
}
public IObservable<Unit> UnmarkLocalBranch(ILocalRepositoryModel repository)
public IObservable<int> GetPullRequestForCurrentBranch(ILocalRepositoryModel repository)
{
return Observable.Defer(async () =>
return Observable.Defer(() =>
{
var repo = gitService.GetRepository(repository.LocalPath);
var configKey = $"branch.{repo.Head.FriendlyName}.ghfvs-pr";
await gitClient.UnsetConfig(repo, configKey);
return Observable.Return(Unit.Default);
});
}
public IObservable<string> ExtractFile(
ILocalRepositoryModel repository,
IModelService modelService,
string commitSha,
string fileName,
string fileSha)
{
return Observable.Defer(async () =>
{
var repo = gitService.GetRepository(repository.LocalPath);
var remote = await gitClient.GetHttpRemote(repo, "origin");
await gitClient.Fetch(repo, remote.Name);
var result = await GetFileFromRepositoryOrApi(repository, repo, modelService, commitSha, fileName, fileSha);
if (result == null)
{
throw new FileNotFoundException($"Could not retrieve {fileName}@{commitSha}");
}
return Observable.Return(result);
var configKey = string.Format(
CultureInfo.InvariantCulture,
"branch.{0}.{1}",
repo.Head.FriendlyName,
SettingGHfVSPullRequest);
return gitClient.GetConfig<int>(repo, configKey).ToObservable();
});
}
public IObservable<Tuple<string, string>> ExtractDiffFiles(
ILocalRepositoryModel repository,
IModelService modelService,
IPullRequestModel pullRequest,
string fileName,
string fileSha,
bool isPullRequestBranchCheckedOut)
{
return Observable.Defer(async () =>
{
var repo = gitService.GetRepository(repository.LocalPath);
var remote = await gitClient.GetHttpRemote(repo, "origin");
await gitClient.Fetch(repo, remote.Name);
// The left file is the target of the PR so this should already be fetched.
var left = await gitClient.ExtractFile(repo, pullRequest.Base.Sha, fileName);
// The right file - if it comes from a fork - may not be fetched so fall back to
// getting the file contents from the model service.
var right = isPullRequestBranchCheckedOut ?
Path.Combine(repository.LocalPath, fileName) :
await GetFileFromRepositoryOrApi(repository, repo, modelService, pullRequest.Head.Sha, fileName, fileSha);
if (left == null)
var baseSha = pullRequest.Base.Sha;
var headSha = pullRequest.Head.Sha;
var baseRef = pullRequest.Base.Ref;
string mergeBase = await gitClient.GetPullRequestMergeBase(repo, remote.Name, baseSha, headSha, baseRef, pullRequest.Number);
if (mergeBase == null)
{
throw new FileNotFoundException($"Could not retrieve {fileName}@{pullRequest.Base.Sha}");
throw new FileNotFoundException($"Couldn't find merge base between {baseSha} and {headSha}.");
}
if (right == null)
string left;
string right;
if (isPullRequestBranchCheckedOut)
{
throw new FileNotFoundException($"Could not retrieve {fileName}@{pullRequest.Head.Sha}");
right = Path.Combine(repository.LocalPath, fileName);
left = await ExtractToTempFile(repo, mergeBase, fileName, GetEncoding(right));
}
else
{
left = await ExtractToTempFile(repo, mergeBase, fileName, Encoding.UTF8);
right = await ExtractToTempFile(repo, headSha, fileName, Encoding.UTF8);
}
return Observable.Return(Tuple.Create(left, right));
});
}
async Task<string> GetFileFromRepositoryOrApi(
ILocalRepositoryModel repository,
IRepository repo,
IModelService modelService,
string commitSha,
string fileName,
string fileSha)
static Encoding GetEncoding(string file)
{
return await gitClient.ExtractFile(repo, commitSha, fileName) ??
await modelService.GetFileContents(repository, commitSha, fileName, fileSha);
if (File.Exists(file))
{
var encoding = Encoding.UTF8;
if (HasPreamble(file, encoding))
{
return encoding;
}
}
return Encoding.Default;
}
static bool HasPreamble(string file, Encoding encoding)
{
using (var stream = File.OpenRead(file))
{
foreach (var b in encoding.GetPreamble())
{
if(b != stream.ReadByte())
{
return false;
}
}
}
return true;
}
public IObservable<Unit> RemoveUnusedRemotes(ILocalRepositoryModel repository)
@ -385,6 +413,23 @@ namespace GitHub.Services
return uniqueName;
}
async Task<string> ExtractToTempFile(IRepository repo, string commitSha, string fileName, Encoding encoding)
{
var contents = await gitClient.ExtractFile(repo, commitSha, fileName) ?? string.Empty;
return CreateTempFile(fileName, commitSha, contents, encoding);
}
static string CreateTempFile(string fileName, string commitSha, string contents, Encoding encoding)
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var tempFileName = $"{Path.GetFileNameWithoutExtension(fileName)}@{commitSha}{Path.GetExtension(fileName)}";
var tempFile = Path.Combine(tempDir, tempFileName);
Directory.CreateDirectory(tempDir);
File.WriteAllText(tempFile, contents, encoding);
return tempFile;
}
IEnumerable<string> GetLocalBranchesInternal(
ILocalRepositoryModel localRepository,
IRepository repository,
@ -404,6 +449,19 @@ namespace GitHub.Services
}
}
async Task<bool> IsBranchMarkedAsPullRequest(IRepository repo, string branchName, int number)
{
var prConfigKey = $"branch.{branchName}.{SettingGHfVSPullRequest}";
return await gitClient.GetConfig<int>(repo, prConfigKey) == number;
}
async Task MarkBranchAsPullRequest(IRepository repo, string branchName, int number)
{
// Store the PR number in the branch config with the key "ghfvs-pr".
var prConfigKey = $"branch.{branchName}.{SettingGHfVSPullRequest}";
await gitClient.SetConfig(repo, prConfigKey, number.ToString());
}
async Task<IPullRequestModel> PushAndCreatePR(IRepositoryHost host,
ILocalRepositoryModel sourceRepository, IRepositoryModel targetRepository,
IBranch sourceBranch, IBranch targetBranch,

Просмотреть файл

@ -27,13 +27,14 @@ namespace GitHub.ViewModels
[NullGuard(ValidationFlags.None)]
public class PullRequestDetailViewModel : PanePageViewModelBase, IPullRequestDetailViewModel
{
readonly ILocalRepositoryModel repository;
readonly IModelService modelService;
readonly IPullRequestService pullRequestsService;
readonly IPullRequestSessionManager sessionManager;
readonly IUsageTracker usageTracker;
IPullRequestModel model;
string sourceBranchDisplayName;
string targetBranchDisplayName;
int commentCount;
string body;
IReadOnlyList<IPullRequestChangeNode> changedFilesTree;
IPullRequestCheckoutState checkoutState;
@ -51,17 +52,20 @@ namespace GitHub.ViewModels
/// <param name="connectionRepositoryHostMap">The connection repository host map.</param>
/// <param name="teservice">The team explorer service.</param>
/// <param name="pullRequestsService">The pull requests service.</param>
/// <param name="sessionManager">The pull request session manager.</param>
/// <param name="avatarProvider">The avatar provider.</param>
[ImportingConstructor]
PullRequestDetailViewModel(
IConnectionRepositoryHostMap connectionRepositoryHostMap,
ITeamExplorerServiceHolder teservice,
IPullRequestService pullRequestsService,
IPullRequestSessionManager sessionManager,
IUsageTracker usageTracker)
: this(teservice.ActiveRepo,
connectionRepositoryHostMap.CurrentRepositoryHost.ModelService,
pullRequestsService,
usageTracker)
connectionRepositoryHostMap.CurrentRepositoryHost.ModelService,
pullRequestsService,
sessionManager,
usageTracker)
{
}
@ -71,16 +75,19 @@ namespace GitHub.ViewModels
/// <param name="repositoryHost">The repository host.</param>
/// <param name="teservice">The team explorer service.</param>
/// <param name="pullRequestsService">The pull requests service.</param>
/// <param name="sessionManager">The pull request session manager.</param>
/// <param name="avatarProvider">The avatar provider.</param>
public PullRequestDetailViewModel(
ILocalRepositoryModel repository,
IModelService modelService,
IPullRequestService pullRequestsService,
IPullRequestSessionManager sessionManager,
IUsageTracker usageTracker)
{
this.repository = repository;
this.Repository = repository;
this.modelService = modelService;
this.pullRequestsService = pullRequestsService;
this.sessionManager = sessionManager;
this.usageTracker = usageTracker;
Checkout = ReactiveCommand.CreateAsyncObservable(
@ -130,6 +137,16 @@ namespace GitHub.ViewModels
}
}
/// <summary>
/// Gets the repository that the pull request relates to.
/// </summary>
public ILocalRepositoryModel Repository { get; }
/// <summary>
/// Gets the session for the pull request.
/// </summary>
public IPullRequestSession Session { get; private set; }
/// <summary>
/// Gets a string describing how to display the pull request's source branch.
/// </summary>
@ -148,6 +165,15 @@ namespace GitHub.ViewModels
private set { this.RaiseAndSetIfChanged(ref targetBranchDisplayName, value); }
}
/// <summary>
/// Gets the number of comments made on the pull request.
/// </summary>
public int CommentCount
{
get { return commentCount; }
private set { this.RaiseAndSetIfChanged(ref commentCount, value); }
}
/// <summary>
/// Gets a value indicating whether the view model is updating.
/// </summary>
@ -273,7 +299,7 @@ namespace GitHub.ViewModels
IsBusy = true;
OperationError = null;
modelService.GetPullRequest(repository, prNumber)
modelService.GetPullRequest(Repository, prNumber)
.TakeLast(1)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => Load(x).Forget());
@ -288,22 +314,25 @@ namespace GitHub.ViewModels
{
var firstLoad = (Model == null);
Model = pullRequest;
Session = await sessionManager.GetSession(pullRequest);
Title = Resources.PullRequestNavigationItemText + " #" + pullRequest.Number;
IsFromFork = pullRequestsService.IsPullRequestFromFork(repository, Model);
IsFromFork = pullRequestsService.IsPullRequestFromFork(Repository, Model);
SourceBranchDisplayName = GetBranchDisplayName(IsFromFork, pullRequest.Head?.Label);
TargetBranchDisplayName = GetBranchDisplayName(IsFromFork, pullRequest.Base.Label);
CommentCount = pullRequest.Comments.Count + pullRequest.ReviewComments.Count;
Body = !string.IsNullOrWhiteSpace(pullRequest.Body) ? pullRequest.Body : Resources.NoDescriptionProvidedMarkdown;
var changes = await pullRequestsService.GetTreeChanges(repository, pullRequest);
ChangedFilesTree = CreateChangedFilesTree(pullRequest, changes).Children.ToList();
var changes = await pullRequestsService.GetTreeChanges(Repository, pullRequest);
ChangedFilesTree = (await CreateChangedFilesTree(pullRequest, changes)).Children.ToList();
var localBranches = await pullRequestsService.GetLocalBranches(repository, pullRequest).ToList();
IsCheckedOut = localBranches.Contains(repository.CurrentBranch);
var localBranches = await pullRequestsService.GetLocalBranches(Repository, pullRequest).ToList();
IsCheckedOut = localBranches.Contains(Repository.CurrentBranch);
if (IsCheckedOut)
{
var divergence = await pullRequestsService.CalculateHistoryDivergence(repository, Model.Number);
var divergence = await pullRequestsService.CalculateHistoryDivergence(Repository, Model.Number);
var pullEnabled = divergence.BehindBy > 0;
var pushEnabled = divergence.AheadBy > 0 && !pullEnabled;
string pullToolTip;
@ -344,8 +373,8 @@ namespace GitHub.ViewModels
{
var caption = localBranches.Count > 0 ?
string.Format(Resources.PullRequestDetailsCheckout, localBranches.First().DisplayName) :
string.Format(Resources.PullRequestDetailsCheckoutTo, await pullRequestsService.GetDefaultLocalBranchName(repository, Model.Number, Model.Title));
var clean = await pullRequestsService.IsWorkingDirectoryClean(repository);
string.Format(Resources.PullRequestDetailsCheckoutTo, await pullRequestsService.GetDefaultLocalBranchName(Repository, Model.Number, Model.Title));
var clean = await pullRequestsService.IsWorkingDirectoryClean(Repository);
string disabled = null;
if (pullRequest.Head == null || !pullRequest.Head.RepositoryCloneUrl.IsValidUri)
@ -370,7 +399,7 @@ namespace GitHub.ViewModels
if (!isInCheckout)
{
pullRequestsService.RemoveUnusedRemotes(repository).Subscribe(_ => { });
pullRequestsService.RemoveUnusedRemotes(Repository).Subscribe(_ => { });
}
}
@ -382,7 +411,7 @@ namespace GitHub.ViewModels
public Task<Tuple<string, string>> ExtractDiffFiles(IPullRequestFileNode file)
{
var path = Path.Combine(file.DirectoryPath, file.FileName);
return pullRequestsService.ExtractDiffFiles(repository, modelService, model, path, file.Sha, IsCheckedOut).ToTask();
return pullRequestsService.ExtractDiffFiles(Repository, model, path, IsCheckedOut).ToTask();
}
/// <summary>
@ -392,7 +421,7 @@ namespace GitHub.ViewModels
/// <returns>The full path to the file in the working directory.</returns>
public string GetLocalFilePath(IPullRequestFileNode file)
{
return Path.Combine(repository.LocalPath, file.DirectoryPath, file.FileName);
return Path.Combine(Repository.LocalPath, file.DirectoryPath, file.FileName);
}
void SubscribeOperationError(ReactiveCommand<Unit> command)
@ -401,7 +430,7 @@ namespace GitHub.ViewModels
command.IsExecuting.Select(x => x).Subscribe(x => OperationError = null);
}
IPullRequestDirectoryNode CreateChangedFilesTree(IPullRequestModel pullRequest, TreeChanges changes)
async Task<IPullRequestDirectoryNode> CreateChangedFilesTree(IPullRequestModel pullRequest, TreeChanges changes)
{
var dirs = new Dictionary<string, PullRequestDirectoryNode>
{
@ -411,11 +440,16 @@ namespace GitHub.ViewModels
foreach (var changedFile in pullRequest.ChangedFiles)
{
var node = new PullRequestFileNode(
repository.LocalPath,
Repository.LocalPath,
changedFile.FileName,
changedFile.Sha,
changedFile.Status,
GetStatusDisplay(changedFile, changes));
var file = await Session.GetFile(changedFile.FileName);
var fileCommentCount = file?.WhenAnyValue(x => x.InlineCommentThreads)
.Subscribe(x => node.CommentCount = x.Count(y => y.LineNumber != -1));
var dir = GetDirectory(node.DirectoryPath, dirs);
dir.Files.Add(node);
}
@ -484,29 +518,30 @@ namespace GitHub.ViewModels
{
return Observable.Defer(async () =>
{
var localBranches = await pullRequestsService.GetLocalBranches(repository, Model).ToList();
var localBranches = await pullRequestsService.GetLocalBranches(Repository, Model).ToList();
if (localBranches.Count > 0)
{
return pullRequestsService.SwitchToBranch(repository, Model);
return pullRequestsService.SwitchToBranch(Repository, Model);
}
else
{
return pullRequestsService
.GetDefaultLocalBranchName(repository, Model.Number, Model.Title)
.SelectMany(x => pullRequestsService.Checkout(repository, Model, x));
.GetDefaultLocalBranchName(Repository, Model.Number, Model.Title)
.SelectMany(x => pullRequestsService.Checkout(Repository, Model, x));
}
}).Do(_ => usageTracker.IncrementPullRequestCheckOutCount(IsFromFork).Forget());
}
IObservable<Unit> DoPull(object unused)
{
return pullRequestsService.Pull(repository)
return pullRequestsService.Pull(Repository)
.Do(_ => usageTracker.IncrementPullRequestPullCount(IsFromFork).Forget());
}
IObservable<Unit> DoPush(object unused)
{
return pullRequestsService.Push(repository)
return pullRequestsService.Push(Repository)
.Do(_ => usageTracker.IncrementPullRequestPushCount(IsFromFork).Forget());
}

Просмотреть файл

@ -2,14 +2,17 @@
using System.IO;
using GitHub.Models;
using NullGuard;
using ReactiveUI;
namespace GitHub.ViewModels
{
/// <summary>
/// A file node in a pull request changes tree.
/// </summary>
public class PullRequestFileNode : IPullRequestFileNode
public class PullRequestFileNode : ReactiveObject, IPullRequestFileNode
{
int commentCount;
/// <summary>
/// Initializes a new instance of the <see cref="PullRequestFileNode"/> class.
/// </summary>
@ -18,7 +21,12 @@ namespace GitHub.ViewModels
/// <param name="sha">The SHA of the file.</param>
/// <param name="status">The way the file was changed.</param>
/// <param name="statusDisplay">The string to display in the [message] box next to the filename.</param>
public PullRequestFileNode(string repositoryPath, string path, string sha, PullRequestFileStatus status, [AllowNull] string statusDisplay)
public PullRequestFileNode(
string repositoryPath,
string path,
string sha,
PullRequestFileStatus status,
[AllowNull] string statusDisplay)
{
FileName = Path.GetFileName(path);
DirectoryPath = Path.GetDirectoryName(path);
@ -57,5 +65,14 @@ namespace GitHub.ViewModels
/// Gets the string to display in the [message] box next to the filename.
/// </summary>
public string StatusDisplay { [return: AllowNull] get; }
/// <summary>
/// Gets or sets the number of review comments on the file.
/// </summary>
public int CommentCount
{
get { return commentCount; }
set { this.RaiseAndSetIfChanged(ref commentCount, value); }
}
}
}

Просмотреть файл

@ -36,10 +36,44 @@ namespace GitHub.Api
IObservable<string> GetGitIgnoreTemplates();
IObservable<LicenseMetadata> GetLicenses();
IObservable<Unit> DeleteApplicationAuthorization(int id, string twoFactorAuthorizationCode);
IObservable<IssueComment> GetIssueComments(string owner, string name, int number);
IObservable<PullRequest> GetPullRequest(string owner, string name, int number);
IObservable<PullRequestFile> GetPullRequestFiles(string owner, string name, int number);
IObservable<PullRequestReviewComment> GetPullRequestReviewComments(string owner, string name, int number);
IObservable<PullRequest> GetPullRequestsForRepository(string owner, string name);
IObservable<PullRequest> CreatePullRequest(NewPullRequest pullRequest, string owner, string repo);
/// <summary>
/// Creates a new PR review comment.
/// </summary>
/// <param name="owner">The repository owner.</param>
/// <param name="name">The repository name.</param>
/// <param name="number">The pull request number.</param>
/// <param name="body">The comment body.</param>
/// <param name="commitId">THe SHA of the commit to comment on.</param>
/// <param name="path">The relative path of the file to comment on.</param>
/// <param name="position">The line index in the diff to comment on.</param>
/// <returns></returns>
IObservable<PullRequestReviewComment> CreatePullRequestReviewComment(
string owner,
string name,
int number,
string body,
string commitId,
string path,
int position);
/// <summary>
/// Creates a new PR review comment reply.
/// </summary>
/// <param name="owner">The repository owner.</param>
/// <param name="name">The repository name.</param>
/// <param name="number">The pull request number.</param>
/// <param name="body">The comment body.</param>
/// <param name="inReplyTo">The comment ID to reply to.</param>
/// <returns></returns>
IObservable<PullRequestReviewComment> CreatePullRequestReviewComment(string owner, string name, int number, string body, int inReplyTo);
IObservable<Branch> GetBranches(string owner, string repo);
IObservable<Repository> GetRepositories();
IObservable<Repository> GetRepository(string owner, string repo);

Просмотреть файл

@ -51,6 +51,14 @@
<HintPath>..\..\packages\LibGit2Sharp.0.22.0\lib\net40\LibGit2Sharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.CoreUtility, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.CoreUtility.14.3.25407\lib\net45\Microsoft.VisualStudio.CoreUtility.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Text.Data, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Text.Data.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Data.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
@ -92,11 +100,17 @@
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="Models\IAvatarContainer.cs" />
<Compile Include="Models\IConnectionRepositoryHostMap.cs" />
<Compile Include="Models\IEditorContentSource.cs" />
<Compile Include="Models\IInlineCommentThreadModel.cs" />
<Compile Include="Models\IPullRequestSessionFile.cs" />
<Compile Include="Models\IRepositoryHosts.cs" />
<Compile Include="Models\PullRequestTextBufferInfo.cs" />
<Compile Include="Models\UserAndScopes.cs" />
<Compile Include="Services\IModelService.cs" />
<Compile Include="Services\IGistPublishService.cs" />
<Compile Include="Services\IPullRequestSession.cs" />
<Compile Include="Services\IPullRequestService.cs" />
<Compile Include="Services\IPullRequestSessionManager.cs" />
<Compile Include="ViewModels\IGistCreationViewModel.cs" />
<Compile Include="Services\NotificationDispatcher.cs" />
<Compile Include="ViewModels\IHasCancel.cs" />

Просмотреть файл

@ -0,0 +1,17 @@
using System;
using System.Threading.Tasks;
namespace GitHub.Models
{
/// <summary>
/// Represents a source of editor content for a <see cref="IPullRequestSessionFile"/>.
/// </summary>
public interface IEditorContentSource
{
/// <summary>
/// Gets the file contents from the editor.
/// </summary>
/// <returns>A task returning the editor content.</returns>
Task<byte[]> GetContent();
}
}

Просмотреть файл

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
namespace GitHub.Models
{
/// <summary>
/// Represents a thread of inline comments on an <see cref="IPullRequestSessionFile"/>.
/// </summary>
public interface IInlineCommentThreadModel
{
/// <summary>
/// Gets or sets the comments in the thread.
/// </summary>
IReadOnlyList<IPullRequestReviewCommentModel> Comments { get; }
/// <summary>
/// Gets the last five lines of the thread's diff hunk, in reverse order.
/// </summary>
IList<DiffLine> DiffMatch { get; }
/// <summary>
/// Gets the type of diff line that the thread was left on
/// </summary>
DiffChangeType DiffLineType { get; }
/// <summary>
/// Gets or sets a value indicating that the <see cref="LineNumber"/> is approximate and
/// needs to be updated.
/// </summary>
/// <remarks>
/// As edits are made, the <see cref="LineNumber"/> for a thread can be shifted up or down,
/// but until <see cref="IPullRequestSession.RecaluateLineNumbers"/> is called we can't tell
/// whether the comment is still valid at the new position. This property indicates such a
/// state.
/// </remarks>
bool IsStale { get; set; }
/// <summary>
/// Gets or sets the 0-based line number of the comment.
/// </summary>
int LineNumber { get; set; }
/// <summary>
/// Gets the SHA of the commit that the thread was left con.
/// </summary>
string OriginalCommitSha { get; }
/// <summary>
/// Gets the 1-based line number in the original diff that the thread was left on.
/// </summary>
int OriginalPosition { get; }
/// <summary>
/// Gets the relative path to the file that the thread is on.
/// </summary>
string RelativePath { get; }
}
}

Просмотреть файл

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using GitHub.Services;
namespace GitHub.Models
{
/// <summary>
/// A file in a pull request session.
/// </summary>
/// <remarks>
/// A pull request session file represents the real-time state of a file in a pull request in
/// the IDE. If the pull request branch is checked out, it represents the state of a file from
/// the pull request model updated to the current state of the code on disk and in the editor.
/// </remarks>
/// <seealso cref="IPullRequestSession"/>
/// <seealso cref="IPullRequestSessionManager"/>
public interface IPullRequestSessionFile : INotifyPropertyChanged
{
/// <summary>
/// Gets the SHA of the current commit of the file, or null if the file has uncommitted
/// changes.
/// </summary>
string CommitSha { get; }
/// <summary>
/// Gets the path to the file relative to the repository.
/// </summary>
string RelativePath { get; }
/// <summary>
/// Gets the diff between the PR merge base and the current state of the file.
/// </summary>
IList<DiffChunk> Diff { get; }
/// <summary>
/// Gets the source for the editor contents for the file.
/// </summary>
IEditorContentSource ContentSource { get; }
/// <summary>
/// Gets the inline comments threads for the file.
/// </summary>
IReadOnlyList<IInlineCommentThreadModel> InlineCommentThreads { get; }
}
}

Просмотреть файл

@ -0,0 +1,45 @@
using System;
using GitHub.Services;
namespace GitHub.Models
{
/// <summary>
/// When attached as a property to a Visual Studio ITextBuffer, informs the inline comment
/// tagger that the buffer represents a buffer opened from a pull request.
/// </summary>
public class PullRequestTextBufferInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="PullRequestTextBufferInfo"/> class.
/// </summary>
/// <param name="session">The pull request session.</param>
/// <param name="filePath">The full path to the file.</param>
/// <param name="isLeftComparisonBuffer">
/// Whether the buffer represents the left-hand-side of a comparison.
/// </param>
public PullRequestTextBufferInfo(
IPullRequestSession session,
string filePath,
bool isLeftComparisonBuffer)
{
Session = session;
FilePath = filePath;
IsLeftComparisonBuffer = isLeftComparisonBuffer;
}
/// <summary>
/// Gets the pull request session.
/// </summary>
public IPullRequestSession Session { get; }
/// <summary>
/// Gets the full path to the file.
/// </summary>
public string FilePath { get; }
/// <summary>
/// Gets a value indicating whether the buffer represents the left-hand-side of a comparison.
/// </summary>
public bool IsLeftComparisonBuffer { get; }
}
}

Просмотреть файл

@ -68,6 +68,30 @@ namespace GitHub.Services
/// </returns>
Task<TreeChanges> Compare(IRepository repository, string sha1, string sha2, bool detectRenames = false);
/// <summary>
/// Compares a file in two commits.
/// </summary>
/// <param name="repository">The repository</param>
/// <param name="sha1">The SHA of the first commit.</param>
/// <param name="sha2">The SHA of the second commit.</param>
/// <param name="path">The relative path to the file.</param>
/// <returns>
/// A <see cref="Patch"/> object or null if one of the commits could not be found in the repository.
/// </returns>
Task<Patch> Compare(IRepository repository, string sha1, string sha2, string path);
/// <summary>
/// Compares a file in a commit to a string.
/// </summary>
/// <param name="repository">The repository</param>
/// <param name="sha">The SHA of the first commit.</param>
/// <param name="path">The relative path to the file.</param>
/// <param name="contents">The contents to compare with the file.</param>
/// <returns>
/// A <see cref="Patch"/> object or null if the commit could not be found in the repository.
/// </returns>
Task<ContentChanges> CompareWith(IRepository repository, string sha, string path, byte[] contents);
/// Gets the value of a configuration key.
/// </summary>
/// <param name="repository">The repository.</param>
@ -119,8 +143,49 @@ namespace GitHub.Services
/// <param name="commitSha">The SHA of the commit.</param>
/// <param name="fileName">The path to the file, relative to the repository.</param>
/// <returns>
/// The filename of a temporary file containing the file contents.
/// The contents of the file, or null if the file was not found at the specified commit.
/// </returns>
Task<string> ExtractFile(IRepository repository, string commitSha, string fileName);
/// <summary>
/// Extracts a file at a specified commit from the repository as binary data.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="commitSha">The SHA of the commit.</param>
/// <param name="fileName">The path to the file, relative to the repository.</param>
/// <returns>
/// The contents of the file, or null if the file was not found at the specified commit.
/// </returns>
Task<byte[]> ExtractFileBinary(IRepository repository, string commitSha, string fileName);
/// <summary>
/// Checks whether the latest commit of a file in the repository has the specified file
/// contents.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="path">The relative path to the file.</param>
/// <param name="contents">The file contents to test.</param>
/// <returns></returns>
Task<bool> IsModified(IRepository repository, string path, byte[] contents);
/// <summary>
/// Find the merge base SHA between two commits.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="remoteName">The name of the remote (e.g. 'origin').</param>
/// <param name="baseSha">The PR base SHA.</param>
/// <param name="headSha">The PR head SHA.</param>
/// <param name="baseRef">The PR base ref (e.g. 'master').</param>
/// <param name="pullNumber">The PR number.</param>
/// <returns>
/// The merge base SHA or null.
/// </returns>
Task<string> GetPullRequestMergeBase(IRepository repo, string remoteName, string baseSha, string headSha, string baseRef, int pullNumber);
/// Checks whether the current head is pushed to its remote tracking branch.
/// </summary>
/// <param name="repository">The repository.</param>
/// <returns></returns>
Task<bool> IsHeadPushed(IRepository repo);
}
}

Просмотреть файл

@ -4,7 +4,6 @@ using System.Reactive;
using GitHub.Models;
using GitHub.Caches;
using GitHub.Collections;
using System.Threading.Tasks;
namespace GitHub.Services
{
@ -14,7 +13,7 @@ namespace GitHub.Services
/// </summary>
public interface IModelService : IDisposable
{
IObservable<AccountCacheItem> GetUserFromCache();
IObservable<IAccount> GetCurrentUser();
IObservable<Unit> InsertUser(AccountCacheItem user);
IObservable<IReadOnlyList<IAccount>> GetAccounts();
ITrackingCollection<IRemoteRepositoryModel> GetRepositories(ITrackingCollection<IRemoteRepositoryModel> collection);

Просмотреть файл

@ -53,10 +53,27 @@ namespace GitHub.Services
/// <summary>
/// Gets the local branches that exist for the specified pull request.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="pullRequest">The pull request details.</param>
/// <returns></returns>
IObservable<IBranch> GetLocalBranches(ILocalRepositoryModel repository, IPullRequestModel pullRequest);
/// <summary>
/// Ensures that all local branches for the specified pull request are marked as PR branches.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="pullRequest">The pull request details.</param>
/// <returns>
/// An observable that produces a single value indicating whether a change to the repository was made.
/// </returns>
/// <remarks>
/// Pull request branches are marked in the local repository with a config value so that they can
/// be easily identified without a roundtrip to the server. This method ensures that the local branches
/// for the specified pull request are indeed marked and returns a value indicating whether any branches
/// have had the mark added.
/// </remarks>
IObservable<bool> EnsureLocalBranchesAreMarkedAsPullRequests(ILocalRepositoryModel repository, IPullRequestModel pullRequest);
/// <summary>
/// Determines whether the specified pull request is from a fork.
/// </summary>
@ -90,27 +107,18 @@ namespace GitHub.Services
IObservable<TreeChanges> GetTreeChanges(ILocalRepositoryModel repository, IPullRequestModel pullRequest);
/// <summary>
/// Removes any association between the current branch and a pull request.
/// Gets the pull request associated with the current branch.
/// </summary>
/// <param name="repository">The repository.</param>
/// <returns></returns>
IObservable<Unit> UnmarkLocalBranch(ILocalRepositoryModel repository);
/// <summary>
/// Extracts a file at a specified commit from the repository.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="modelService">A model service to use as a cache if the file is not fetched.</param>
/// <param name="commitSha">The SHA of the commit.</param>
/// <param name="fileName">The path to the file, relative to the repository.</param>
/// <param name="fileSha">The SHA of the file in the pull request.</param>
/// <returns>The filename of the extracted file.</returns>
IObservable<string> ExtractFile(
ILocalRepositoryModel repository,
IModelService modelService,
string commitSha,
string fileName,
string fileSha);
/// <returns>
/// An observable that produces a single value: the pull request number, or 0 if the
/// current branch is not a PR branch.
/// </returns>
/// <remarks>
/// This method does not do an API request - it simply checks the mark left in the git
/// config by <see cref="Checkout(ILocalRepositoryModel, IPullRequestModel, string)"/>.
/// </remarks>
IObservable<int> GetPullRequestForCurrentBranch(ILocalRepositoryModel repository);
/// <summary>
/// Gets the left and right files for a diff.
@ -127,10 +135,8 @@ namespace GitHub.Services
/// <returns>The paths of the left and right files for the diff.</returns>
IObservable<Tuple<string, string>> ExtractDiffFiles(
ILocalRepositoryModel repository,
IModelService modelService,
IPullRequestModel pullRequest,
string fileName,
string fileSha,
bool isPullRequestBranchCheckedOut);
/// <summary>

Просмотреть файл

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.Models;
using ReactiveUI;
namespace GitHub.Services
{
/// <summary>
/// A pull request session used to display inline reviews.
/// </summary>
/// <remarks>
/// A pull request session represents the real-time state of a pull request in the IDE.
/// It takes the pull request model and updates according to the current state of the
/// repository on disk and in the editor.
/// </remarks>
public interface IPullRequestSession
{
/// <summary>
/// Gets a value indicating whether the pull request branch is the currently checked out branch.
/// </summary>
bool IsCheckedOut { get; }
/// <summary>
/// Gets the current user.
/// </summary>
IAccount User { get; }
/// <summary>
/// Gets the pull request.
/// </summary>
IPullRequestModel PullRequest { get; }
/// <summary>
/// Gets the pull request's repository.
/// </summary>
ILocalRepositoryModel Repository { get; }
/// <summary>
/// Adds a new comment to the session.
/// </summary>
/// <param name="comment">The comment.</param>
Task AddComment(IPullRequestReviewCommentModel comment);
/// <summary>
/// Gets all files touched by the pull request.
/// </summary>
/// <returns>
/// A list of the files touched by the pull request.
/// </returns>
Task<IReadOnlyList<IPullRequestSessionFile>> GetAllFiles();
/// <summary>
/// Gets a file touched by the pull request.
/// </summary>
/// <param name="relativePath">The relative path to the file.</param>
/// <returns>
/// A <see cref="IPullRequestSessionFile"/> object or null if the file was not touched by
/// the pull request.
/// </returns>
Task<IPullRequestSessionFile> GetFile(string relativePath);
/// <summary>
/// Gets a file touched by the pull request.
/// </summary>
/// <param name="relativePath">The relative path to the file.</param>
/// <param name="contentSource">The editor file content source.</param>
/// <returns>
/// A <see cref="IPullRequestSessionFile"/> object or null if the file was not touched by
/// the pull request.
/// </returns>
Task<IPullRequestSessionFile> GetFile(
string relativePath,
IEditorContentSource contentSource);
/// <summary>
/// Converts a path to a path relative to the current repository.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>
/// The relative path, or null if the specified path is not in the repository.
/// </returns>
string GetRelativePath(string path);
/// <summary>
/// Updates the pull request session with a new pull request model in response to a refresh
/// from the server.
/// </summary>
/// <param name="pullRequest">The new pull request model.</param>
/// <returns>A task which completes when the session has completed updating.</returns>
Task Update(IPullRequestModel pullRequest);
/// <summary>
/// Notifies the session that the contents of a file in the editor have changed.
/// </summary>
/// <param name="relativePath">The relative path to the file.</param>
/// <returns>A task which completes when the session has completed updating.</returns>
Task UpdateEditorContent(string relativePath);
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using GitHub.Models;
using Microsoft.VisualStudio.Text;
namespace GitHub.Services
{
/// <summary>
/// Manages pull request sessions.
/// </summary>
public interface IPullRequestSessionManager : INotifyPropertyChanged
{
/// <summary>
/// Gets the current pull request session.
/// </summary>
/// <returns>
/// The current pull request session, or null if the currently checked out branch is not
/// a pull request branch.
/// </returns>
IPullRequestSession CurrentSession { get; }
/// <summary>
/// Gets a pull request session for a pull request that may not be checked out.
/// </summary>
/// <param name="pullRequest">The pull request model.</param>
/// <returns>An <see cref="IPullRequestSession"/>.</returns>
/// <remarks>
/// If the provided pull request model represents the current session then that will be
/// returned. If not, a new pull request session object will be created.
/// </remarks>
Task<IPullRequestSession> GetSession(IPullRequestModel pullRequest);
/// <summary>
/// Gets information about the pull request that a Visual Studio text buffer is a part of.
/// </summary>
/// <param name="buffer">The text buffer.</param>
/// <returns>
/// A <see cref="PullRequestTextBufferInfo"/> or null if the pull request for the text
/// buffer could not be determined.
/// </returns>
/// <remarks>
/// This method looks for a <see cref="PullRequestTextBufferInfo"/> object stored in the text
/// buffer's properties.
/// </remarks>
PullRequestTextBufferInfo GetTextBufferInfo(ITextBuffer buffer);
}
}

Просмотреть файл

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using ReactiveUI;
namespace GitHub.ViewModels
@ -70,6 +71,16 @@ namespace GitHub.ViewModels
/// </summary>
IPullRequestModel Model { get; }
/// <summary>
/// Gets the session for the pull request.
/// </summary>
IPullRequestSession Session { get; }
/// <summary>
/// Gets the repository that the pull request relates to.
/// </summary>
ILocalRepositoryModel Repository { get; }
/// <summary>
/// Gets a string describing how to display the pull request's source branch.
/// </summary>
@ -80,6 +91,11 @@ namespace GitHub.ViewModels
/// </summary>
string TargetBranchDisplayName { get; }
/// <summary>
/// Gets the number of comments made on the pull request.
/// </summary>
int CommentCount { get; }
/// <summary>
/// Gets a value indicating whether the pull request branch is checked out.
/// </summary>

Просмотреть файл

@ -28,5 +28,10 @@ namespace GitHub.ViewModels
/// Gets the string to display in the [message] box next to the filename.
/// </summary>
string StatusDisplay { get; }
/// <summary>
/// Gets the number of review comments on the file.
/// </summary>
int CommentCount { get; }
}
}

Просмотреть файл

@ -2,6 +2,8 @@
<packages>
<package id="LibGit2Sharp" version="0.22.0" targetFramework="net461" />
<package id="LibGit2Sharp.NativeBinaries" version="1.0.129" targetFramework="net461" />
<package id="Microsoft.VisualStudio.CoreUtility" version="14.3.25407" targetFramework="net461" />
<package id="Microsoft.VisualStudio.Text.Data" version="14.3.25407" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net45" />
<package id="Rx-Linq" version="2.2.5-custom" targetFramework="net45" />

Просмотреть файл

@ -129,6 +129,14 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="GlobalCommands.cs" />
<Compile Include="Models\DiffChangeType.cs" />
<Compile Include="Models\DiffChunk.cs" />
<Compile Include="Models\DiffLine.cs" />
<Compile Include="Models\DiffUtilities.cs" />
<Compile Include="Models\ICommentModel.cs" />
<Compile Include="Models\IInlineCommentModel.cs" />
<Compile Include="Models\IPullRequestReviewCommentModel.cs" />
<Compile Include="ViewModels\IHasLoading.cs" />
<Compile Include="ViewModels\IPanePageViewModel.cs" />
<Compile Include="ViewModels\IViewModel.cs" />

Просмотреть файл

@ -0,0 +1,14 @@
using System;
namespace GitHub
{
public static class GlobalCommands
{
public const string CommandSetString = "C5F1193E-F300-41B3-B4C4-5A703DD3C1C6";
public const int ShowPullRequestCommentsId = 0x1000;
public const int NextInlineCommentId = 0x1001;
public const int PreviousInlineCommentId = 0x1002;
public static readonly Guid CommandSetGuid = new Guid(CommandSetString);
}
}

Просмотреть файл

@ -0,0 +1,12 @@
using System;
namespace GitHub.Models
{
public enum DiffChangeType
{
None,
Add,
Delete,
Control
}
}

Просмотреть файл

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace GitHub.Models
{
public class DiffChunk
{
public int OldLineNumber { get; set; }
public int NewLineNumber { get; set; }
public int DiffLine { get; set; }
public IList<DiffLine> Lines { get; } = new List<DiffLine>();
public override string ToString()
{
var builder = new StringBuilder();
foreach (var line in Lines)
{
builder.AppendLine(line.Content);
}
return builder.ToString();
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
using System;
namespace GitHub.Models
{
public class DiffLine
{
/// <summary>
/// Was the line added, deleted or unchanged.
/// </summary>
public DiffChangeType Type { get; set; }
/// <summary>
/// Gets the old 1-based line number.
/// </summary>
public int OldLineNumber { get; set; } = -1;
/// <summary>
/// Gets the new 1-based line number.
/// </summary>
public int NewLineNumber { get; set; } = -1;
/// <summary>
/// Gets the unified diff line number where the first chunk header is line 0.
/// </summary>
public int DiffLineNumber { get; set; } = -1;
/// <summary>
/// Gets the content of the diff line (including +, - or space).
/// </summary>
public string Content { get; set; }
public override string ToString() => Content;
}
}

Просмотреть файл

@ -0,0 +1,124 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Diagnostics.CodeAnalysis;
namespace GitHub.Models
{
public static class DiffUtilities
{
static readonly Regex ChunkHeaderRegex = new Regex(@"^@@\s+\-(\d+),?\d*\s+\+(\d+),?\d*\s@@");
public static IEnumerable<DiffChunk> ParseFragment(string diff)
{
using (var reader = new StringReader(diff))
{
string line;
DiffChunk chunk = null;
int diffLine = 0;
int oldLine = -1;
int newLine = -1;
while ((line = reader.ReadLine()) != null)
{
var headerMatch = ChunkHeaderRegex.Match(line);
if (headerMatch.Success)
{
if (chunk != null)
{
yield return chunk;
}
chunk = new DiffChunk
{
OldLineNumber = oldLine = int.Parse(headerMatch.Groups[1].Value),
NewLineNumber = newLine = int.Parse(headerMatch.Groups[2].Value),
DiffLine = diffLine,
};
}
else if (chunk != null)
{
var type = GetLineChange(line[0]);
if (type == DiffChangeType.Control)
{
// This might contain info about previous line (e.g. "\ No newline at end of file").
continue;
}
chunk.Lines.Add(new DiffLine
{
Type = type,
OldLineNumber = type != DiffChangeType.Add ? oldLine : -1,
NewLineNumber = type != DiffChangeType.Delete ? newLine : -1,
DiffLineNumber = diffLine,
Content = line,
});
switch (type)
{
case DiffChangeType.None:
++oldLine;
++newLine;
break;
case DiffChangeType.Delete:
++oldLine;
break;
case DiffChangeType.Add:
++newLine;
break;
}
}
++diffLine;
}
if (chunk != null)
{
yield return chunk;
}
}
}
public static DiffLine Match(IEnumerable<DiffChunk> diff, IList<DiffLine> target)
{
int j = 0;
if (target.Count == 0)
{
return null; // no lines to match
}
foreach (var source in diff)
{
for (var i = source.Lines.Count - 1; i >= 0; --i)
{
if (source.Lines[i].Content == target[j].Content)
{
if (++j == target.Count) return source.Lines[i + j - 1];
}
else
{
j = 0;
}
}
}
return null;
}
[SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object)")]
static DiffChangeType GetLineChange(char c)
{
switch (c)
{
case ' ': return DiffChangeType.None;
case '+': return DiffChangeType.Add;
case '-': return DiffChangeType.Delete;
case '\\': return DiffChangeType.Control;
default: throw new InvalidDataException($"Invalid diff line change char: '{c}'.");
}
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using System;
namespace GitHub.Models
{
/// <summary>
/// An issue or pull request review comment.
/// </summary>
public interface ICommentModel
{
/// <summary>
/// Gets the ID of the comment.
/// </summary>
int Id { get; }
/// <summary>
/// Gets the author of the comment.
/// </summary>
IAccount User { get; }
/// <summary>
/// Gets the body of the comment.
/// </summary>
string Body { get; }
/// <summary>
/// Gets the creation time of the comment.
/// </summary>
DateTimeOffset CreatedAt { get; }
}
}

Просмотреть файл

@ -0,0 +1,26 @@
using System;
namespace GitHub.Models
{
/// <summary>
/// Represents an pull request review comment that can be displayed inline in a code editor.
/// </summary>
public interface IInlineCommentModel
{
/// <summary>
/// Gets the 0-based line number of the comment.
/// </summary>
int LineNumber { get; }
/// <summary>
/// Gets a value indicating whether the model is stale due to a change in the underlying
/// file.
/// </summary>
bool IsStale { get; }
/// <summary>
/// Gets the original pull request review comment.
/// </summary>
IPullRequestReviewCommentModel Original { get; }
}
}

Просмотреть файл

@ -32,5 +32,7 @@ namespace GitHub.Models
IAccount Author { get; }
IAccount Assignee { get; }
IReadOnlyCollection<IPullRequestFileModel> ChangedFiles { get; }
IReadOnlyCollection<ICommentModel> Comments { get; }
IReadOnlyCollection<IPullRequestReviewCommentModel> ReviewComments { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,42 @@
using System;
namespace GitHub.Models
{
/// <summary>
/// Represents a comment on a changed file in a pull request.
/// </summary>
public interface IPullRequestReviewCommentModel : ICommentModel
{
/// <summary>
/// The relative path to the file that the comment was made on.
/// </summary>
string Path { get; }
/// <summary>
/// The line number in the diff between <see cref="IPullRequestModel.Base"/> and
/// <see cref="CommitId"/> that the comment appears on.
/// </summary>
int? Position { get; }
/// <summary>
/// The line number in the diff between <see cref="IPullRequestModel.Base"/> and
/// <see cref="OriginalCommitId"/> that the comment was originally left on.
/// </summary>
int? OriginalPosition { get; }
/// <summary>
/// The commit that the comment appears on.
/// </summary>
string CommitId { get; }
/// <summary>
/// The commit that the comment was originally left on.
/// </summary>
string OriginalCommitId { get; }
/// <summary>
/// The diff hunk used to match the pull request.
/// </summary>
string DiffHunk { get; }
}
}

Просмотреть файл

@ -12,6 +12,7 @@ namespace GitHub.VisualStudio
public const string GitHubServiceProviderId = "76909E1A-9D58-41AB-8957-C26B9550787B";
public const string StartPagePackageId = "3b764d23-faf7-486f-94c7-b3accc44a70e";
public const string CodeContainerProviderId = "6CE146CB-EF57-4F2C-A93F-5BA685317660";
public const string InlineReviewsPackageId = "248325BE-4A2D-4111-B122-E7D59BF73A35";
public const string TeamExplorerWelcomeMessage = "C529627F-8AA6-4FDB-82EB-4BFB7DB753C3";
public const string LoginManagerId = "7BA2071A-790A-4F95-BE4A-0EEAA5928AAF";

Просмотреть файл

@ -180,6 +180,21 @@ namespace GitHub.Extensions
return sb.ToString();
}
public static bool EqualsIgnoringLineEndings(this string a, string b)
{
if (ReferenceEquals(a, b))
return true;
if (a == null || b == null)
return false;
// TODO: Write a non-allocating comparison.
a = a.Replace("\r\n", "\n");
b = b.Replace("\r\n", "\n");
return a == b;
}
public static Uri ToUriSafe(this string url)
{
Uri uri;

Просмотреть файл

@ -0,0 +1,26 @@
using System;
using System.ComponentModel.Composition;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Exports a <see cref="VsCommand"/>.
/// </summary>
/// <remarks>
/// To implement a new command, inherit from the <see cref="VsCommand"/> or <see cref="Command{TParam}"/>
/// class and add an <see cref="ExportCommandAttribute"/> to the class with the type of the package that
/// the command is registered by.
/// </remarks>
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
class ExportCommandAttribute : ExportAttribute
{
public ExportCommandAttribute(Type packageType)
: base(typeof(IPackageResource))
{
PackageType = packageType;
}
public Type PackageType { get; }
}
}

Просмотреть файл

@ -0,0 +1,16 @@
using System;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Defines a MEF metadata view that matches <see cref="ExportCommandAttribute"/>.
/// </summary>
/// <remarks>
/// For more information see the Metadata and Metadata views section at
/// https://msdn.microsoft.com/en-us/library/ee155691(v=vs.110).aspx#Anchor_3
/// </remarks>
public interface IExportCommandMetadata
{
Type PackageType { get; }
}
}

Просмотреть файл

@ -0,0 +1,11 @@
using System;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Navigates to and opens the the next inline comment thread in the currently active text view.
/// </summary>
public interface INextInlineCommentCommand : IVsCommand<InlineCommentNavigationParams>
{
}
}

Просмотреть файл

@ -0,0 +1,20 @@
using System;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Represents a resource to be registered on package initialization.
/// </summary>
public interface IPackageResource
{
/// <summary>
/// Registers the resource with a package.
/// </summary>
/// <param name="package">The package registering the resource.</param>
/// <remarks>
/// This method should not be called directly, instead packages should call
/// <see cref="PackageResources.Register{TPackage}(TPackage)"/> on initialization.
/// </remarks>
void Register(IServiceProvider package);
}
}

Просмотреть файл

@ -0,0 +1,11 @@
using System;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Navigates to and opens the the previous inline comment thread in the currently active text view.
/// </summary>
public interface IPreviousInlineCommentCommand : IVsCommand<InlineCommentNavigationParams>
{
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Represents a Visual Studio command that does not accept a parameter.
/// </summary>
public interface IVsCommand : IVsCommandBase
{
/// <summary>
/// Executes the command.
/// </summary>
/// <returns>A task that tracks the execution of the command.</returns>
Task Execute();
}
/// <summary>
/// Represents a Visual Studio command that accepts a parameter.
/// </summary>
public interface IVsCommand<TParam> : IVsCommandBase
{
/// <summary>
/// Executes the command.
/// </summary>
/// <param name="parameter">The command parameter.</param>
/// <returns>A task that tracks the execution of the command.</returns>
Task Execute(TParam parameter);
}
}

Просмотреть файл

@ -0,0 +1,21 @@
using System;
using System.Windows.Input;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Represents a Visual Studio command.
/// </summary>
public interface IVsCommandBase : IPackageResource, ICommand
{
/// <summary>
/// Gets a value indicating whether the command is enabled.
/// </summary>
bool IsEnabled { get; }
/// <summary>
/// Gets a value indicating whether the command is visible.
/// </summary>
bool IsVisible { get; }
}
}

Просмотреть файл

@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using GitHub.Extensions;
using GitHub.InlineReviews.Services;
using GitHub.InlineReviews.Tags;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Differencing;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.TextManager.Interop;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Base class for commands that navigate between inline comments.
/// </summary>
abstract class InlineCommentNavigationCommand : VsCommand<InlineCommentNavigationParams>
{
readonly IViewTagAggregatorFactoryService tagAggregatorFactory;
readonly IInlineCommentPeekService peekService;
/// <summary>
/// Initializes a new instance of the <see cref="InlineCommentNavigationCommand"/> class.
/// </summary>
/// <param name="tagAggregatorFactory">The tag aggregator factory.</param>
/// <param name="peekService">The peek service.</param>
/// <param name="commandSet">The GUID of the group the command belongs to.</param>
/// <param name="commandId">The numeric identifier of the command.</param>
protected InlineCommentNavigationCommand(
IViewTagAggregatorFactoryService tagAggregatorFactory,
IInlineCommentPeekService peekService,
Guid commandSet,
int commandId)
: base(commandSet, commandId)
{
this.tagAggregatorFactory = tagAggregatorFactory;
this.peekService = peekService;
}
/// <inheritdoc/>
public override bool IsEnabled
{
get
{
var tags = GetTags(GetCurrentTextViews());
return tags.Count > 1;
}
}
/// <summary>
/// Gets the text buffer position for the line specified in the parameters or from the
/// cursor point if no line is specified or <paramref name="parameter"/> is null.
/// </summary>
/// <param name="parameter">The parameters.</param>
/// <param name="textView">The text view.</param>
/// <returns></returns>
protected int GetCursorPoint(ITextView textView, InlineCommentNavigationParams parameter)
{
if (parameter?.FromLine != null)
{
return parameter.FromLine > -1 ? GetCursorPoint(textView, parameter.FromLine.Value) : -1;
}
else
{
return textView.Caret.Position.BufferPosition.Position;
}
}
/// <summary>
/// Gets the text buffer position for the specified line.
/// </summary>
/// <param name="parameter">The parameters.</param>
/// <param name="lineNumber">The 0-based line number.</param>
/// <returns></returns>
protected int GetCursorPoint(ITextView textView, int lineNumber)
{
lineNumber = Math.Max(0, Math.Min(lineNumber, textView.TextSnapshot.LineCount - 1));
return textView.TextSnapshot.GetLineFromLineNumber(lineNumber).Start.Position;
}
/// <summary>
/// Gets the currently active text view(s) from Visual Studio.
/// </summary>
/// <returns>
/// Zero, one or two active <see cref="ITextView"/> objects.
/// </returns>
/// <remarks>
/// This method will return a single text view for a normal code window, or a pair of text
/// views if the currently active text view is a difference view in side by side mode, with
/// the first item being the side that currently has focus. If there is no active text view,
/// an empty collection will be returned.
/// </remarks>
protected IEnumerable<ITextView> GetCurrentTextViews()
{
var serviceProvider = Package;
var monitorSelection = (IVsMonitorSelection)serviceProvider.GetService(typeof(SVsShellMonitorSelection));
if (monitorSelection == null)
{
yield break;
}
object curDocument;
if (ErrorHandler.Failed(monitorSelection.GetCurrentElementValue((uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out curDocument)))
{
yield break;
}
IVsWindowFrame frame = curDocument as IVsWindowFrame;
if (frame == null)
{
yield break;
}
object docView = null;
if (ErrorHandler.Failed(frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out docView)))
{
yield break;
}
if (docView is IVsDifferenceCodeWindow)
{
var diffWindow = (IVsDifferenceCodeWindow)docView;
switch (diffWindow.DifferenceViewer.ViewMode)
{
case DifferenceViewMode.Inline:
yield return diffWindow.DifferenceViewer.InlineView;
break;
case DifferenceViewMode.SideBySide:
switch (diffWindow.DifferenceViewer.ActiveViewType)
{
case DifferenceViewType.LeftView:
yield return diffWindow.DifferenceViewer.LeftView;
yield return diffWindow.DifferenceViewer.RightView;
break;
case DifferenceViewType.RightView:
yield return diffWindow.DifferenceViewer.RightView;
yield return diffWindow.DifferenceViewer.LeftView;
break;
}
yield return diffWindow.DifferenceViewer.LeftView;
break;
case DifferenceViewMode.RightViewOnly:
yield return diffWindow.DifferenceViewer.RightView;
break;
}
}
else if (docView is IVsCodeWindow)
{
IVsTextView textView;
if (ErrorHandler.Failed(((IVsCodeWindow)docView).GetPrimaryView(out textView)))
{
yield break;
}
var model = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));
var adapterFactory = model.GetService<IVsEditorAdaptersFactoryService>();
var wpfTextView = adapterFactory.GetWpfTextView(textView);
yield return wpfTextView;
}
}
/// <summary>
/// Creates a tag aggregator for the specified text view.
/// </summary>
/// <param name="textView">The text view.</param>
/// <returns>The tag aggregator</returns>
protected ITagAggregator<InlineCommentTag> CreateTagAggregator(ITextView textView)
{
return tagAggregatorFactory.CreateTagAggregator<InlineCommentTag>(textView);
}
/// <summary>
/// Gets the <see cref="ShowInlineCommentTag"/>s for the specified text view.
/// </summary>
/// <param name="textViews">The active text views.</param>
/// <returns>A collection of <see cref="ITagInfo"/> objects, ordered by line.</returns>
protected IReadOnlyList<ITagInfo> GetTags(IEnumerable<ITextView> textViews)
{
var result = new List<ITagInfo>();
foreach (var textView in textViews)
{
var tagAggregator = CreateTagAggregator(textView);
var span = new SnapshotSpan(textView.TextSnapshot, 0, textView.TextSnapshot.Length);
var mappingSpan = textView.BufferGraph.CreateMappingSpan(span, SpanTrackingMode.EdgeExclusive);
var tags = tagAggregator.GetTags(mappingSpan)
.Select(x => new TagInfo
{
TextView = textView,
Point = Map(x.Span.Start, textView.TextSnapshot),
Tag = x.Tag as ShowInlineCommentTag,
})
.Where(x => x.Tag != null && x.Point.HasValue);
result.AddRange(tags);
}
result.Sort(TagInfoComparer.Instance);
return result;
}
/// <summary>
/// Shows the inline comments for the specified tag in a peek view.
/// </summary>
/// <param name="tag"></param>
protected void ShowPeekComments(
InlineCommentNavigationParams parameter,
ITextView textView,
ShowInlineCommentTag tag,
IEnumerable<ITextView> allTextViews)
{
var point = peekService.Show(textView, tag);
if (parameter?.MoveCursor != false)
{
var caretPoint = textView.BufferGraph.MapUpToSnapshot(
point.GetPoint(point.TextBuffer.CurrentSnapshot),
PointTrackingMode.Negative,
PositionAffinity.Successor,
textView.TextSnapshot);
if (caretPoint.HasValue)
{
(textView as FrameworkElement)?.Focus();
textView.Caret.MoveTo(caretPoint.Value);
}
}
foreach (var other in allTextViews)
{
if (other != textView)
{
peekService.Hide(other);
}
}
}
SnapshotPoint? Map(IMappingPoint p, ITextSnapshot textSnapshot)
{
return p.GetPoint(textSnapshot.TextBuffer, PositionAffinity.Predecessor);
}
protected interface ITagInfo
{
ITextView TextView { get; }
ShowInlineCommentTag Tag { get; }
SnapshotPoint Point { get; }
}
class TagInfo : ITagInfo
{
public ITextView TextView { get; set; }
public ShowInlineCommentTag Tag { get; set; }
public SnapshotPoint? Point { get; set; }
SnapshotPoint ITagInfo.Point => Point.Value;
}
class TagInfoComparer : IComparer<ITagInfo>
{
public static readonly TagInfoComparer Instance = new TagInfoComparer();
public int Compare(ITagInfo x, ITagInfo y) => x.Point.Position - y.Point.Position;
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
using System;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Supplies parameters to <see cref="INextInlineCommentCommand"/> and
/// <see cref="IPreviousInlineCommentCommand"/>.
/// </summary>
public class InlineCommentNavigationParams
{
/// <summary>
/// Gets or sets the line that should be used as the start point for navigation.
/// </summary>
/// <remarks>
/// If null, the current line will be used. If -1 then the absolute first or last
/// comment in the file will be navigated to.
/// </remarks>
public int? FromLine { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the cursor will be moved to the newly opened
/// comment.
/// </summary>
public bool MoveCursor { get; set; } = true;
}
}

Просмотреть файл

@ -0,0 +1,60 @@
using System;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading.Tasks;
using GitHub.InlineReviews.Services;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Navigates to and opens the the next inline comment thread in the currently active text view.
/// </summary>
[ExportCommand(typeof(InlineReviewsPackage))]
[Export(typeof(INextInlineCommentCommand))]
class NextInlineCommentCommand : InlineCommentNavigationCommand, INextInlineCommentCommand
{
/// <summary>
/// Gets the GUID of the group the command belongs to.
/// </summary>
public static readonly Guid CommandSet = GlobalCommands.CommandSetGuid;
/// <summary>
/// Gets the numeric identifier of the command.
/// </summary>
public const int CommandId = GlobalCommands.NextInlineCommentId;
/// <summary>
/// Initializes a new instance of the <see cref="NextInlineCommentCommand"/> class.
/// </summary>
/// <param name="tagAggregatorFactory">The tag aggregator factory.</param>
/// <param name="peekService">The peek service.</param>
[ImportingConstructor]
public NextInlineCommentCommand(
IViewTagAggregatorFactoryService tagAggregatorFactory,
IInlineCommentPeekService peekService)
: base(tagAggregatorFactory, peekService, CommandSet, CommandId)
{
}
/// <summary>
/// Executes the command.
/// </summary>
/// <returns>A task that tracks the execution of the command.</returns>
public override Task Execute(InlineCommentNavigationParams parameter)
{
var textViews = GetCurrentTextViews().ToList();
var tags = GetTags(textViews);
if (tags.Count > 0)
{
var cursorPoint = GetCursorPoint(textViews[0], parameter);
var next = tags.FirstOrDefault(x => x.Point > cursorPoint) ?? tags.First();
ShowPeekComments(parameter, next.TextView, next.Tag, textViews);
}
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using System;
using GitHub.Extensions;
using GitHub.Services;
using Microsoft.VisualStudio.Shell;
namespace GitHub.InlineReviews.Commands
{
static class PackageResources
{
/// <summary>
/// Registers the resources for a package.
/// </summary>
/// <typeparam name="TPackage">The type of the package.</typeparam>
/// <param name="package">The package.</param>
public static void Register<TPackage>(TPackage package) where TPackage : Package
{
var serviceProvider = package.GetServiceSafe<IGitHubServiceProvider>();
var commands = serviceProvider?.ExportProvider?.GetExports<IPackageResource, IExportCommandMetadata>();
if (commands != null)
{
foreach (var command in commands)
{
if (command.Metadata.PackageType == typeof(TPackage))
{
command.Value.Register(package);
}
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,59 @@
using System;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading.Tasks;
using GitHub.InlineReviews.Services;
using Microsoft.VisualStudio.Text.Tagging;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Navigates to and opens the the previous inline comment thread in the currently active text view.
/// </summary>
[ExportCommand(typeof(InlineReviewsPackage))]
[Export(typeof(IPreviousInlineCommentCommand))]
class PreviousInlineCommentCommand : InlineCommentNavigationCommand, IPreviousInlineCommentCommand
{
/// <summary>
/// Gets the GUID of the group the command belongs to.
/// </summary>
public static readonly Guid CommandSet = GlobalCommands.CommandSetGuid;
/// <summary>
/// Gets the numeric identifier of the command.
/// </summary>
public const int CommandId = GlobalCommands.PreviousInlineCommentId;
/// <summary>
/// Initializes a new instance of the <see cref="PreviousInlineCommentCommand"/> class.
/// </summary>
/// <param name="tagAggregatorFactory">The tag aggregator factory.</param>
/// <param name="peekService">The peek service.</param>
[ImportingConstructor]
public PreviousInlineCommentCommand(
IViewTagAggregatorFactoryService tagAggregatorFactory,
IInlineCommentPeekService peekService)
: base(tagAggregatorFactory, peekService, CommandSet, CommandId)
{
}
/// <summary>
/// Executes the command.
/// </summary>
/// <returns>A task that tracks the execution of the command.</returns>
public override Task Execute(InlineCommentNavigationParams parameter)
{
var textViews = GetCurrentTextViews().ToList();
var tags = GetTags(textViews);
if (tags.Count > 0)
{
var cursorPoint = GetCursorPoint(textViews[0], parameter);
var next = tags.LastOrDefault(x => x.Point < cursorPoint) ?? tags.Last();
ShowPeekComments(parameter, next.TextView, next.Tag, textViews);
}
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -0,0 +1,67 @@
using System;
using System.ComponentModel.Composition;
using System.Threading.Tasks;
using GitHub.Factories;
using GitHub.InlineReviews.Views;
using GitHub.Models;
using GitHub.Primitives;
using GitHub.Services;
using Microsoft.VisualStudio.Shell.Interop;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Shows the pull request comments view for a specified pull request.
/// </summary>
[ExportCommand(typeof(InlineReviewsPackage))]
class ShowPullRequestCommentsCommand : VsCommand<IPullRequestModel>
{
public static readonly Guid CommandSet = GlobalCommands.CommandSetGuid;
public const int CommandId = GlobalCommands.ShowPullRequestCommentsId;
readonly IApiClientFactory apiClientFactory;
readonly IPullRequestSessionManager sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="ShowPullRequestCommentsCommand"/> class.
/// </summary>
/// <param name="apiClientFactory">The API client factory.</param>
/// <param name="sessionManager">The pull request session manager.</param>
[ImportingConstructor]
public ShowPullRequestCommentsCommand(
IApiClientFactory apiClientFactory,
IPullRequestSessionManager sessionManager)
: base(CommandSet, CommandId)
{
this.apiClientFactory = apiClientFactory;
this.sessionManager = sessionManager;
}
/// <summary>
/// Executes the command.
/// </summary>
/// <param name="pullRequest">The pull request.</param>
/// <returns>A task that tracks the execution of the command.</returns>
public override async Task Execute(IPullRequestModel pullRequest)
{
if (pullRequest == null) return;
var package = (Microsoft.VisualStudio.Shell.Package)Package;
var window = (PullRequestCommentsPane)package.FindToolWindow(
typeof(PullRequestCommentsPane), pullRequest.Number, true);
if (window?.Frame == null)
{
throw new NotSupportedException("Cannot create Pull Request Comments tool window");
}
var session = await sessionManager.GetSession(pullRequest);
var address = HostAddress.Create(session.Repository.CloneUrl);
var apiClient = await apiClientFactory.Create(address);
await window.Initialize(session, apiClient);
var windowFrame = (IVsWindowFrame)window.Frame;
Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
}
}
}

Просмотреть файл

@ -0,0 +1,114 @@
using System;
using System.Windows.Input;
using GitHub.Extensions;
using Microsoft.VisualStudio.Shell;
using Task = System.Threading.Tasks.Task;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Wraps a Visual Studio <see cref="OleMenuCommand"/> for commands that don't accept a parameter.
/// </summary>
/// <remarks>
/// <para>
/// This class wraps an <see cref="OleMenuCommand"/> and also implements <see cref="ICommand"/>
/// so that the command can be bound to in the UI.
/// </para>
/// <para>
/// To implement a new command, inherit from this class and add an <see cref="ExportCommandAttribute"/>
/// to the class with the type of the package that the command is registered by. You can then override
/// the <see cref="Execute"/> method to provide the implementation of the command.
/// </para>
/// <para>
/// Commands are registered by a package on initialization by calling
/// <see cref="RegisterPackageCommands{TPackage}(TPackage)"/>.
/// </para>
/// </remarks>
abstract class VsCommand : VsCommandBase, IVsCommand
{
/// <summary>
/// Initializes a new instance of the <see cref="VsCommand"/> class.
/// </summary>
/// <param name="commandSet">The GUID of the group the command belongs to.</param>
/// <param name="commandId">The numeric identifier of the command.</param>
protected VsCommand(Guid commandSet, int commandId)
: base(commandSet, commandId)
{
}
/// <inheritdoc/>
public override void Register(IServiceProvider package)
{
var command = new OleMenuCommand(
(s, e) => Execute().Forget(),
(s, e) => { },
(s, e) => BeforeQueryStatus((OleMenuCommand)s),
VsCommandID);
Register(package, command);
}
/// <summary>
/// Overridden by derived classes with the implementation of the command.
/// </summary>
/// <returns>A task that tracks the execution of the command.</returns>
public abstract Task Execute();
/// <inheritdoc/>
protected override void ExecuteUntyped(object parameter)
{
Execute().Forget();
}
}
/// <summary>
/// Wraps a Visual Studio <see cref="OleMenuCommand"/> for commands that accept a parameter.
/// </summary>
/// <typeparam name="TParam">The type of the parameter accepted by the command.</typeparam>
/// <remarks>
/// <para>
/// To implement a new command, inherit from this class and add an <see cref="ExportCommandAttribute"/>
/// to the class with the type of the package that the command is registered by. You can then override
/// the <see cref="Execute"/> method to provide the implementation of the command.
/// </para>
/// <para>
/// Commands are registered by a package on initialization by calling
/// <see cref="RegisterPackageCommands{TPackage}(TPackage)"/>.
/// </para>
/// </remarks>
abstract class VsCommand<TParam> : VsCommandBase, IVsCommand<TParam>, ICommand
{
/// <summary>
/// Initializes a new instance of the <see cref="VsCommand"/> class.
/// </summary>
/// <param name="commandSet">The GUID of the group the command belongs to.</param>
/// <param name="commandId">The numeric identifier of the command.</param>
protected VsCommand(Guid commandSet, int commandId)
: base(commandSet, commandId)
{
}
/// <inheritdoc/>
public override void Register(IServiceProvider package)
{
var command = new OleMenuCommand(
(s, e) => Execute((TParam)((OleMenuCmdEventArgs)e).InValue).Forget(),
(s, e) => { },
(s, e) => BeforeQueryStatus((OleMenuCommand)s),
VsCommandID);
Register(package, command);
}
/// <summary>
/// Overridden by derived classes with the implementation of the command.
/// </summary>
/// /// <param name="parameter">The command parameter.</param>
/// <returns>A task that tracks the execution of the command.</returns>
public abstract Task Execute(TParam parameter);
/// <inheritdoc/>
protected override void ExecuteUntyped(object parameter)
{
Execute((TParam)parameter).Forget();
}
}
}

Просмотреть файл

@ -0,0 +1,97 @@
using System;
using System.ComponentModel.Design;
using System.Windows.Input;
using Microsoft.VisualStudio.Shell;
namespace GitHub.InlineReviews.Commands
{
/// <summary>
/// Base class for <see cref="VsCommand"/> and <see cref="VsCommand{TParam}"/>.
/// </summary>
abstract class VsCommandBase : IVsCommandBase
{
EventHandler canExecuteChanged;
/// <summary>
/// Initializes a new instance of the <see cref="VsCommandBase"/> class.
/// </summary>
/// <param name="commandSet">The GUID of the group the command belongs to.</param>
/// <param name="commandId">The numeric identifier of the command.</param>
protected VsCommandBase(Guid commandSet, int commandId)
{
VsCommandID = new CommandID(commandSet, commandId);
}
/// <summary>
/// Gets a value indicating whether the command is enabled.
/// </summary>
public virtual bool IsEnabled => true;
/// <summary>
/// Gets a value indicating whether the command is visible.
/// </summary>
public virtual bool IsVisible => true;
/// <inheritdoc/>
event EventHandler ICommand.CanExecuteChanged
{
add { canExecuteChanged += value; }
remove { canExecuteChanged -= value; }
}
/// <inheritdoc/>
bool ICommand.CanExecute(object parameter)
{
return IsEnabled && IsVisible;
}
/// <inheritdoc/>
void ICommand.Execute(object parameter)
{
ExecuteUntyped(parameter);
}
/// <inheritdoc/>
public abstract void Register(IServiceProvider package);
/// <summary>
/// Gets the package that registered the command.
/// </summary>
protected IServiceProvider Package { get; private set; }
/// <summary>
/// Gets the group and identifier for the command.
/// </summary>
protected CommandID VsCommandID { get; }
/// <summary>
/// Implements the event handler for <see cref="OleMenuCommand.BeforeQueryStatus"/>.
/// </summary>
/// <param name="command">The event parameter.</param>
protected void BeforeQueryStatus(OleMenuCommand command)
{
command.Enabled = IsEnabled;
command.Visible = IsVisible;
}
/// <summary>
/// When overridden in a derived class, executes the command after casting the passed
/// parameter to the correct type.
/// </summary>
/// <param name="parameter">The parameter</param>
protected abstract void ExecuteUntyped(object parameter);
/// <summary>
/// Registers an <see cref="OleMenuCommand"/> with a package.
/// </summary>
/// <param name="package">The package.</param>
/// <param name="command">The command.</param>
protected void Register(IServiceProvider package, OleMenuCommand command)
{
Package = package;
var serviceProvider = (IServiceProvider)package;
var mcs = (IMenuCommandService)serviceProvider.GetService(typeof(IMenuCommandService));
mcs?.AddCommand(command);
}
}
}

Просмотреть файл

@ -0,0 +1,441 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\LibGit2Sharp.NativeBinaries.1.0.129\build\LibGit2Sharp.NativeBinaries.props" Condition="Exists('..\..\packages\LibGit2Sharp.NativeBinaries.1.0.129\build\LibGit2Sharp.NativeBinaries.props')" />
<Import Project="..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.props" Condition="Exists('..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.props')" />
<PropertyGroup>
<!-- This is added to prevent forced migrations in Visual Studio 2012 and newer -->
<MinimumVisualStudioVersion Condition="'$(VisualStudioVersion)' != ''">$(VisualStudioVersion)</MinimumVisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<UseCodebase>true</UseCodebase>
<TargetFrameworkProfile />
<BuildType Condition="Exists('..\..\script\src\ApiClientConfiguration.cs')">Internal</BuildType>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>..\..\script\Key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<SchemaVersion>2.0</SchemaVersion>
<ProjectTypeGuids>{82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<ProjectGuid>{7F5ED78B-74A3-4406-A299-70CFB5885B8B}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GitHub.InlineReviews</RootNamespace>
<AssemblyName>GitHub.InlineReviews</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<GeneratePkgDefFile>true</GeneratePkgDefFile>
<IncludeAssemblyInVSIXContainer>true</IncludeAssemblyInVSIXContainer>
<IncludeDebugSymbolsInVSIXContainer>true</IncludeDebugSymbolsInVSIXContainer>
<IncludeDebugSymbolsInLocalVSIXDeployment>true</IncludeDebugSymbolsInLocalVSIXDeployment>
<CopyBuildOutputToOutputDirectory>true</CopyBuildOutputToOutputDirectory>
<CopyOutputSymbolsToOutputDirectory>true</CopyOutputSymbolsToOutputDirectory>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CreateVsixContainer>False</CreateVsixContainer>
<DeployExtension>False</DeployExtension>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\common\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="Commands\INextInlineCommentCommand.cs" />
<Compile Include="Commands\InlineCommentNavigationParams.cs" />
<Compile Include="Commands\IPreviousInlineCommentCommand.cs" />
<Compile Include="Commands\IVsCommandBase.cs" />
<Compile Include="Commands\IVsCommand.cs" />
<Compile Include="Commands\PackageResources.cs" />
<Compile Include="Commands\VsCommand.cs" />
<Compile Include="Commands\ExportCommandAttribute.cs" />
<Compile Include="Commands\IPackageResource.cs" />
<Compile Include="Commands\IExportCommandMetadata.cs" />
<Compile Include="Commands\InlineCommentNavigationCommand.cs" />
<Compile Include="Commands\PreviousInlineCommentCommand.cs" />
<Compile Include="Commands\NextInlineCommentCommand.cs" />
<Compile Include="Commands\ShowPullRequestCommentsCommand.cs" />
<Compile Include="Commands\VsCommandBase.cs" />
<Compile Include="Glyph\GlyphData.cs" />
<Compile Include="Glyph\GlyphMargin.cs" />
<Compile Include="Glyph\GlyphMarginVisualManager.cs" />
<Compile Include="Glyph\IGlyphFactory.cs" />
<Compile Include="InlineReviewsPackage.cs" />
<Compile Include="Models\InlineCommentThreadModel.cs" />
<Compile Include="Models\PullRequestSessionFile.cs" />
<Compile Include="Peek\InlineCommentPeekableItem.cs" />
<Compile Include="Peek\InlineCommentPeekableItemSource.cs" />
<Compile Include="Peek\InlineCommentPeekableItemSourceProvider.cs" />
<Compile Include="Peek\InlineCommentPeekableResultSource.cs" />
<Compile Include="Peek\InlineCommentPeekRelationship.cs" />
<Compile Include="Peek\InlineCommentPeekResult.cs" />
<Compile Include="Peek\InlineCommentPeekResultPresentation.cs" />
<Compile Include="Peek\InlineCommentPeekResultPresenter.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SampleData\CommentThreadViewModelDesigner.cs" />
<Compile Include="SampleData\DiffCommentThreadViewModelDesigner.cs" />
<Compile Include="SampleData\PullRequestCommentsViewModelDesigner.cs" />
<Compile Include="Services\IInlineCommentPeekService.cs" />
<Compile Include="Services\IPullRequestSessionService.cs" />
<Compile Include="Services\InlineCommentPeekService.cs" />
<Compile Include="Services\PullRequestSession.cs" />
<Compile Include="Services\PullRequestSessionManager.cs" />
<Compile Include="InlineCommentMarginProvider.cs" />
<Compile Include="Services\PullRequestSessionService.cs" />
<Compile Include="ViewModels\CommentViewModel.cs" />
<Compile Include="ViewModels\DiffCommentThreadViewModel.cs" />
<Compile Include="ViewModels\ICommentThreadViewModel.cs" />
<Compile Include="ViewModels\CommentThreadViewModel.cs" />
<Compile Include="ViewModels\IDiffCommentThreadViewModel.cs" />
<Compile Include="ViewModels\IInlineCommentViewModel.cs" />
<Compile Include="ViewModels\InlineCommentPeekViewModel.cs" />
<Compile Include="ViewModels\NewInlineCommentThreadViewModel.cs" />
<Compile Include="ViewModels\InlineCommentViewModel.cs" />
<Compile Include="ViewModels\IPullRequestCommentsViewModel.cs" />
<Compile Include="ViewModels\IssueCommentThreadViewModel.cs" />
<Compile Include="ViewModels\PullRequestCommentsViewModel.cs" />
<Compile Include="Views\DiffCommentThreadView.xaml.cs">
<DependentUpon>DiffCommentThreadView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\DiffView.cs" />
<Compile Include="Views\GlyphMarginGrid.xaml.cs">
<DependentUpon>GlyphMarginGrid.xaml</DependentUpon>
</Compile>
<Compile Include="Views\InlineCommentPeekView.xaml.cs">
<DependentUpon>InlineCommentPeekView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PullRequestCommentsPane.cs" />
<Compile Include="Views\PullRequestCommentsView.xaml.cs">
<DependentUpon>PullRequestCommentsView.xaml</DependentUpon>
</Compile>
<Compile Include="SampleData\CommentViewModelDesigner.cs" />
<Compile Include="Services\DiffService.cs" />
<Compile Include="Services\IDiffService.cs" />
<Compile Include="Tags\AddInlineCommentTag.cs" />
<Compile Include="Tags\AddInlineCommentGlyph.xaml.cs">
<DependentUpon>AddInlineCommentGlyph.xaml</DependentUpon>
</Compile>
<Compile Include="Tags\ShowInlineCommentGlyph.xaml.cs">
<DependentUpon>ShowInlineCommentGlyph.xaml</DependentUpon>
</Compile>
<Compile Include="Tags\InlineCommentGlyphFactory.cs" />
<Compile Include="Tags\InlineCommentTag.cs" />
<Compile Include="Tags\ShowInlineCommentTag.cs" />
<Compile Include="Tags\InlineCommentTagger.cs" />
<Compile Include="Tags\InlineCommentTaggerProvider.cs" />
<Compile Include="ViewModels\InlineCommentThreadViewModel.cs" />
<Compile Include="ViewModels\ICommentViewModel.cs" />
<Compile Include="Views\CommentThreadView.xaml.cs">
<DependentUpon>CommentThreadView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\CommentView.xaml.cs">
<DependentUpon>CommentView.xaml</DependentUpon>
</Compile>
<Compile Include="VisualStudioExtensions.cs" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\script\Key.snk" Condition="$(Buildtype) == 'Internal'">
<Link>Key.snk</Link>
</None>
<None Include="app.config" />
<None Include="packages.config" />
<None Include="source.extension.vsixmanifest">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\submodules\octokit.net\Octokit\Octokit.csproj">
<Project>{08dd4305-7787-4823-a53f-4d0f725a07f3}</Project>
<Name>Octokit</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\reactiveui\ReactiveUI\ReactiveUI_Net45.csproj">
<Project>{1CE2D235-8072-4649-BA5A-CFB1AF8776E0}</Project>
<Name>ReactiveUI_Net45</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}</Project>
<Name>Rothko</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\splat\Splat\Splat-Net45.csproj">
<Project>{252ce1c2-027a-4445-a3c2-e4d6c80a935a}</Project>
<Name>Splat-Net45</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.App\GitHub.App.csproj">
<Project>{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}</Project>
<Name>GitHub.App</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Exports.Reactive\GitHub.Exports.Reactive.csproj">
<Project>{e4ed0537-d1d9-44b6-9212-3096d7c3f7a1}</Project>
<Name>GitHub.Exports.Reactive</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Exports\GitHub.Exports.csproj">
<Project>{9aea02db-02b5-409c-b0ca-115d05331a6b}</Project>
<Name>GitHub.Exports</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
<Project>{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}</Project>
<Name>GitHub.Extensions</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.UI\GitHub.UI.csproj">
<Project>{346384dd-2445-4a28-af22-b45f3957bd89}</Project>
<Name>GitHub.UI</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.VisualStudio.UI\GitHub.VisualStudio.UI.csproj">
<Project>{d1dfbb0c-b570-4302-8f1e-2e3a19c41961}</Project>
<Name>GitHub.VisualStudio.UI</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Reference Include="EnvDTE, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="EnvDTE100, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="EnvDTE80, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="EnvDTE90, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="LibGit2Sharp, Version=0.22.0.0, Culture=neutral, PublicKeyToken=7cbde695407f0333, processorArchitecture=MSIL">
<HintPath>..\..\packages\LibGit2Sharp.0.22.0\lib\net40\LibGit2Sharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Build.Framework" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.VisualStudio.CommandBars, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\VSSDK.ComponentModelHost.12.0.4\lib\net45\Microsoft.VisualStudio.ComponentModelHost.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.CoreUtility, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.CoreUtility.14.3.25407\lib\net45\Microsoft.VisualStudio.CoreUtility.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Editor, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Editor.14.3.25407\lib\net45\Microsoft.VisualStudio.Editor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Imaging, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Imaging.14.3.25407\lib\net45\Microsoft.VisualStudio.Imaging.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Language.Intellisense, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Language.Intellisense.14.3.25407\lib\net45\Microsoft.VisualStudio.Language.Intellisense.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.OLE.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6070\lib\Microsoft.VisualStudio.OLE.Interop.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.14.0, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.14.0.14.3.25407\lib\Microsoft.VisualStudio.Shell.14.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.10.0, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Immutable.10.0.10.0.30319\lib\net40\Microsoft.VisualStudio.Shell.Immutable.10.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.11.0, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Immutable.11.0.11.0.50727\lib\net45\Microsoft.VisualStudio.Shell.Immutable.11.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Immutable.12.0.12.0.21003\lib\net45\Microsoft.VisualStudio.Shell.Immutable.12.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.14.0, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Immutable.14.0.14.3.25407\lib\net45\Microsoft.VisualStudio.Shell.Immutable.14.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Interop.10.0, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<EmbedInteropTypes>True</EmbedInteropTypes>
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Interop.10.0.10.0.30319\lib\Microsoft.VisualStudio.Shell.Interop.10.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Interop.11.0, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<EmbedInteropTypes>True</EmbedInteropTypes>
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Interop.11.0.11.0.61030\lib\Microsoft.VisualStudio.Shell.Interop.11.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Interop.12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<EmbedInteropTypes>True</EmbedInteropTypes>
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Interop.12.0.12.0.30110\lib\Microsoft.VisualStudio.Shell.Interop.12.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Interop.8.0, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Interop.9.0, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\Microsoft.VisualStudio.Shell.Interop.9.0.9.0.30729\lib\Microsoft.VisualStudio.Shell.Interop.9.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Text.Data, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Text.Data.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Data.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Text.Logic, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Text.Logic.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Logic.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Text.UI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Text.UI.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Text.UI.Wpf, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Text.UI.Wpf.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.Wpf.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TextManager.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TextManager.Interop.8.0, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Threading, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Threading.14.1.111\lib\net45\Microsoft.VisualStudio.Threading.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Utilities, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Validation, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.VisualStudio.Validation.14.1.111\lib\net45\Microsoft.VisualStudio.Validation.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="stdole, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Data" />
<Reference Include="System.Design" />
<Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Numerics" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=62aa029873c516b4, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Core.2.2.5-custom\lib\net45\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=62aa029873c516b4, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5-custom\lib\net45\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=62aa029873c516b4, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Linq.2.2.5-custom\lib\net45\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=62aa029873c516b4, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5-custom\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<VSCTCompile Include="InlineReviewsPackage.vsct">
<ResourceName>Menus.ctmenu</ResourceName>
<SubType>Designer</SubType>
</VSCTCompile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="VSPackage.resx">
<MergeWithCTO>true</MergeWithCTO>
<ManifestResourceName>VSPackage</ManifestResourceName>
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Page Include="Views\DiffCommentThreadView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\GlyphMarginGrid.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\InlineCommentPeekView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PullRequestCommentsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Tags\AddInlineCommentGlyph.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Tags\ShowInlineCommentGlyph.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\CommentThreadView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\CommentView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Content Include="Resources\logo_32x32%402x.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets" Condition="'$(VSToolsPath)' != '' And '$(NCrunch)' != '1'" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\packages\LibGit2Sharp.NativeBinaries.1.0.129\build\LibGit2Sharp.NativeBinaries.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\LibGit2Sharp.NativeBinaries.1.0.129\build\LibGit2Sharp.NativeBinaries.props'))" />
</Target>
<Import Project="..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.targets" Condition="Exists('..\..\packages\Microsoft.VSSDK.BuildTools.14.3.25407\build\Microsoft.VSSDK.BuildTools.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Просмотреть файл

@ -0,0 +1,42 @@
using System;
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualStudio.Text;
namespace GitHub.InlineReviews.Glyph.Implementation
{
internal class GlyphData<TGlyphTag>
{
double deltaY;
public GlyphData(SnapshotSpan visualSpan, TGlyphTag tag, UIElement element)
{
VisualSpan = visualSpan;
GlyphType = tag.GetType();
Glyph = element;
deltaY = Canvas.GetTop(element);
if (double.IsNaN(deltaY))
{
deltaY = 0.0;
}
}
public void SetSnapshot(ITextSnapshot snapshot)
{
VisualSpan = VisualSpan.Value.TranslateTo(snapshot, SpanTrackingMode.EdgeInclusive);
}
public void SetTop(double top)
{
Canvas.SetTop(Glyph, top + deltaY);
}
public UIElement Glyph { get; }
public Type GlyphType { get; }
public SnapshotSpan? VisualSpan { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,208 @@
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using System.Collections.Generic;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Text.Formatting;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using GitHub.InlineReviews.Glyph.Implementation;
namespace GitHub.InlineReviews.Glyph
{
public sealed class GlyphMargin<TGlyphTag> : IWpfTextViewMargin, ITextViewMargin, IDisposable where TGlyphTag: ITag
{
bool handleZoom;
bool isDisposed;
Grid marginVisual;
double marginWidth;
bool refreshAllGlyphs;
ITagAggregator<TGlyphTag> tagAggregator;
IWpfTextView textView;
string marginName;
GlyphMarginVisualManager<TGlyphTag> visualManager;
Func<ITextBuffer, bool> isMarginVisible;
public GlyphMargin(
IWpfTextViewHost wpfTextViewHost,
IGlyphFactory<TGlyphTag> glyphFactory,
Func<Grid> gridFactory,
ITagAggregator<TGlyphTag> tagAggregator,
IEditorFormatMap editorFormatMap,
Func<ITextBuffer, bool> isMarginVisible,
string marginPropertiesName, string marginName, bool handleZoom = true, double marginWidth = 17.0)
{
textView = wpfTextViewHost.TextView;
this.tagAggregator = tagAggregator;
this.isMarginVisible = isMarginVisible;
this.marginName = marginName;
this.handleZoom = handleZoom;
this.marginWidth = marginWidth;
marginVisual = gridFactory();
marginVisual.Width = marginWidth;
visualManager = new GlyphMarginVisualManager<TGlyphTag>(textView, glyphFactory, marginVisual,
this, editorFormatMap, marginPropertiesName);
// Do on Loaded to give diff view a chance to initialize.
marginVisual.Loaded += OnLoaded;
}
public void Dispose()
{
if (!isDisposed)
{
tagAggregator.Dispose();
marginVisual = null;
isDisposed = true;
}
}
public ITextViewMargin GetTextViewMargin(string marginName)
{
return (marginName == this.marginName) ? this : null;
}
public bool Enabled
{
get
{
ThrowIfDisposed();
return true;
}
}
public double MarginSize
{
get
{
ThrowIfDisposed();
return marginVisual.Width;
}
}
public FrameworkElement VisualElement
{
get
{
ThrowIfDisposed();
return marginVisual;
}
}
void OnLoaded(object sender, RoutedEventArgs e)
{
RefreshMarginVisibility();
tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged;
textView.LayoutChanged += OnLayoutChanged;
if (handleZoom)
{
textView.ZoomLevelChanged += OnZoomLevelChanged;
}
if (textView.InLayout)
{
refreshAllGlyphs = true;
}
else
{
foreach (var line in textView.TextViewLines)
{
RefreshGlyphsOver(line);
}
}
if (handleZoom)
{
marginVisual.LayoutTransform = new ScaleTransform(textView.ZoomLevel / 100.0, textView.ZoomLevel / 100.0);
marginVisual.LayoutTransform.Freeze();
}
}
void OnBatchedTagsChanged(object sender, BatchedTagsChangedEventArgs e)
{
RefreshMarginVisibility();
if (!textView.IsClosed)
{
var list = new List<SnapshotSpan>();
foreach (var span in e.Spans)
{
list.AddRange(span.GetSpans(textView.TextSnapshot));
}
if (list.Count > 0)
{
var span = list[0];
int start = span.Start;
int end = span.End;
for (int i = 1; i < list.Count; i++)
{
span = list[i];
start = Math.Min(start, span.Start);
end = Math.Max(end, span.End);
}
var rangeSpan = new SnapshotSpan(textView.TextSnapshot, start, end - start);
visualManager.RemoveGlyphsByVisualSpan(rangeSpan);
foreach (var line in textView.TextViewLines.GetTextViewLinesIntersectingSpan(rangeSpan))
{
RefreshGlyphsOver(line);
}
}
}
}
void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
RefreshMarginVisibility();
visualManager.SetSnapshotAndUpdate(textView.TextSnapshot, e.NewOrReformattedLines, e.VerticalTranslation ? (IList<ITextViewLine>)textView.TextViewLines : e.TranslatedLines);
var lines = refreshAllGlyphs ? (IList<ITextViewLine>)textView.TextViewLines : e.NewOrReformattedLines;
foreach (var line in lines)
{
visualManager.RemoveGlyphsByVisualSpan(line.Extent);
RefreshGlyphsOver(line);
}
refreshAllGlyphs = false;
}
void OnZoomLevelChanged(object sender, ZoomLevelChangedEventArgs e)
{
refreshAllGlyphs = true;
marginVisual.LayoutTransform = e.ZoomTransform;
}
void RefreshGlyphsOver(ITextViewLine textViewLine)
{
foreach (IMappingTagSpan<TGlyphTag> span in tagAggregator.GetTags(textViewLine.ExtentAsMappingSpan))
{
NormalizedSnapshotSpanCollection spans;
if (span.Span.Start.GetPoint(textView.VisualSnapshot.TextBuffer, PositionAffinity.Predecessor).HasValue &&
((spans = span.Span.GetSpans(textView.TextSnapshot)).Count > 0))
{
visualManager.AddGlyph(span.Tag, spans[0]);
}
}
}
void RefreshMarginVisibility()
{
marginVisual.Visibility = isMarginVisible(textView.TextBuffer) ? Visibility.Visible : Visibility.Collapsed;
}
void ThrowIfDisposed()
{
if (isDisposed)
{
throw new ObjectDisposedException(marginName);
}
}
}
}

Просмотреть файл

@ -0,0 +1,196 @@
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using System.Collections.Generic;
using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Text.Formatting;
using Microsoft.VisualStudio.Text.Classification;
namespace GitHub.InlineReviews.Glyph.Implementation
{
internal class GlyphMarginVisualManager<TGlyphTag> where TGlyphTag: ITag
{
readonly IEditorFormatMap editorFormatMap;
readonly IGlyphFactory<TGlyphTag> glyphFactory;
readonly Grid glyphMarginGrid;
readonly IWpfTextViewMargin margin;
readonly string marginPropertiesName;
readonly IWpfTextView textView;
readonly Dictionary<Type, Canvas> visuals;
Dictionary<UIElement, GlyphData<TGlyphTag>> glyphs;
public GlyphMarginVisualManager(IWpfTextView textView, IGlyphFactory<TGlyphTag> glyphFactory, Grid glyphMarginGrid,
IWpfTextViewMargin margin, IEditorFormatMap editorFormatMap, string marginPropertiesName)
{
this.textView = textView;
this.margin = margin;
this.marginPropertiesName = marginPropertiesName;
this.editorFormatMap = editorFormatMap;
this.editorFormatMap.FormatMappingChanged += OnFormatMappingChanged;
this.textView.Closed += new EventHandler(OnTextViewClosed);
this.glyphFactory = glyphFactory;
this.glyphMarginGrid = glyphMarginGrid;
UpdateBackgroundColor();
glyphs = new Dictionary<UIElement, GlyphData<TGlyphTag>>();
visuals = new Dictionary<Type, Canvas>();
foreach (Type type in glyphFactory.GetTagTypes())
{
if (!visuals.ContainsKey(type))
{
var element = new Canvas();
element.ClipToBounds = true;
glyphMarginGrid.Children.Add(element);
visuals[type] = element;
}
}
}
public FrameworkElement MarginVisual => glyphMarginGrid;
public void AddGlyph(TGlyphTag tag, SnapshotSpan span)
{
var textViewLines = textView.TextViewLines;
var glyphType = tag.GetType();
if (textView.TextViewLines.IntersectsBufferSpan(span))
{
var startingLine = GetStartingLine(textViewLines, span) as IWpfTextViewLine;
if (startingLine != null)
{
var element = (FrameworkElement)glyphFactory.GenerateGlyph(startingLine, tag);
if (element != null)
{
var data = new GlyphData<TGlyphTag>(span, tag, element);
element.Width = glyphMarginGrid.Width;
// draw where text is
element.Height = startingLine.TextHeight + 1; // HACK: +1 to fill gaps
data.SetTop(startingLine.TextTop - textView.ViewportTop);
glyphs[element] = data;
visuals[glyphType].Children.Add(element);
}
}
}
}
public void RemoveGlyphsByVisualSpan(SnapshotSpan span)
{
var list = new List<UIElement>();
foreach (var pair in glyphs)
{
var data = pair.Value;
if (data.VisualSpan.HasValue && span.IntersectsWith(data.VisualSpan.Value))
{
list.Add(pair.Key);
visuals[data.GlyphType].Children.Remove(data.Glyph);
}
}
foreach (var element in list)
{
glyphs.Remove(element);
}
}
public void SetSnapshotAndUpdate(ITextSnapshot snapshot, IList<ITextViewLine> newOrReformattedLines, IList<ITextViewLine> translatedLines)
{
if (glyphs.Count > 0)
{
var dictionary = new Dictionary<UIElement, GlyphData<TGlyphTag>>(glyphs.Count);
foreach (var pair in glyphs)
{
var data = pair.Value;
if (!data.VisualSpan.HasValue)
{
dictionary[pair.Key] = data;
continue;
}
data.SetSnapshot(snapshot);
SnapshotSpan bufferSpan = data.VisualSpan.Value;
if (!textView.TextViewLines.IntersectsBufferSpan(bufferSpan) || GetStartingLine(newOrReformattedLines, bufferSpan) != null)
{
visuals[data.GlyphType].Children.Remove(data.Glyph);
continue;
}
dictionary[data.Glyph] = data;
var startingLine = GetStartingLine(translatedLines, bufferSpan);
if (startingLine != null)
{
data.SetTop(startingLine.TextTop - textView.ViewportTop);
}
}
glyphs = dictionary;
}
}
static ITextViewLine GetStartingLine(IList<ITextViewLine> lines, Span span)
{
if (lines.Count > 0)
{
int num = 0;
int count = lines.Count;
while (num < count)
{
int middle = (num + count) / 2;
var middleLine = lines[middle];
if (span.Start < middleLine.Start)
{
count = middle;
}
else
{
if (span.Start >= middleLine.EndIncludingLineBreak)
{
num = middle + 1;
continue;
}
return middleLine;
}
}
var line = lines[lines.Count - 1];
if (line.EndIncludingLineBreak == line.Snapshot.Length && span.Start == line.EndIncludingLineBreak)
{
return line;
}
}
return null;
}
void OnFormatMappingChanged(object sender, FormatItemsEventArgs e)
{
if (e.ChangedItems.Contains(marginPropertiesName))
{
UpdateBackgroundColor();
}
}
void OnTextViewClosed(object sender, EventArgs e)
{
editorFormatMap.FormatMappingChanged -= OnFormatMappingChanged;
}
void UpdateBackgroundColor()
{
// set background color for children
var properties = editorFormatMap.GetProperties(marginPropertiesName);
if (properties.Contains("BackgroundColor"))
{
var backgroundColor = (Color)properties["BackgroundColor"];
ImageThemingUtilities.SetImageBackgroundColor(glyphMarginGrid, backgroundColor);
}
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
using System;
using System.Windows;
using System.Collections.Generic;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Text.Formatting;
namespace GitHub.InlineReviews.Glyph
{
public interface IGlyphFactory<TGlyphTag> where TGlyphTag : ITag
{
UIElement GenerateGlyph(IWpfTextViewLine line, TGlyphTag tag);
IEnumerable<Type> GetTagTypes();
}
}

Просмотреть файл

@ -0,0 +1,84 @@
using System;
using System.Windows.Controls;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text;
using GitHub.InlineReviews.Tags;
using GitHub.InlineReviews.Glyph;
using GitHub.InlineReviews.Services;
using GitHub.InlineReviews.Views;
using GitHub.Models;
using GitHub.Services;
namespace GitHub.InlineReviews
{
[Export(typeof(IWpfTextViewMarginProvider))]
[Name(MarginName)]
[Order(After = PredefinedMarginNames.Glyph)]
[MarginContainer(PredefinedMarginNames.Left)]
[ContentType("text")]
[TextViewRole(PredefinedTextViewRoles.Interactive)]
internal sealed class InlineCommentMarginProvider : IWpfTextViewMarginProvider
{
const string MarginName = "InlineComment";
const string MarginPropertiesName = "Indicator Margin"; // Same background color as Glyph margin
readonly IEditorFormatMapService editorFormatMapService;
readonly IViewTagAggregatorFactoryService tagAggregatorFactory;
readonly IInlineCommentPeekService peekService;
readonly IPullRequestSessionManager sessionManager;
[ImportingConstructor]
public InlineCommentMarginProvider(
IEditorFormatMapService editorFormatMapService,
IViewTagAggregatorFactoryService tagAggregatorFactory,
IInlineCommentPeekService peekService,
IPullRequestSessionManager sessionManager)
{
this.editorFormatMapService = editorFormatMapService;
this.tagAggregatorFactory = tagAggregatorFactory;
this.peekService = peekService;
this.sessionManager = sessionManager;
}
public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin parent)
{
var textView = wpfTextViewHost.TextView;
var editorFormatMap = editorFormatMapService.GetEditorFormatMap(textView);
var glyphFactory = new InlineCommentGlyphFactory(peekService, textView, editorFormatMap);
Func<Grid> gridFactory = () => new GlyphMarginGrid();
return CreateMargin(glyphFactory, gridFactory, wpfTextViewHost, parent, editorFormatMap);
}
IWpfTextViewMargin CreateMargin<TGlyphTag>(IGlyphFactory<TGlyphTag> glyphFactory, Func<Grid> gridFactory,
IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin parent, IEditorFormatMap editorFormatMap) where TGlyphTag : ITag
{
var tagAggregator = tagAggregatorFactory.CreateTagAggregator<TGlyphTag>(wpfTextViewHost.TextView);
return new GlyphMargin<TGlyphTag>(wpfTextViewHost, glyphFactory, gridFactory, tagAggregator, editorFormatMap,
IsMarginVisible, MarginPropertiesName, MarginName, true, 17.0);
}
bool IsMarginVisible(ITextBuffer buffer)
{
if (sessionManager.GetTextBufferInfo(buffer) != null)
{
return true;
}
InlineCommentTagger inlineCommentTagger;
if (buffer.Properties.TryGetProperty(typeof(InlineCommentTagger), out inlineCommentTagger))
{
if (inlineCommentTagger.ShowMargin)
{
return true;
}
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
using System;
using System.Runtime.InteropServices;
using GitHub.InlineReviews.Commands;
using GitHub.InlineReviews.Views;
using GitHub.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
namespace GitHub.InlineReviews
{
[PackageRegistration(UseManagedResourcesOnly = true)]
[Guid(Guids.InlineReviewsPackageId)]
[ProvideAutoLoad(UIContextGuids80.SolutionExists)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[ProvideToolWindow(typeof(PullRequestCommentsPane), DocumentLikeTool=true)]
public class InlineReviewsPackage : Package
{
protected override void Initialize()
{
base.Initialize();
PackageResources.Register(this);
}
}
}

Просмотреть файл

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This is the file that defines the actual layout and type of the commands.
It is divided in different sections (e.g. command definition, command
placement, ...), with each defining a specific set of properties.
See the comment before each section for more details about how to
use it. -->
<!-- The VSCT compiler (the tool that translates this file into the binary
format that VisualStudio will consume) has the ability to run a preprocessor
on the vsct file; this preprocessor is (usually) the C++ preprocessor, so
it is possible to define includes and macros with the same syntax used
in C++ files. Using this ability of the compiler here, we include some files
defining some of the constants that we will use inside the file. -->
<!--This is the file that defines the IDs for all the commands exposed by VisualStudio. -->
<Extern href="stdidcmd.h"/>
<!--This header contains the command ids for the menus provided by the shell. -->
<Extern href="vsshlids.h"/>
<!--The Commands section is where commands, menus, and menu groups are defined.
This section uses a Guid to identify the package that provides the command defined inside it. -->
<Commands package="guidInlineReviewsPackage">
<!-- Inside this section we have different sub-sections: one for the menus, another
for the menu groups, one for the buttons (the actual commands), one for the combos
and the last one for the bitmaps used. Each element is identified by a command id that
is a unique pair of guid and numeric identifier; the guid part of the identifier is usually
called "command set" and is used to group different command inside a logically related
group; your package should define its own command set in order to avoid collisions
with command ids defined by other packages. -->
<!--Buttons section. -->
<!--This section defines the elements the user can interact with, like a menu command or a button
or combo box in a toolbar. -->
<Buttons>
<!--To define a menu group you have to specify its ID, the parent menu and its display priority.
The command is visible and enabled by default. If you need to change the visibility, status, etc, you can use
the CommandFlag node.
You can add more than one CommandFlag node e.g.:
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
If you do not want an image next to your command, remove the Icon node /> -->
<Button guid="guidGitHubCommandSet" id="PullRequestCommentsToolWindowCommandId" priority="0x0100" type="Button">
<Strings>
<CommandName>GitHub.InlineReviews.ShowPullRequestComments</CommandName>
<ButtonText>Pull Request Comments</ButtonText>
</Strings>
</Button>
<Button guid="guidGitHubCommandSet" id="NextInlineCommentId" priority="0x0100" type="Button">
<Parent guid="guidSHLMainMenu" id="IDG_VS_EDIT_GOTO" />
<CommandFlag>DefaultDisabled</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<CommandName>GitHub.InlineReviews.NextInlineComment</CommandName>
<ButtonText>Next Comment</ButtonText>
</Strings>
</Button>
<Button guid="guidGitHubCommandSet" id="PreviousInlineCommentId" priority="0x0100" type="Button">
<Parent guid="guidSHLMainMenu" id="IDG_VS_EDIT_GOTO" />
<CommandFlag>DefaultDisabled</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<CommandName>GitHub.InlineReviews.PreviousInlineComment</CommandName>
<ButtonText>Previous Comment</ButtonText>
</Strings>
</Button>
</Buttons>
</Commands>
<KeyBindings>
<KeyBinding guid="guidGitHubCommandSet" id="NextInlineCommentId" editor="guidVSStd97" key1="VK_OEM_6" mod1="Alt"/>
<KeyBinding guid="guidGitHubCommandSet" id="PreviousInlineCommentId" editor="guidVSStd97" key1="VK_OEM_4" mod1="Alt"/>
</KeyBindings>
<Symbols>
<!-- This is the package guid. -->
<GuidSymbol name="guidInlineReviewsPackage" value="{248325be-4a2d-4111-b122-e7d59bf73a35}" />
<!-- This is the guid used to group the menu commands together -->
<GuidSymbol name="guidGitHubCommandSet" value="{C5F1193E-F300-41B3-B4C4-5A703DD3C1C6}">
<IDSymbol name="PullRequestCommentsToolWindowCommandId" value="0x1000" />
<IDSymbol name="NextInlineCommentId" value="0x1001" />
<IDSymbol name="PreviousInlineCommentId" value="0x1002" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{775aa523-6c52-4c11-9c28-823c99d15613}" >
<IDSymbol name="bmpPic1" value="1" />
<IDSymbol name="bmpPic2" value="2" />
<IDSymbol name="bmpPicSearch" value="3" />
<IDSymbol name="bmpPicX" value="4" />
<IDSymbol name="bmpPicArrows" value="5" />
<IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>
</Symbols>
</CommandTable>

Просмотреть файл

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Extensions;
using GitHub.Models;
using ReactiveUI;
namespace GitHub.InlineReviews.Models
{
/// <summary>
/// Represents a thread of inline comments on an <see cref="IPullRequestSessionFile"/>.
/// </summary>
class InlineCommentThreadModel : ReactiveObject, IInlineCommentThreadModel
{
bool isStale;
int lineNumber;
/// <summary>
/// Initializes a new instance of the <see cref="InlineCommentThreadModel"/> class.
/// </summary>
/// <param name="relativePath">The relative path to the file that the thread is on.</param>
/// <param name="originalCommitSha">The SHA of the commit that the thread was left con.</param>
/// <param name="originalPosition">
/// The 1-based line number in the original diff that the thread was left on.
/// </param>
/// <param name="diffMatch">
/// The last five lines of the thread's diff hunk, in reverse order.
/// </param>
public InlineCommentThreadModel(
string relativePath,
string originalCommitSha,
int originalPosition,
IList<DiffLine> diffMatch,
IEnumerable<IPullRequestReviewCommentModel> comments)
{
Guard.ArgumentNotNull(originalCommitSha, nameof(originalCommitSha));
Guard.ArgumentNotNull(diffMatch, nameof(diffMatch));
Comments = comments.ToList();
DiffMatch = diffMatch;
OriginalCommitSha = originalCommitSha;
OriginalPosition = originalPosition;
RelativePath = relativePath;
}
/// <inheritdoc/>
public IReadOnlyList<IPullRequestReviewCommentModel> Comments { get; }
/// <inheritdoc/>
public IList<DiffLine> DiffMatch { get; }
/// <inheritdoc/>
public DiffChangeType DiffLineType => DiffMatch.First().Type;
/// <inheritdoc/>
public bool IsStale
{
get { return isStale; }
set { this.RaiseAndSetIfChanged(ref isStale, value); }
}
/// <inheritdoc/>
public int LineNumber
{
get { return lineNumber; }
set { this.RaiseAndSetIfChanged(ref lineNumber, value); }
}
/// <inheritdoc/>
public string OriginalCommitSha { get; }
/// <inheritdoc/>
public int OriginalPosition { get; }
/// <inheritdoc/>
public string RelativePath { get; }
}
}

Просмотреть файл

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using GitHub.Models;
using ReactiveUI;
using GitHub.InlineReviews.Services;
namespace GitHub.InlineReviews.Models
{
/// <summary>
/// A file in a pull request session.
/// </summary>
/// <remarks>
/// A pull request session file represents the real-time state of a file in a pull request in
/// the IDE. If the pull request branch is checked out, it represents the state of a file from
/// the pull request model updated to the current state of the code on disk and in the editor.
/// </remarks>
/// <seealso cref="PullRequestSession"/>
/// <seealso cref="PullRequestSessionManager"/>
class PullRequestSessionFile : ReactiveObject, IPullRequestSessionFile
{
IList<DiffChunk> diff;
string commitSha;
IReadOnlyList<IInlineCommentThreadModel> inlineCommentThreads;
/// <summary>
/// Initializes a new instance of the <see cref="PullRequestSessionFile"/> class.
/// </summary>
/// <param name="relativePath">
/// The relative path to the file in the repository.
/// </param>
public PullRequestSessionFile(string relativePath)
{
RelativePath = relativePath;
}
/// <inheritdoc/>
public string RelativePath { get; }
/// <inheritdoc/>
public IList<DiffChunk> Diff
{
get { return diff; }
internal set { this.RaiseAndSetIfChanged(ref diff, value); }
}
/// <inheritdoc/>
public string BaseSha { get; internal set; }
/// <inheritdoc/>
public string CommitSha
{
get { return commitSha; }
internal set { this.RaiseAndSetIfChanged(ref commitSha, value); }
}
/// <inheritdoc/>
public IEditorContentSource ContentSource { get; internal set; }
/// <inheritdoc/>
public IReadOnlyList<IInlineCommentThreadModel> InlineCommentThreads
{
get { return inlineCommentThreads; }
internal set { this.RaiseAndSetIfChanged(ref inlineCommentThreads, value); }
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using System;
using Microsoft.VisualStudio.Language.Intellisense;
namespace GitHub.InlineReviews.Peek
{
class InlineCommentPeekRelationship : IPeekRelationship
{
static InlineCommentPeekRelationship instance;
private InlineCommentPeekRelationship()
{
}
public static InlineCommentPeekRelationship Instance
{
get
{
if (instance == null)
{
instance = new InlineCommentPeekRelationship();
}
return instance;
}
}
public string DisplayName => "GitHub Code Review";
public string Name => "GitHubCodeReview";
}
}

Просмотреть файл

@ -0,0 +1,35 @@
using System;
using Microsoft.VisualStudio.Language.Intellisense;
using GitHub.Extensions;
using GitHub.InlineReviews.ViewModels;
namespace GitHub.InlineReviews.Peek
{
class InlineCommentPeekResult : IPeekResult
{
public InlineCommentPeekResult(InlineCommentPeekViewModel viewModel)
{
Guard.ArgumentNotNull(viewModel, nameof(viewModel));
this.ViewModel = viewModel;
}
public bool CanNavigateTo => true;
public InlineCommentPeekViewModel ViewModel { get; }
public IPeekResultDisplayInfo DisplayInfo { get; }
= new PeekResultDisplayInfo("Review", null, "GitHub Review", "GitHub Review");
public Action<IPeekResult, object, object> PostNavigationCallback => null;
public event EventHandler Disposed;
public void Dispose()
{
}
public void NavigateTo(object data)
{
}
}
}

Просмотреть файл

@ -0,0 +1,71 @@
using System;
using System.Windows;
using Microsoft.VisualStudio.Language.Intellisense;
using GitHub.InlineReviews.ViewModels;
using GitHub.InlineReviews.Views;
namespace GitHub.InlineReviews.Peek
{
class InlineCommentPeekResultPresentation : IPeekResultPresentation
{
readonly InlineCommentPeekViewModel viewModel;
public bool IsDirty => false;
public bool IsReadOnly => true;
public InlineCommentPeekResultPresentation(InlineCommentPeekViewModel viewModel)
{
this.viewModel = viewModel;
}
public double ZoomLevel
{
get { return 1.0; }
set { }
}
public event EventHandler IsDirtyChanged;
public event EventHandler IsReadOnlyChanged;
public event EventHandler<RecreateContentEventArgs> RecreateContent;
public bool CanSave(out string defaultPath)
{
defaultPath = null;
return false;
}
public IPeekResultScrollState CaptureScrollState()
{
return null;
}
public void Close()
{
}
public UIElement Create(IPeekSession session, IPeekResultScrollState scrollState)
{
var view = new InlineCommentPeekView();
view.DataContext = viewModel;
return view;
}
public void Dispose()
{
}
public void ScrollIntoView(IPeekResultScrollState scrollState)
{
}
public void SetKeyboardFocus()
{
}
public bool TryOpen(IPeekResult otherResult) => false;
public bool TryPrepareToClose() => true;
public bool TrySave(bool saveAs) => true;
}
}

Просмотреть файл

@ -0,0 +1,20 @@
using System;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Utilities;
namespace GitHub.InlineReviews.Peek
{
[Export(typeof(IPeekResultPresenter))]
[Name("GitHub Inline Comments Peek Presenter")]
class InlineCommentPeekResultPresenter : IPeekResultPresenter
{
public IPeekResultPresentation TryCreatePeekResultPresentation(IPeekResult result)
{
var review = result as InlineCommentPeekResult;
return review != null ?
new InlineCommentPeekResultPresentation(review.ViewModel) :
null;
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.Language.Intellisense;
using GitHub.InlineReviews.ViewModels;
namespace GitHub.InlineReviews.Peek
{
class InlineCommentPeekableItem : IPeekableItem
{
public InlineCommentPeekableItem(InlineCommentPeekViewModel viewModel)
{
ViewModel = viewModel;
}
public string DisplayName => "GitHub Code Review";
public InlineCommentPeekViewModel ViewModel { get; }
public IEnumerable<IPeekRelationship> Relationships => new[] { InlineCommentPeekRelationship.Instance };
public IPeekResultSource GetOrCreateResultSource(string relationshipName)
{
return new InlineCommentPeekableResultSource(ViewModel);
}
}
}

Просмотреть файл

@ -0,0 +1,54 @@
using System.Collections.Generic;
using GitHub.Extensions;
using GitHub.Factories;
using GitHub.InlineReviews.Commands;
using GitHub.InlineReviews.Services;
using GitHub.InlineReviews.ViewModels;
using GitHub.Services;
using Microsoft.VisualStudio.Language.Intellisense;
namespace GitHub.InlineReviews.Peek
{
class InlineCommentPeekableItemSource : IPeekableItemSource
{
readonly IApiClientFactory apiClientFactory;
readonly IInlineCommentPeekService peekService;
readonly IPullRequestSessionManager sessionManager;
readonly INextInlineCommentCommand nextCommentCommand;
readonly IPreviousInlineCommentCommand previousCommentCommand;
public InlineCommentPeekableItemSource(
IApiClientFactory apiClientFactory,
IInlineCommentPeekService peekService,
IPullRequestSessionManager sessionManager,
INextInlineCommentCommand nextCommentCommand,
IPreviousInlineCommentCommand previousCommentCommand)
{
this.apiClientFactory = apiClientFactory;
this.peekService = peekService;
this.sessionManager = sessionManager;
this.nextCommentCommand = nextCommentCommand;
this.previousCommentCommand = previousCommentCommand;
}
public void AugmentPeekSession(IPeekSession session, IList<IPeekableItem> peekableItems)
{
if (session.RelationshipName == InlineCommentPeekRelationship.Instance.Name)
{
var viewModel = new InlineCommentPeekViewModel(
apiClientFactory,
peekService,
session,
sessionManager,
nextCommentCommand,
previousCommentCommand);
viewModel.Initialize().Forget();
peekableItems.Add(new InlineCommentPeekableItem(viewModel));
}
}
public void Dispose()
{
}
}
}

Просмотреть файл

@ -0,0 +1,49 @@
using System;
using System.ComponentModel.Composition;
using GitHub.Factories;
using GitHub.InlineReviews.Commands;
using GitHub.InlineReviews.Services;
using GitHub.Services;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
namespace GitHub.InlineReviews.Peek
{
[Export(typeof(IPeekableItemSourceProvider))]
[ContentType("text")]
[Name("GitHub Inline Comments Peekable Item Source")]
class InlineCommentPeekableItemSourceProvider : IPeekableItemSourceProvider
{
readonly IApiClientFactory apiClientFactory;
readonly IInlineCommentPeekService peekService;
readonly IPullRequestSessionManager sessionManager;
readonly INextInlineCommentCommand nextCommentCommand;
readonly IPreviousInlineCommentCommand previousCommentCommand;
[ImportingConstructor]
public InlineCommentPeekableItemSourceProvider(
IApiClientFactory apiClientFactory,
IInlineCommentPeekService peekService,
IPullRequestSessionManager sessionManager,
INextInlineCommentCommand nextCommentCommand,
IPreviousInlineCommentCommand previousCommentCommand)
{
this.apiClientFactory = apiClientFactory;
this.peekService = peekService;
this.sessionManager = sessionManager;
this.nextCommentCommand = nextCommentCommand;
this.previousCommentCommand = previousCommentCommand;
}
public IPeekableItemSource TryCreatePeekableItemSource(ITextBuffer textBuffer)
{
return new InlineCommentPeekableItemSource(
apiClientFactory,
peekService,
sessionManager,
nextCommentCommand,
previousCommentCommand);
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
using System;
using System.Threading;
using Microsoft.VisualStudio.Language.Intellisense;
using GitHub.InlineReviews.ViewModels;
namespace GitHub.InlineReviews.Peek
{
class InlineCommentPeekableResultSource : IPeekResultSource
{
readonly InlineCommentPeekViewModel viewModel;
public InlineCommentPeekableResultSource(InlineCommentPeekViewModel viewModel)
{
this.viewModel = viewModel;
}
public void FindResults(string relationshipName, IPeekResultCollection resultCollection, CancellationToken cancellationToken, IFindPeekResultsCallback callback)
{
resultCollection.Add(new InlineCommentPeekResult(viewModel));
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("GitHub.InlineReviews")]
[assembly: AssemblyDescription("Provides inline viewing of PR review comments")]
[assembly: Guid("3bf91177-3d16-425d-9c62-50a86cf26298")]

Двоичные данные
src/GitHub.InlineReviews/Resources/logo_32x32@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.3 KiB

Просмотреть файл

@ -0,0 +1,20 @@
using System;
using System.Collections.ObjectModel;
using GitHub.InlineReviews.ViewModels;
using GitHub.Models;
using GitHub.SampleData;
using ReactiveUI;
namespace GitHub.InlineReviews.SampleData
{
class CommentThreadViewModelDesigner : ICommentThreadViewModel
{
public ObservableCollection<ICommentViewModel> Comments { get; }
= new ObservableCollection<ICommentViewModel>();
public IAccount CurrentUser { get; set; }
= new AccountDesigner { Login = "shana", IsUser = true };
public ReactiveCommand<ICommentModel> PostComment { get; }
}
}

Просмотреть файл

@ -0,0 +1,29 @@
using System;
using System.Reactive;
using GitHub.InlineReviews.ViewModels;
using GitHub.Models;
using GitHub.SampleData;
using ReactiveUI;
namespace GitHub.InlineReviews.SampleData
{
class CommentViewModelDesigner : ICommentViewModel
{
public CommentViewModelDesigner()
{
User = new AccountDesigner { Login = "shana", IsUser = true };
}
public int Id { get; set; }
public string Body { get; set; }
public string ErrorMessage { get; set; }
public CommentEditState EditState { get; set; }
public bool IsReadOnly { get; set; }
public DateTimeOffset UpdatedAt => DateTime.Now.Subtract(TimeSpan.FromDays(3));
public IAccount User { get; set; }
public ReactiveCommand<object> BeginEdit { get; }
public ReactiveCommand<object> CancelEdit { get; }
public ReactiveCommand<Unit> CommitEdit { get; }
}
}

Просмотреть файл

@ -0,0 +1,13 @@
using System;
using GitHub.InlineReviews.ViewModels;
namespace GitHub.InlineReviews.SampleData
{
class DiffCommentThreadViewModelDesigner : IDiffCommentThreadViewModel
{
public string DiffHunk { get; set; }
public int LineNumber { get; set; }
public string Path { get; set; }
public ICommentThreadViewModel Comments { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,18 @@
using System;
using System.Collections.ObjectModel;
using GitHub.InlineReviews.ViewModels;
using GitHub.Models;
using ReactiveUI;
namespace GitHub.InlineReviews.SampleData
{
class PullRequestCommentsViewModelDesigner : IPullRequestCommentsViewModel
{
public IRepositoryModel Repository { get; set; }
public int Number { get; set; }
public string Title { get; set; }
public ICommentThreadViewModel Conversation { get; set; }
public IReactiveList<IDiffCommentThreadViewModel> FileComments { get; }
= new ReactiveList<IDiffCommentThreadViewModel>();
}
}

Просмотреть файл

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using LibGit2Sharp;
namespace GitHub.InlineReviews.Services
{
[Export(typeof(IDiffService))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class DiffService : IDiffService
{
readonly IGitClient gitClient;
[ImportingConstructor]
public DiffService(IGitClient gitClient)
{
this.gitClient = gitClient;
}
public async Task<IList<DiffChunk>> Diff(
IRepository repo,
string sha,
string path,
byte[] contents)
{
var changes = await gitClient.CompareWith(repo, sha, path, contents);
return DiffUtilities.ParseFragment(changes.Patch).ToList();
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.Models;
using LibGit2Sharp;
namespace GitHub.InlineReviews.Services
{
public interface IDiffService
{
Task<IList<DiffChunk>> Diff(IRepository repo, string baseSha, string relativePath, byte[] contents);
}
}

Просмотреть файл

@ -0,0 +1,44 @@
using System;
using GitHub.InlineReviews.Tags;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace GitHub.InlineReviews.Services
{
/// <summary>
/// Shows inline comments in a peek view.
/// </summary>
public interface IInlineCommentPeekService
{
/// <summary>
/// Gets the line number for a peek session tracking point.
/// </summary>
/// <param name="session">The peek session.</param>
/// <returns>
/// A tuple containing the line number and whether the line number represents a line in the
/// left hand side of a diff view.
/// </returns>
Tuple<int, bool> GetLineNumber(IPeekSession session, ITrackingPoint point);
/// <summary>
/// Hides the inline comment peek view for a text view.
/// </summary>
/// <param name="textView">The text view.</param>
void Hide(ITextView textView);
/// <summary>
/// Shows the peek view for a <see cref="ShowInlineCommentTag"/>.
/// </summary>
/// <param name="textView">The text view.</param>
/// <param name="tag">The tag.</param>
ITrackingPoint Show(ITextView textView, ShowInlineCommentTag tag);
/// <summary>
/// Shows the peek view for an <see cref="AddInlineCommentTag"/>.
/// </summary>
/// <param name="textView">The text view.</param>
/// <param name="tag">The tag.</param>
ITrackingPoint Show(ITextView textView, AddInlineCommentTag tag);
}
}

Просмотреть файл

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.Models;
namespace GitHub.InlineReviews.Services
{
/// <summary>
/// Provides a common interface for services required by <see cref="PullRequestSession"/>.
/// </summary>
public interface IPullRequestSessionService
{
/// <summary>
/// Carries out a diff between a file at a commit and the current file contents.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="baseSha">The commit to use as the base.</param>
/// <param name="relativePath">The relative path to the file.</param>
/// <param name="contents">The contents of the file.</param>
/// <returns></returns>
Task<IList<DiffChunk>> Diff(
ILocalRepositoryModel repository,
string baseSha,
string relativePath,
byte[] contents);
/// <summary>
/// Tests whether the contents of a file represent a commit that is pushed to origin.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="relativePath">The relative path to the file.</param>
/// <param name="contents">The contents of the file.</param>
/// <returns>
/// A task returning true if the file is unmodified with respect to the latest commit
/// pushed to origin; otherwise false.
/// </returns>
Task<bool> IsUnmodifiedAndPushed(
ILocalRepositoryModel repository,
string relativePath,
byte[] contents);
/// <summary>
/// Extracts a file at a specified commit from the repository.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="commitSha">The SHA of the commit.</param>
/// <param name="relativePath">The path to the file, relative to the repository.</param>
/// <returns>
/// The contents of the file, or null if the file was not found at the specified commit.
/// </returns>
Task<byte[]> ExtractFileFromGit(
ILocalRepositoryModel repository,
int pullRequestNumber,
string sha,
string relativePath);
/// <summary>
/// Gets the SHA of the tip of the current branch.
/// </summary>
/// <param name="repository">The repository.</param>
/// <returns>The tip SHA.</returns>
Task<string> GetTipSha(ILocalRepositoryModel repository);
/// <summary>
/// Asynchronously reads the contents of a file.
/// </summary>
/// <param name="path">The full path to the file.</param>
/// <returns>
/// A task returning the contents of the file, or null if the file was not found.
/// </returns>
Task<byte[]> ReadFileAsync(string path);
/// <summary>
/// Find the merge base for a pull request.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="pullRequest">The pull request.</param>
/// <returns>
/// The merge base SHA for the PR.
/// </returns>
Task<string> GetPullRequestMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest);
}
}

Просмотреть файл

@ -0,0 +1,154 @@
using System;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using GitHub.Api;
using GitHub.Extensions;
using GitHub.Factories;
using GitHub.InlineReviews.Peek;
using GitHub.InlineReviews.Tags;
using GitHub.Models;
using GitHub.Primitives;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Differencing;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.Text.Projection;
namespace GitHub.InlineReviews.Services
{
/// <summary>
/// Shows inline comments in a peek view.
/// </summary>
[Export(typeof(IInlineCommentPeekService))]
class InlineCommentPeekService : IInlineCommentPeekService
{
readonly IApiClientFactory apiClientFactory;
readonly IOutliningManagerService outliningService;
readonly IPeekBroker peekBroker;
[ImportingConstructor]
public InlineCommentPeekService(
IApiClientFactory apiClientFactory,
IOutliningManagerService outliningManager,
IPeekBroker peekBroker)
{
this.apiClientFactory = apiClientFactory;
this.outliningService = outliningManager;
this.peekBroker = peekBroker;
}
/// <inheritdoc/>
public Tuple<int, bool> GetLineNumber(IPeekSession session, ITrackingPoint point)
{
var diffModel = (session.TextView as IWpfTextView)?.TextViewModel as IDifferenceTextViewModel;
var leftBuffer = false;
ITextSnapshotLine line = null;
if (diffModel != null)
{
if (diffModel.ViewType == DifferenceViewType.InlineView)
{
// If we're displaying a diff in inline mode, then we're in the left buffer if
// the point can be mapped down to the left buffer.
var snapshotPoint = point.GetPoint(point.TextBuffer.CurrentSnapshot);
var mappedPoint = session.TextView.BufferGraph.MapDownToBuffer(
snapshotPoint,
PointTrackingMode.Negative,
diffModel.Viewer.DifferenceBuffer.LeftBuffer,
PositionAffinity.Successor);
if (mappedPoint != null)
{
leftBuffer = true;
line = mappedPoint.Value.GetContainingLine();
}
}
else
{
// If we're displaying a diff in any other mode than inline, then we're in the
// left buffer if the session's text view is the diff's left view.
leftBuffer = session.TextView == diffModel.Viewer.LeftView;
}
}
if (line == null)
{
line = point.GetPoint(point.TextBuffer.CurrentSnapshot).GetContainingLine();
}
return Tuple.Create(line.LineNumber, leftBuffer);
}
/// <inheritdoc/>
public void Hide(ITextView textView)
{
peekBroker.DismissPeekSession(textView);
}
/// <inheritdoc/>
public ITrackingPoint Show(ITextView textView, AddInlineCommentTag tag)
{
Guard.ArgumentNotNull(tag, nameof(tag));
var line = textView.TextSnapshot.GetLineFromLineNumber(tag.LineNumber);
var trackingPoint = textView.TextSnapshot.CreateTrackingPoint(line.Start.Position, PointTrackingMode.Positive);
ExpandCollapsedRegions(textView, line.Extent);
var session = peekBroker.TriggerPeekSession(textView, trackingPoint, InlineCommentPeekRelationship.Instance.Name);
var item = session.PeekableItems.OfType<InlineCommentPeekableItem>().FirstOrDefault();
if (item != null)
{
var placeholder = item.ViewModel.Thread.Comments.Last();
placeholder.CancelEdit.Take(1).Subscribe(_ => session.Dismiss());
}
return trackingPoint;
}
/// <inheritdoc/>
public ITrackingPoint Show(ITextView textView, ShowInlineCommentTag tag)
{
Guard.ArgumentNotNull(textView, nameof(textView));
Guard.ArgumentNotNull(tag, nameof(tag));
var projectionBuffer = textView.TextBuffer as IProjectionBuffer;
var snapshot = textView.TextSnapshot;
// If we're displaying a comment on a deleted line, then check if we're displaying in a
// diff view in inline mode. If so, get the line from the left buffer.
if (tag.DiffChangeType == DiffChangeType.Delete)
{
var diffModel = (textView as IWpfTextView)?.TextViewModel as IDifferenceTextViewModel;
if (diffModel?.ViewType == DifferenceViewType.InlineView)
{
snapshot = diffModel.Viewer.DifferenceBuffer.LeftBuffer.CurrentSnapshot;
}
}
var line = snapshot.GetLineFromLineNumber(tag.LineNumber);
var trackingPoint = snapshot.CreateTrackingPoint(line.Start.Position, PointTrackingMode.Positive);
ExpandCollapsedRegions(textView, line.Extent);
peekBroker.TriggerPeekSession(textView, trackingPoint, InlineCommentPeekRelationship.Instance.Name);
return trackingPoint;
}
void ExpandCollapsedRegions(ITextView textView, SnapshotSpan span)
{
var outlining = outliningService.GetOutliningManager(textView);
if (outlining != null)
{
foreach (var collapsed in outlining.GetCollapsedRegions(span))
{
outlining.Expand(collapsed);
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.InlineReviews.Models;
using GitHub.Models;
using GitHub.Services;
using ReactiveUI;
using System.Threading;
namespace GitHub.InlineReviews.Services
{
/// <summary>
/// A pull request session used to display inline reviews.
/// </summary>
/// <remarks>
/// A pull request session represents the real-time state of a pull request in the IDE.
/// It takes the pull request model and updates according to the current state of the
/// repository on disk and in the editor.
/// </remarks>
public class PullRequestSession : ReactiveObject, IPullRequestSession
{
static readonly List<IPullRequestReviewCommentModel> Empty = new List<IPullRequestReviewCommentModel>();
readonly IPullRequestSessionService service;
readonly Dictionary<string, PullRequestSessionFile> fileIndex = new Dictionary<string, PullRequestSessionFile>();
readonly SemaphoreSlim getFilesLock = new SemaphoreSlim(1);
bool isCheckedOut;
IReadOnlyList<IPullRequestSessionFile> files;
public PullRequestSession(
IPullRequestSessionService service,
IAccount user,
IPullRequestModel pullRequest,
ILocalRepositoryModel repository,
bool isCheckedOut)
{
Guard.ArgumentNotNull(service, nameof(service));
Guard.ArgumentNotNull(user, nameof(user));
Guard.ArgumentNotNull(pullRequest, nameof(pullRequest));
Guard.ArgumentNotNull(repository, nameof(repository));
this.service = service;
this.isCheckedOut = isCheckedOut;
User = user;
PullRequest = pullRequest;
Repository = repository;
}
/// <inheritdoc/>
public bool IsCheckedOut
{
get { return isCheckedOut; }
internal set { this.RaiseAndSetIfChanged(ref isCheckedOut, value); }
}
/// <inheritdoc/>
public IAccount User { get; }
/// <inheritdoc/>
public IPullRequestModel PullRequest { get; private set; }
/// <inheritdoc/>
public ILocalRepositoryModel Repository { get; }
IEnumerable<string> FilePaths
{
get { return PullRequest.ChangedFiles.Select(x => x.FileName); }
}
/// <inheritdoc/>
public async Task AddComment(IPullRequestReviewCommentModel comment)
{
PullRequest.ReviewComments = PullRequest.ReviewComments
.Concat(new[] { comment })
.ToList();
await Update(PullRequest);
}
/// <inheritdoc/>
public async Task<IReadOnlyList<IPullRequestSessionFile>> GetAllFiles()
{
if (files == null)
{
files = await CreateAllFiles();
}
return files;
}
/// <inheritdoc/>
public async Task<IPullRequestSessionFile> GetFile(string relativePath)
{
return await GetFile(relativePath, null);
}
/// <inheritdoc/>
public async Task<IPullRequestSessionFile> GetFile(
string relativePath,
IEditorContentSource contentSource)
{
await getFilesLock.WaitAsync();
try
{
PullRequestSessionFile file;
relativePath = relativePath.Replace("\\", "/");
if (!fileIndex.TryGetValue(relativePath, out file))
{
// TODO: Check for binary files.
file = await CreateFile(relativePath, contentSource);
fileIndex.Add(relativePath, file);
}
else if (contentSource != null && file.ContentSource != contentSource)
{
file.ContentSource = contentSource;
await UpdateEditorContent(relativePath);
}
return file;
}
finally
{
getFilesLock.Release();
}
}
/// <inheritdoc/>
public string GetRelativePath(string path)
{
if (Path.IsPathRooted(path))
{
var basePath = Repository.LocalPath;
if (path.StartsWith(basePath) && path.Length > basePath.Length + 1)
{
return path.Substring(basePath.Length + 1);
}
}
return null;
}
/// <inheritdoc/>
public async Task UpdateEditorContent(string relativePath)
{
PullRequestSessionFile file;
relativePath = relativePath.Replace("\\", "/");
if (fileIndex.TryGetValue(relativePath, out file))
{
var content = await GetFileContent(file);
file.CommitSha = await CalculateCommitSha(file, content);
var mergeBaseSha = await service.GetPullRequestMergeBase(Repository, PullRequest);
file.Diff = await service.Diff(Repository, mergeBaseSha, relativePath, content);
foreach (var thread in file.InlineCommentThreads)
{
thread.LineNumber = GetUpdatedLineNumber(thread, file.Diff);
thread.IsStale = false;
}
}
}
public async Task Update(IPullRequestModel pullRequest)
{
PullRequest = pullRequest;
foreach (var file in this.fileIndex.Values)
{
await UpdateFile(file);
}
}
async Task UpdateFile(PullRequestSessionFile file)
{
var content = await GetFileContent(file);
file.BaseSha = PullRequest.Base.Sha;
file.CommitSha = await CalculateCommitSha(file, content);
var mergeBaseSha = await service.GetPullRequestMergeBase(Repository, PullRequest);
file.Diff = await service.Diff(Repository, mergeBaseSha, file.RelativePath, content);
var commentsByPosition = PullRequest.ReviewComments
.Where(x => x.Path == file.RelativePath && x.OriginalPosition.HasValue)
.OrderBy(x => x.Id)
.GroupBy(x => Tuple.Create(x.OriginalCommitId, x.OriginalPosition.Value));
var threads = new List<IInlineCommentThreadModel>();
foreach (var comments in commentsByPosition)
{
var hunk = comments.First().DiffHunk;
var chunks = DiffUtilities.ParseFragment(hunk);
var chunk = chunks.Last();
var diffLines = chunk.Lines.Reverse().Take(5).ToList();
var thread = new InlineCommentThreadModel(
file.RelativePath,
comments.Key.Item1,
comments.Key.Item2,
diffLines,
comments);
thread.LineNumber = GetUpdatedLineNumber(thread, file.Diff);
threads.Add(thread);
}
file.InlineCommentThreads = threads;
}
async Task<PullRequestSessionFile> CreateFile(
string relativePath,
IEditorContentSource contentSource)
{
var file = new PullRequestSessionFile(relativePath);
file.ContentSource = contentSource;
await UpdateFile(file);
return file;
}
async Task<IReadOnlyList<IPullRequestSessionFile>> CreateAllFiles()
{
var result = new List<IPullRequestSessionFile>();
foreach (var path in FilePaths)
{
result.Add(await CreateFile(path, null));
}
return result;
}
async Task<string> CalculateCommitSha(IPullRequestSessionFile file, byte[] content)
{
if (IsCheckedOut)
{
return await service.IsUnmodifiedAndPushed(Repository, file.RelativePath, content) ?
await service.GetTipSha(Repository) : null;
}
else
{
return PullRequest.Head.Sha;
}
}
Task<byte[]> GetFileContent(IPullRequestSessionFile file)
{
if (!IsCheckedOut)
{
return service.ExtractFileFromGit(
Repository,
PullRequest.Number,
PullRequest.Head.Sha,
file.RelativePath);
}
else if (file.ContentSource != null)
{
return file.ContentSource?.GetContent();
}
else
{
return service.ReadFileAsync(Path.Combine(Repository.LocalPath, file.RelativePath));
}
}
string GetFullPath(string relativePath)
{
return Path.Combine(Repository.LocalPath, relativePath);
}
int GetUpdatedLineNumber(IInlineCommentThreadModel thread, IEnumerable<DiffChunk> diff)
{
var line = DiffUtilities.Match(diff, thread.DiffMatch);
if (line != null)
{
return (thread.DiffLineType == DiffChangeType.Delete) ?
line.OldLineNumber - 1 :
line.NewLineNumber - 1;
}
return -1;
}
}
}

Просмотреть файл

@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Models;
using GitHub.Primitives;
using GitHub.Services;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Projection;
using ReactiveUI;
namespace GitHub.InlineReviews.Services
{
/// <summary>
/// Manages pull request sessions.
/// </summary>
[Export(typeof(IPullRequestSessionManager))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class PullRequestSessionManager : ReactiveObject, IPullRequestSessionManager
{
readonly IPullRequestService service;
readonly IPullRequestSessionService sessionService;
readonly IRepositoryHosts hosts;
readonly ITeamExplorerServiceHolder teamExplorerService;
readonly Dictionary<int, WeakReference<PullRequestSession>> sessions =
new Dictionary<int, WeakReference<PullRequestSession>>();
IPullRequestSession currentSession;
ILocalRepositoryModel repository;
/// <summary>
/// Initializes a new instance of the <see cref="PullRequestSessionManager"/> class.
/// </summary>
/// <param name="gitService">The git service to use.</param>
/// <param name="gitClient">The git client to use.</param>
/// <param name="diffService">The diff service to use.</param>
/// <param name="service">The pull request service to use.</param>
/// <param name="hosts">The repository hosts.</param>
/// <param name="teamExplorerService">The team explorer service to use.</param>
[ImportingConstructor]
public PullRequestSessionManager(
IPullRequestService service,
IPullRequestSessionService sessionService,
IRepositoryHosts hosts,
ITeamExplorerServiceHolder teamExplorerService)
{
Guard.ArgumentNotNull(service, nameof(service));
Guard.ArgumentNotNull(sessionService, nameof(sessionService));
Guard.ArgumentNotNull(hosts, nameof(hosts));
Guard.ArgumentNotNull(teamExplorerService, nameof(teamExplorerService));
this.service = service;
this.sessionService = sessionService;
this.hosts = hosts;
this.teamExplorerService = teamExplorerService;
teamExplorerService.Subscribe(this, x => RepoChanged(x).Forget());
}
/// <inheritdoc/>
public IPullRequestSession CurrentSession
{
get { return currentSession; }
private set { this.RaiseAndSetIfChanged(ref currentSession, value); }
}
/// <inheritdoc/>
public async Task<IPullRequestSession> GetSession(IPullRequestModel pullRequest)
{
if (await service.EnsureLocalBranchesAreMarkedAsPullRequests(repository, pullRequest))
{
// The branch for the PR was not previously marked with the PR number in the git
// config so we didn't pick up that the current branch is a PR branch. That has
// now been corrected, so call RepoChanged to make sure everything is up-to-date.
await RepoChanged(repository);
}
return await GetSessionInternal(pullRequest);
}
/// <inheritdoc/>
public PullRequestTextBufferInfo GetTextBufferInfo(ITextBuffer buffer)
{
var projectionBuffer = buffer as IProjectionBuffer;
if (projectionBuffer == null)
{
return buffer.Properties.GetProperty<PullRequestTextBufferInfo>(typeof(PullRequestTextBufferInfo), null);
}
else
{
foreach (var sourceBuffer in projectionBuffer.SourceBuffers)
{
var sourceBufferInfo = GetTextBufferInfo(sourceBuffer);
if (sourceBufferInfo != null) return sourceBufferInfo;
}
}
return null;
}
async Task RepoChanged(ILocalRepositoryModel repository)
{
try
{
await EnsureLoggedIn(repository);
if (repository != this.repository)
{
this.repository = repository;
CurrentSession = null;
sessions.Clear();
}
var modelService = hosts.LookupHost(HostAddress.Create(repository.CloneUrl))?.ModelService;
var session = CurrentSession;
if (modelService != null)
{
var number = await service.GetPullRequestForCurrentBranch(repository).FirstOrDefaultAsync();
if (number != (CurrentSession?.PullRequest.Number ?? 0))
{
var pullRequest = await GetPullRequestForTip(modelService, repository);
if (pullRequest != null)
{
var newSession = await GetSessionInternal(pullRequest); ;
if (newSession != null) newSession.IsCheckedOut = true;
session = newSession;
}
}
}
else
{
session = null;
}
CurrentSession = session;
}
catch
{
// TODO: Log
}
}
async Task<IPullRequestModel> GetPullRequestForTip(IModelService modelService, ILocalRepositoryModel repository)
{
if (modelService != null)
{
var number = await service.GetPullRequestForCurrentBranch(repository);
if (number != 0) return await modelService.GetPullRequest(repository, number).ToTask();
}
return null;
}
async Task<PullRequestSession> GetSessionInternal(IPullRequestModel pullRequest)
{
PullRequestSession session = null;
WeakReference<PullRequestSession> weakSession;
if (sessions.TryGetValue(pullRequest.Number, out weakSession))
{
weakSession.TryGetTarget(out session);
}
if (session == null)
{
var modelService = hosts.LookupHost(HostAddress.Create(repository.CloneUrl))?.ModelService;
if (modelService != null)
{
session = new PullRequestSession(
sessionService,
await modelService.GetCurrentUser(),
pullRequest,
repository,
false);
sessions[pullRequest.Number] = new WeakReference<PullRequestSession>(session);
}
}
else
{
await session.Update(pullRequest);
}
return session;
}
async Task EnsureLoggedIn(ILocalRepositoryModel repository)
{
if (!hosts.IsLoggedInToAnyHost && repository != null)
{
var hostAddress = HostAddress.Create(repository.CloneUrl);
await hosts.LogInFromCache(hostAddress);
}
}
}
}

Просмотреть файл

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using LibGit2Sharp;
namespace GitHub.InlineReviews.Services
{
/// <summary>
/// Provides a common interface for services required by <see cref="PullRequestSession"/>.
/// </summary>
[Export(typeof(IPullRequestSessionService))]
class PullRequestSessionService : IPullRequestSessionService
{
readonly IGitService gitService;
readonly IGitClient gitClient;
readonly IDiffService diffService;
readonly IDictionary<Tuple<string, string>, string> mergeBaseCache;
[ImportingConstructor]
public PullRequestSessionService(
IGitService gitService,
IGitClient gitClient,
IDiffService diffService)
{
this.gitService = gitService;
this.gitClient = gitClient;
this.diffService = diffService;
mergeBaseCache = new Dictionary<Tuple<string, string>, string>();
}
/// <inheritdoc/>
public async Task<IList<DiffChunk>> Diff(ILocalRepositoryModel repository, string baseSha, string relativePath, byte[] contents)
{
var repo = await GetRepository(repository);
return await diffService.Diff(repo, baseSha, relativePath, contents);
}
/// <inheritdoc/>
public async Task<string> GetTipSha(ILocalRepositoryModel repository)
{
var repo = await GetRepository(repository);
return repo.Head.Tip.Sha;
}
/// <inheritdoc/>
public async Task<bool> IsUnmodifiedAndPushed(ILocalRepositoryModel repository, string relativePath, byte[] contents)
{
var repo = await GetRepository(repository);
return !await gitClient.IsModified(repo, relativePath, contents) &&
await gitClient.IsHeadPushed(repo);
}
public async Task<byte[]> ExtractFileFromGit(
ILocalRepositoryModel repository,
int pullRequestNumber,
string sha,
string relativePath)
{
var repo = await GetRepository(repository);
try
{
return await gitClient.ExtractFileBinary(repo, sha, relativePath);
}
catch (FileNotFoundException)
{
var pullHeadRef = $"refs/pull/{pullRequestNumber}/head";
await gitClient.Fetch(repo, "origin", sha, pullHeadRef);
return await gitClient.ExtractFileBinary(repo, sha, relativePath);
}
}
/// <inheritdoc/>
public async Task<byte[]> ReadFileAsync(string path)
{
if (File.Exists(path))
{
try
{
using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
{
var buffer = new MemoryStream();
await file.CopyToAsync(buffer);
return buffer.ToArray();
}
}
catch { }
}
return null;
}
/// <inheritdoc/>
public async Task<string> GetPullRequestMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest)
{
var baseSha = pullRequest.Base.Sha;
var headSha = pullRequest.Head.Sha;
var key = new Tuple<string, string>(baseSha, headSha);
string mergeBase;
if(mergeBaseCache.TryGetValue(key, out mergeBase))
{
return mergeBase;
}
var repo = await GetRepository(repository);
var remote = await gitClient.GetHttpRemote(repo, "origin");
var baseRef = pullRequest.Base.Ref;
var pullNumber = pullRequest.Number;
mergeBase = await gitClient.GetPullRequestMergeBase(repo, remote.Name, baseSha, headSha, baseRef, pullNumber);
if (mergeBase == null)
{
throw new FileNotFoundException($"Couldn't find merge base between {baseSha} and {headSha}.");
}
mergeBaseCache[key] = mergeBase;
return mergeBase;
}
Task<IRepository> GetRepository(ILocalRepositoryModel repository)
{
return Task.Factory.StartNew(() => gitService.GetRepository(repository.LocalPath));
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
<UserControl x:Class="GitHub.InlineReviews.Tags.AddInlineCommentGlyph"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<Viewbox x:Name="AddViewbox">
<Path Stroke="Black"
Data="M13 2H1c-0.55 0-1 0.45-1 1v8c0 0.55 0.45 1 1 1h2v3.5l3.5-3.5h6.5c0.55 0 1-0.45 1-1V3c0-0.55-0.45-1-1-1z m0 9H6L4 13V11H1V3h12v8z M7,5 L7,9 M5,7 L9,7"/>
</Viewbox>
</UserControl>

Просмотреть файл

@ -0,0 +1,18 @@
using System;
using System.Windows;
using System.Windows.Controls;
namespace GitHub.InlineReviews.Tags
{
public partial class AddInlineCommentGlyph : UserControl
{
public AddInlineCommentGlyph()
{
InitializeComponent();
AddViewbox.Visibility = Visibility.Hidden;
MouseEnter += (s, e) => AddViewbox.Visibility = Visibility.Visible;
MouseLeave += (s, e) => AddViewbox.Visibility = Visibility.Hidden;
}
}
}

Просмотреть файл

@ -0,0 +1,57 @@
using System;
using GitHub.Services;
using GitHub.Models;
namespace GitHub.InlineReviews.Tags
{
/// <summary>
/// A tag which marks a line in an editor where a new review comment can be added.
/// </summary>
public class AddInlineCommentTag : InlineCommentTag
{
/// <summary>
/// Initializes a new instance of the <see cref="AddInlineCommentTag"/> class.
/// </summary>
/// <param name="session">The pull request session.</param>
/// <param name="commitSha">
/// The SHA of the commit to which a new comment should be added. May be null if the tag
/// represents trying to add a comment to a line that hasn't yet been pushed.
/// </param>
/// <param name="filePath">The path to the file.</param>
/// <param name="diffLine">The line in the diff that the line relates to.</param>
/// <param name="lineNumber">The line in the file.</param>
/// <param name="diffChangeType">The type of represented by the diff line.</param>
public AddInlineCommentTag(
IPullRequestSession session,
string commitSha,
string filePath,
int diffLine,
int lineNumber,
DiffChangeType diffChangeType)
: base(session, lineNumber, diffChangeType)
{
CommitSha = commitSha;
DiffLine = diffLine;
FilePath = filePath;
}
/// <summary>
/// Gets the SHA of the commit to which a new comment should be added.
/// </summary>
/// <remarks>
/// May be null if the tag represents trying to add a comment to a line that hasn't yet been
/// pushed.
/// </remarks>
public string CommitSha { get; }
/// <summary>
/// Gets the line in the diff that the line relates to.
/// </summary>
public int DiffLine { get; }
/// <summary>
/// Gets the path to the file.
/// </summary>
public string FilePath { get; }
}
}

Просмотреть файл

@ -0,0 +1,129 @@
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using System.Collections.Generic;
using GitHub.InlineReviews.Glyph;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Formatting;
using Microsoft.VisualStudio.Text.Classification;
using GitHub.InlineReviews.Services;
using GitHub.Models;
namespace GitHub.InlineReviews.Tags
{
class InlineCommentGlyphFactory : IGlyphFactory<InlineCommentTag>
{
readonly IInlineCommentPeekService peekService;
readonly ITextView textView;
readonly BrushesManager brushesManager;
public InlineCommentGlyphFactory(
IInlineCommentPeekService peekService,
ITextView textView,
IEditorFormatMap editorFormatMap)
{
this.peekService = peekService;
this.textView = textView;
brushesManager = new BrushesManager(editorFormatMap);
}
class BrushesManager
{
const string AddPropertiesKey = "deltadiff.add.word";
const string DeletePropertiesKey = "deltadiff.remove.word";
const string NonePropertiesKey = "Indicator Margin";
readonly ResourceDictionary addProperties;
readonly ResourceDictionary deleteProperties;
readonly ResourceDictionary noneProperties;
internal BrushesManager(IEditorFormatMap editorFormatMap)
{
addProperties = editorFormatMap.GetProperties(AddPropertiesKey);
deleteProperties = editorFormatMap.GetProperties(DeletePropertiesKey);
noneProperties = editorFormatMap.GetProperties(NonePropertiesKey);
}
internal Brush GetBackground(DiffChangeType diffChangeType)
{
switch (diffChangeType)
{
case DiffChangeType.Add:
return GetBackground(addProperties);
case DiffChangeType.Delete:
return GetBackground(deleteProperties);
case DiffChangeType.None:
default:
return GetBackground(noneProperties);
}
}
static Brush GetBackground(ResourceDictionary dictionary)
{
return dictionary["Background"] as Brush;
}
}
public UIElement GenerateGlyph(IWpfTextViewLine line, InlineCommentTag tag)
{
var glyph = CreateGlyph(tag);
glyph.MouseLeftButtonUp += (s, e) =>
{
if (OpenThreadView(tag)) e.Handled = true;
};
glyph.Background = brushesManager.GetBackground(tag.DiffChangeType);
return glyph;
}
public IEnumerable<Type> GetTagTypes()
{
return new[]
{
typeof(AddInlineCommentTag),
typeof(ShowInlineCommentTag)
};
}
static UserControl CreateGlyph(InlineCommentTag tag)
{
var addTag = tag as AddInlineCommentTag;
var showTag = tag as ShowInlineCommentTag;
if (addTag != null)
{
return new AddInlineCommentGlyph();
}
else if (showTag != null)
{
return new ShowInlineCommentGlyph()
{
Opacity = showTag.Thread.IsStale ? 0.5 : 1,
};
}
throw new ArgumentException($"Unknown 'InlineCommentTag' type '{tag}'");
}
bool OpenThreadView(InlineCommentTag tag)
{
var addTag = tag as AddInlineCommentTag;
var showTag = tag as ShowInlineCommentTag;
if (addTag != null)
{
peekService.Show(textView, addTag);
return true;
}
else if (showTag != null)
{
peekService.Show(textView, showTag);
return true;
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
using GitHub.Extensions;
using GitHub.Services;
using GitHub.Models;
using Microsoft.VisualStudio.Text.Tagging;
namespace GitHub.InlineReviews.Tags
{
/// <summary>
/// Base class for inline comment tags.
/// </summary>
/// <seealso cref="AddInlineCommentTag"/>
/// <seealso cref="ShowInlineCommentTag"/>
public abstract class InlineCommentTag : ITag
{
public InlineCommentTag(
IPullRequestSession session,
int lineNumber,
DiffChangeType diffChangeType)
{
Guard.ArgumentNotNull(session, nameof(session));
LineNumber = lineNumber;
Session = session;
DiffChangeType = diffChangeType;
}
public int LineNumber { get; }
public IPullRequestSession Session { get; }
public DiffChangeType DiffChangeType { get; }
}
}

Просмотреть файл

@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.InlineReviews.Services;
using GitHub.Models;
using GitHub.Services;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Text.Tagging;
using ReactiveUI;
using System.Collections;
namespace GitHub.InlineReviews.Tags
{
/// <summary>
/// Creates tags in an <see cref="ITextBuffer"/> for inline comment threads.
/// </summary>
sealed class InlineCommentTagger : ITagger<InlineCommentTag>, IEditorContentSource, IDisposable
{
readonly IGitService gitService;
readonly IGitClient gitClient;
readonly IDiffService diffService;
readonly ITextBuffer buffer;
readonly ITextView view;
readonly IPullRequestSessionManager sessionManager;
readonly IInlineCommentPeekService peekService;
readonly Subject<ITextSnapshot> signalRebuild;
readonly Dictionary<IInlineCommentThreadModel, ITrackingPoint> trackingPoints;
readonly int? tabsToSpaces;
bool initialized;
ITextDocument document;
string fullPath;
string relativePath;
bool leftHandSide;
IDisposable managerSubscription;
IDisposable sessionSubscription;
IPullRequestSession session;
IPullRequestSessionFile file;
public InlineCommentTagger(
IGitService gitService,
IGitClient gitClient,
IDiffService diffService,
ITextView view,
ITextBuffer buffer,
IPullRequestSessionManager sessionManager,
IInlineCommentPeekService peekService)
{
Guard.ArgumentNotNull(gitService, nameof(gitService));
Guard.ArgumentNotNull(gitClient, nameof(gitClient));
Guard.ArgumentNotNull(diffService, nameof(diffService));
Guard.ArgumentNotNull(buffer, nameof(buffer));
Guard.ArgumentNotNull(sessionManager, nameof(sessionManager));
Guard.ArgumentNotNull(peekService, nameof(peekService));
this.gitService = gitService;
this.gitClient = gitClient;
this.diffService = diffService;
this.buffer = buffer;
this.view = view;
this.sessionManager = sessionManager;
this.peekService = peekService;
trackingPoints = new Dictionary<IInlineCommentThreadModel, ITrackingPoint>();
if (view.Options.GetOptionValue("Tabs/ConvertTabsToSpaces", false))
{
tabsToSpaces = view.Options.GetOptionValue<int?>("Tabs/TabSize", null);
}
signalRebuild = new Subject<ITextSnapshot>();
signalRebuild.Throttle(TimeSpan.FromMilliseconds(500))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => Rebuild(x).Forget());
this.buffer.Changed += Buffer_Changed;
}
public bool ShowMargin => file != null;
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
public void Dispose()
{
sessionSubscription?.Dispose();
managerSubscription?.Dispose();
}
public IEnumerable<ITagSpan<InlineCommentTag>> GetTags(NormalizedSnapshotSpanCollection spans)
{
if (!initialized)
{
// Sucessful initialization will call NotifyTagsChanged, causing this method to be re-called.
Initialize();
}
else if (file != null)
{
foreach (var span in spans)
{
var startLine = span.Start.GetContainingLine().LineNumber;
var endLine = span.End.GetContainingLine().LineNumber;
var linesWithComments = new BitArray((endLine - startLine) + 1);
var spanThreads = file.InlineCommentThreads.Where(x =>
x.LineNumber >= startLine &&
x.LineNumber <= endLine);
foreach (var thread in spanThreads)
{
var snapshot = span.Snapshot;
var line = snapshot.GetLineFromLineNumber(thread.LineNumber);
if ((leftHandSide && thread.DiffLineType == DiffChangeType.Delete) ||
(!leftHandSide && thread.DiffLineType != DiffChangeType.Delete))
{
var trackingPoint = snapshot.CreateTrackingPoint(line.Start, PointTrackingMode.Positive);
trackingPoints[thread] = trackingPoint;
linesWithComments[thread.LineNumber - startLine] = true;
yield return new TagSpan<ShowInlineCommentTag>(
new SnapshotSpan(line.Start, line.End),
new ShowInlineCommentTag(session, thread));
}
}
foreach (var chunk in file.Diff)
{
foreach (var line in chunk.Lines)
{
var lineNumber = (leftHandSide ? line.OldLineNumber : line.NewLineNumber) - 1;
if (lineNumber >= startLine &&
lineNumber <= endLine &&
!linesWithComments[lineNumber - startLine]
&& (!leftHandSide || line.Type == DiffChangeType.Delete))
{
var snapshotLine = span.Snapshot.GetLineFromLineNumber(lineNumber);
yield return new TagSpan<InlineCommentTag>(
new SnapshotSpan(snapshotLine.Start, snapshotLine.End),
new AddInlineCommentTag(session, file.CommitSha, relativePath, line.DiffLineNumber, lineNumber, line.Type));
}
}
}
}
}
}
Task<byte[]> IEditorContentSource.GetContent()
{
return Task.FromResult(GetContents(buffer.CurrentSnapshot));
}
void Initialize()
{
document = buffer.Properties.GetProperty<ITextDocument>(typeof(ITextDocument));
if (document == null)
return;
var bufferInfo = sessionManager.GetTextBufferInfo(buffer);
IPullRequestSession session = null;
if (bufferInfo != null)
{
fullPath = bufferInfo.FilePath;
leftHandSide = bufferInfo.IsLeftComparisonBuffer;
if (!bufferInfo.Session.IsCheckedOut)
{
session = bufferInfo.Session;
}
}
else
{
fullPath = document.FilePath;
}
if (session == null)
{
managerSubscription = sessionManager.WhenAnyValue(x => x.CurrentSession).Subscribe(SessionChanged);
}
else
{
SessionChanged(session);
}
initialized = true;
}
void NotifyTagsChanged()
{
var entireFile = new SnapshotSpan(buffer.CurrentSnapshot, 0, buffer.CurrentSnapshot.Length);
TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(entireFile));
}
void NotifyTagsChanged(int lineNumber)
{
var line = buffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber);
var span = new SnapshotSpan(buffer.CurrentSnapshot, line.Start, line.Length);
TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(span));
}
async void SessionChanged(IPullRequestSession session)
{
sessionSubscription?.Dispose();
this.session = session;
if (file != null)
{
file = null;
NotifyTagsChanged();
}
if (session == null) return;
relativePath = session.GetRelativePath(fullPath);
if (relativePath == null) return;
var snapshot = buffer.CurrentSnapshot;
if (leftHandSide)
{
// If we're tagging the LHS of a diff, then the snapshot will be the base commit
// (as you'd expect) but that means that the diff will be empty, so get the RHS
// snapshot from the view for the comparison.
var projection = view.TextSnapshot as IProjectionSnapshot;
snapshot = projection?.SourceSnapshots.Count == 2 ? projection.SourceSnapshots[1] : null;
}
if (snapshot == null) return;
var repository = gitService.GetRepository(session.Repository.LocalPath);
file = await session.GetFile(relativePath, !leftHandSide ? this : null);
if (file == null) return;
sessionSubscription = file.WhenAnyValue(x => x.InlineCommentThreads)
.Subscribe(_ => NotifyTagsChanged());
NotifyTagsChanged();
}
void Buffer_Changed(object sender, TextContentChangedEventArgs e)
{
if (file != null)
{
var snapshot = buffer.CurrentSnapshot;
foreach (var thread in file.InlineCommentThreads)
{
ITrackingPoint trackingPoint;
if (trackingPoints.TryGetValue(thread, out trackingPoint))
{
var position = trackingPoint.GetPosition(snapshot);
var lineNumber = snapshot.GetLineNumberFromPosition(position);
if (lineNumber != thread.LineNumber)
{
thread.LineNumber = lineNumber;
thread.IsStale = true;
NotifyTagsChanged(thread.LineNumber);
}
}
}
signalRebuild.OnNext(buffer.CurrentSnapshot);
}
}
byte[] GetContents(ITextSnapshot snapshot)
{
var currentText = snapshot.GetText();
var content = document.Encoding.GetBytes(currentText);
var preamble = document.Encoding.GetPreamble();
if (preamble.Length == 0) return content;
var completeContent = new byte[preamble.Length + content.Length];
Buffer.BlockCopy(preamble, 0, completeContent, 0, preamble.Length);
Buffer.BlockCopy(content, 0, completeContent, preamble.Length, content.Length);
return completeContent;
}
async Task Rebuild(ITextSnapshot snapshot)
{
if (buffer.CurrentSnapshot == snapshot)
{
await session.UpdateEditorContent(relativePath);
foreach (var thread in file.InlineCommentThreads)
{
if (thread.LineNumber == -1)
{
trackingPoints.Remove(thread);
}
}
if (buffer.CurrentSnapshot == snapshot)
{
NotifyTagsChanged();
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,60 @@
using System;
using System.ComponentModel.Composition;
using GitHub.Extensions;
using GitHub.InlineReviews.Services;
using GitHub.Services;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
namespace GitHub.InlineReviews.Tags
{
/// <summary>
/// Factory class for <see cref="InlineCommentTagger"/>s.
/// </summary>
[Export(typeof(IViewTaggerProvider))]
[ContentType("text")]
[TagType(typeof(ShowInlineCommentTag))]
class InlineCommentTaggerProvider : IViewTaggerProvider
{
readonly IGitService gitService;
readonly IGitClient gitClient;
readonly IDiffService diffService;
readonly IPullRequestSessionManager sessionManager;
readonly IInlineCommentPeekService peekService;
[ImportingConstructor]
public InlineCommentTaggerProvider(
IGitService gitService,
IGitClient gitClient,
IDiffService diffService,
IPullRequestSessionManager sessionManager,
IInlineCommentPeekService peekService)
{
Guard.ArgumentNotNull(gitService, nameof(gitService));
Guard.ArgumentNotNull(gitClient, nameof(gitClient));
Guard.ArgumentNotNull(sessionManager, nameof(sessionManager));
Guard.ArgumentNotNull(peekService, nameof(peekService));
this.gitService = gitService;
this.gitClient = gitClient;
this.diffService = diffService;
this.sessionManager = sessionManager;
this.peekService = peekService;
}
public ITagger<T> CreateTagger<T>(ITextView view, ITextBuffer buffer) where T : ITag
{
return buffer.Properties.GetOrCreateSingletonProperty(()=>
new InlineCommentTagger(
gitService,
gitClient,
diffService,
view,
buffer,
sessionManager,
peekService)) as ITagger<T>;
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
<UserControl x:Class="GitHub.InlineReviews.Tags.ShowInlineCommentGlyph"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<Viewbox>
<Path Stroke="Black"
Data="M13 2H1c-0.55 0-1 0.45-1 1v8c0 0.55 0.45 1 1 1h2v3.5l3.5-3.5h6.5c0.55 0 1-0.45 1-1V3c0-0.55-0.45-1-1-1z m0 9H6L4 13V11H1V3h12v8z"/>
</Viewbox>
</UserControl>

Просмотреть файл

@ -0,0 +1,14 @@
using System;
using System.Windows.Controls;
namespace GitHub.InlineReviews.Tags
{
public partial class ShowInlineCommentGlyph : UserControl
{
public ShowInlineCommentGlyph()
{
InitializeComponent();
}
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше