зеркало из https://github.com/github/VisualStudio.git
Merge remote-tracking branch 'origin/master' into fixes/726-markdig
This commit is contained in:
Коммит
2113a98f0d
|
@ -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>
|
33
GitHubVS.sln
33
GitHubVS.sln
|
@ -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")]
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче