diff --git a/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs b/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs index cd282d81c7..aaf13bafef 100644 --- a/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs @@ -1,5 +1,7 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using GitHub.Models; using GitHub.ViewModels; using ReactiveUI; @@ -8,6 +10,16 @@ namespace GitHub.SampleData [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")] public class CommentThreadViewModelDesigner : ViewModelBase, ICommentThreadViewModel { + public CommentThreadViewModelDesigner() + { + Comments = new ReactiveList(){new CommentViewModelDesigner() + { + Author = new ActorViewModel{ Login = "shana"}, + Body = "You can use a `CompositeDisposable` type here, it's designed to handle disposables in an optimal way (you can just call `Dispose()` on it and it will handle disposing everything it holds)." + }}; + + } + public IReadOnlyReactiveList Comments { get; } = new ReactiveList(); diff --git a/src/GitHub.App/SampleData/InlineAnnotationViewModelDesigner.cs b/src/GitHub.App/SampleData/InlineAnnotationViewModelDesigner.cs new file mode 100644 index 0000000000..4aea77efb7 --- /dev/null +++ b/src/GitHub.App/SampleData/InlineAnnotationViewModelDesigner.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using GitHub.Models; +using GitHub.ViewModels; + +namespace GitHub.SampleData +{ + public class InlineAnnotationViewModelDesigner : IInlineAnnotationViewModel + { + public InlineAnnotationViewModelDesigner() + { + var checkRunAnnotationModel = new CheckRunAnnotationModel + { + AnnotationLevel = CheckAnnotationLevel.Failure, + Path = "SomeFile.cs", + EndLine = 12, + StartLine = 12, + Message = "Some Error Message", + Title = "CS12345" + }; + + var checkRunModel = + new CheckRunModel + { + Annotations = new List {checkRunAnnotationModel}, + Name = "Fake Check Run" + }; + + var checkSuiteModel = new CheckSuiteModel() + { + ApplicationName = "Fake Check Suite", + HeadSha = "ed6198c37b13638e902716252b0a17d54bd59e4a", + CheckRuns = new List { checkRunModel} + }; + + Model= new InlineAnnotationModel(checkSuiteModel, checkRunModel, checkRunAnnotationModel); + } + + public InlineAnnotationModel Model { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestAnnotationItemViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestAnnotationItemViewModelDesigner.cs index a2e670fd87..03f4e7d056 100644 --- a/src/GitHub.App/SampleData/PullRequestAnnotationItemViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestAnnotationItemViewModelDesigner.cs @@ -12,7 +12,7 @@ public sealed class PullRequestAnnotationItemViewModelDesigner : IPullRequestAnn public CheckRunAnnotationModel Annotation { get; set; } public bool IsExpanded { get; set; } public string LineDescription => $"{Annotation.StartLine}:{Annotation.EndLine}"; - public bool IsFileInPullRequest { get; } + public bool IsFileInPullRequest { get; set; } public ReactiveCommand OpenAnnotation { get; } } } \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestAnnotationsViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestAnnotationsViewModelDesigner.cs index 6dafbec312..8e728ee8f3 100644 --- a/src/GitHub.App/SampleData/PullRequestAnnotationsViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestAnnotationsViewModelDesigner.cs @@ -40,6 +40,7 @@ public sealed class PullRequestAnnotationsViewModelDesigner : PanePageViewModelB Title = "CS 12345" }, IsExpanded = true, + IsFileInPullRequest = true }, new PullRequestAnnotationItemViewModelDesigner { @@ -53,6 +54,7 @@ public sealed class PullRequestAnnotationsViewModelDesigner : PanePageViewModelB Title = "CS 12345" }, IsExpanded = true, + IsFileInPullRequest = true }, } }, diff --git a/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs index 263f2a8eff..ec75bf27be 100644 --- a/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs @@ -1,6 +1,5 @@ using System; using System.Reactive; -using System.Windows.Media.Imaging; using GitHub.Models; using GitHub.ViewModels; using GitHub.ViewModels.GitHubPane; diff --git a/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs index b60b43bc03..5dda31c86d 100644 --- a/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs @@ -36,6 +36,9 @@ public PullRequestFilesViewModelDesigner() public ReactiveCommand DiffFileWithWorkingDirectory { get; } public ReactiveCommand OpenFileInWorkingDirectory { get; } public ReactiveCommand OpenFirstComment { get; } + public ReactiveCommand OpenFirstAnnotationNotice { get; } + public ReactiveCommand OpenFirstAnnotationWarning { get; } + public ReactiveCommand OpenFirstAnnotationFailure { get; } public Task InitializeAsync( IPullRequestSession session, diff --git a/src/GitHub.App/Services/PullRequestEditorService.cs b/src/GitHub.App/Services/PullRequestEditorService.cs index aaf44c50ad..db4889e936 100644 --- a/src/GitHub.App/Services/PullRequestEditorService.cs +++ b/src/GitHub.App/Services/PullRequestEditorService.cs @@ -285,7 +285,7 @@ await pullRequestService.ExtractToTempFile( } /// - public async Task OpenDiff( + public Task OpenDiff( IPullRequestSession session, string relativePath, IInlineCommentThreadModel thread) @@ -294,11 +294,17 @@ public async Task OpenDiff( Guard.ArgumentNotEmptyString(relativePath, nameof(relativePath)); Guard.ArgumentNotNull(thread, nameof(thread)); - var diffViewer = await OpenDiff(session, relativePath, thread.CommitSha, scrollToFirstDraftOrDiff: false); + return OpenDiff(session, relativePath, thread.CommitSha, thread.LineNumber - 1); + } + + /// + public async Task OpenDiff(IPullRequestSession session, string relativePath, string headSha, int fromLine) + { + var diffViewer = await OpenDiff(session, relativePath, headSha, scrollToFirstDraftOrDiff: false); - var param = (object)new InlineCommentNavigationParams + var param = (object) new InlineCommentNavigationParams { - FromLine = thread.LineNumber - 1, + FromLine = fromLine, }; // HACK: We need to wait here for the inline comment tags to initialize so we can find the next inline comment. diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationItemViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationItemViewModel.cs index 5d5f4250e2..d07c806f89 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationItemViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationItemViewModel.cs @@ -1,5 +1,7 @@ using System.Reactive; +using System.Reactive.Linq; using GitHub.Models; +using GitHub.Services; using ReactiveUI; namespace GitHub.ViewModels.GitHubPane @@ -14,12 +16,22 @@ public class PullRequestAnnotationItemViewModel : ViewModelBase, IPullRequestAnn /// /// The check run annotation model. /// A flag that denotes if the annotation is part of the pull request's changes. - public PullRequestAnnotationItemViewModel(CheckRunAnnotationModel annotation, bool isFileInPullRequest) + /// The check suite model. + /// The pull request session. + /// The pull request editor service. + public PullRequestAnnotationItemViewModel( + CheckRunAnnotationModel annotation, + bool isFileInPullRequest, + CheckSuiteModel checkSuite, + IPullRequestSession session, + IPullRequestEditorService editorService) { Annotation = annotation; IsFileInPullRequest = isFileInPullRequest; - OpenAnnotation = ReactiveCommand.Create(() => { }); + OpenAnnotation = ReactiveCommand.CreateFromTask( + async _ => await editorService.OpenDiff(session, annotation.Path, checkSuite.HeadSha, annotation.EndLine - 1), + Observable.Return(IsFileInPullRequest)); } /// diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationsViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationsViewModel.cs index c638271423..8b74b94ca5 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationsViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationsViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; @@ -15,7 +15,8 @@ namespace GitHub.ViewModels.GitHubPane [PartCreationPolicy(CreationPolicy.NonShared)] public class PullRequestAnnotationsViewModel : PanePageViewModelBase, IPullRequestAnnotationsViewModel { - private readonly IPullRequestSessionManager sessionManager; + readonly IPullRequestSessionManager sessionManager; + readonly IPullRequestEditorService pullRequestEditorService; IPullRequestSession session; string title; @@ -29,10 +30,12 @@ public class PullRequestAnnotationsViewModel : PanePageViewModelBase, IPullReque /// Initializes a new instance of the class. /// /// The pull request session manager. + /// The pull request editor service. [ImportingConstructor] - public PullRequestAnnotationsViewModel(IPullRequestSessionManager sessionManager) + public PullRequestAnnotationsViewModel(IPullRequestSessionManager sessionManager, IPullRequestEditorService pullRequestEditorService) { this.sessionManager = sessionManager; + this.pullRequestEditorService = pullRequestEditorService; NavigateToPullRequest = ReactiveCommand.Create(() => { NavigateTo(FormattableString.Invariant( $"{LocalRepository.Owner}/{LocalRepository.Name}/pull/{PullRequestNumber}")); @@ -151,7 +154,7 @@ void Load(PullRequestDetailModel pullRequest) .ToDictionary( path => path, path => annotationsLookup[path] - .Select(annotation => new PullRequestAnnotationItemViewModel(annotation, changedFiles.Contains(path))) + .Select(annotation => new PullRequestAnnotationItemViewModel(annotation, changedFiles.Contains(path), checkSuiteRun.checkSuite, session, pullRequestEditorService)) .Cast() .ToArray() ); diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs index b14cc5b233..0f0ab9224c 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs @@ -7,6 +7,7 @@ using GitHub.Extensions; using GitHub.Factories; using GitHub.Models; +using GitHub.Primitives; using GitHub.Services; using ReactiveUI; @@ -17,6 +18,8 @@ namespace GitHub.ViewModels.GitHubPane [PartCreationPolicy(CreationPolicy.NonShared)] public class PullRequestCheckViewModel: ViewModelBase, IPullRequestCheckViewModel { + const string DefaultAvatar = "pack://application:,,,/GitHub.App;component/Images/default_user_avatar.png"; + private readonly IUsageTracker usageTracker; /// @@ -56,11 +59,14 @@ public static IEnumerable Build(IViewViewModelFactor return pullRequestCheckViewModel; }) ?? Array.Empty(); - var checks = pullRequest.CheckSuites?.SelectMany(checkSuiteModel => checkSuiteModel.CheckRuns) - .Select(checkRunModel => + var checks = + pullRequest.CheckSuites? + .SelectMany(checkSuite => checkSuite.CheckRuns + .Select(checkRun => new { checkSuiteModel = checkSuite, checkRun})) + .Select(arg => { PullRequestCheckStatus checkStatus; - switch (checkRunModel.Status) + switch (arg.checkRun.Status) { case CheckStatusState.Requested: case CheckStatusState.Queued: @@ -69,7 +75,7 @@ public static IEnumerable Build(IViewViewModelFactor break; case CheckStatusState.Completed: - switch (checkRunModel.Conclusion) + switch (arg.checkRun.Conclusion) { case CheckConclusionState.Success: checkStatus = PullRequestCheckStatus.Success; @@ -94,13 +100,12 @@ public static IEnumerable Build(IViewViewModelFactor var pullRequestCheckViewModel = (PullRequestCheckViewModel)viewViewModelFactory.CreateViewModel(); pullRequestCheckViewModel.CheckType = PullRequestCheckType.ChecksApi; - pullRequestCheckViewModel.CheckRunId = checkRunModel.Id; - pullRequestCheckViewModel.HasAnnotations = checkRunModel.Annotations?.Any() ?? false; - pullRequestCheckViewModel.Title = checkRunModel.Name; - pullRequestCheckViewModel.Description = checkRunModel.Summary; + pullRequestCheckViewModel.CheckRunId = arg.checkRun.Id; + pullRequestCheckViewModel.HasAnnotations = arg.checkRun.Annotations?.Any() ?? false; + pullRequestCheckViewModel.Title = arg.checkRun.Name; + pullRequestCheckViewModel.Description = arg.checkRun.Summary; pullRequestCheckViewModel.Status = checkStatus; - pullRequestCheckViewModel.DetailsUrl = new Uri(checkRunModel.DetailsUrl); - + pullRequestCheckViewModel.DetailsUrl = new Uri(arg.checkRun.DetailsUrl); return pullRequestCheckViewModel; }) ?? Array.Empty(); diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs index ca5fa050aa..6f7ef97e54 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs @@ -65,6 +65,26 @@ public PullRequestFilesViewModel( await editorService.OpenDiff(pullRequestSession, file.RelativePath, thread); } }); + + OpenFirstAnnotationNotice = ReactiveCommand.CreateFromTask( + async file => await OpenFirstAnnotation(editorService, file, CheckAnnotationLevel.Notice)); + + OpenFirstAnnotationWarning = ReactiveCommand.CreateFromTask( + async file => await OpenFirstAnnotation(editorService, file, CheckAnnotationLevel.Warning)); + + OpenFirstAnnotationFailure = ReactiveCommand.CreateFromTask( + async file => await OpenFirstAnnotation(editorService, file, CheckAnnotationLevel.Failure)); + } + + private async Task OpenFirstAnnotation(IPullRequestEditorService editorService, IPullRequestFileNode file, + CheckAnnotationLevel checkAnnotationLevel) + { + var annotationModel = await GetFirstAnnotation(file, checkAnnotationLevel); + + if (annotationModel != null) + { + await editorService.OpenDiff(pullRequestSession, file.RelativePath, annotationModel.HeadSha, annotationModel.EndLine); + } } /// @@ -160,6 +180,15 @@ public async Task InitializeAsync( /// public ReactiveCommand OpenFirstComment { get; } + /// + public ReactiveCommand OpenFirstAnnotationNotice { get; } + + /// + public ReactiveCommand OpenFirstAnnotationWarning { get; } + + /// + public ReactiveCommand OpenFirstAnnotationFailure { get; } + static int CountComments( IEnumerable thread, Func commentFilter) @@ -212,6 +241,15 @@ async Task GetFirstCommentThread(IPullRequestFileNode return threads.FirstOrDefault(); } + async Task GetFirstAnnotation(IPullRequestFileNode file, + CheckAnnotationLevel annotationLevel) + { + var sessionFile = await pullRequestSession.GetFile(file.RelativePath); + var annotations = sessionFile.InlineAnnotations; + + return annotations.FirstOrDefault(model => model.AnnotationLevel == annotationLevel); + } + /// /// Implements the command. /// diff --git a/src/GitHub.App/ViewModels/InlineAnnotationViewModel.cs b/src/GitHub.App/ViewModels/InlineAnnotationViewModel.cs new file mode 100644 index 0000000000..c395a9abef --- /dev/null +++ b/src/GitHub.App/ViewModels/InlineAnnotationViewModel.cs @@ -0,0 +1,21 @@ +using GitHub.Models; +using GitHub.ViewModels; + +namespace GitHub.ViewModels +{ + /// + public class InlineAnnotationViewModel: IInlineAnnotationViewModel + { + /// + public InlineAnnotationModel Model { get; } + + /// + /// Initializes a . + /// + /// The inline annotation model. + public InlineAnnotationViewModel(InlineAnnotationModel model) + { + Model = model; + } + } +} \ No newline at end of file diff --git a/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs b/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs index 43b0d299ed..c821754f8b 100644 --- a/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs +++ b/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.Composition; using System.Globalization; using System.Linq; @@ -75,8 +76,7 @@ public bool IsNewThread public bool NeedsPush => needsPush.Value; /// - public async Task InitializeAsync( - IPullRequestSession session, + public async Task InitializeAsync(IPullRequestSession session, IPullRequestSessionFile file, IInlineCommentThreadModel thread, bool addPlaceholder) diff --git a/src/GitHub.Exports.Reactive/Models/InlineAnnotationModel.cs b/src/GitHub.Exports.Reactive/Models/InlineAnnotationModel.cs index 2774aad4ce..c102614014 100644 --- a/src/GitHub.Exports.Reactive/Models/InlineAnnotationModel.cs +++ b/src/GitHub.Exports.Reactive/Models/InlineAnnotationModel.cs @@ -7,21 +7,34 @@ namespace GitHub.Models /// public class InlineAnnotationModel { - private CheckRunModel checkRun; - private CheckRunAnnotationModel annotation; + readonly CheckSuiteModel checkSuite; + readonly CheckRunModel checkRun; + readonly CheckRunAnnotationModel annotation; /// /// Initializes the . /// + /// The check suite model. /// The check run model. /// The annotation model. - public InlineAnnotationModel(CheckRunModel checkRun, CheckRunAnnotationModel annotation) + public InlineAnnotationModel(CheckSuiteModel checkSuite, CheckRunModel checkRun, + CheckRunAnnotationModel annotation) { + Guard.ArgumentNotNull(checkRun, nameof(checkRun)); + Guard.ArgumentNotNull(annotation, nameof(annotation)); Guard.ArgumentNotNull(annotation.AnnotationLevel, nameof(annotation.AnnotationLevel)); + this.checkSuite = checkSuite; this.checkRun = checkRun; this.annotation = annotation; } + + /// + /// Gets the annotation path. + /// + public string Path => annotation.Path; + + /// /// Gets the start line of the annotation. /// public int StartLine => annotation.StartLine; @@ -35,5 +48,35 @@ public InlineAnnotationModel(CheckRunModel checkRun, CheckRunAnnotationModel ann /// Gets the annotation level. /// public CheckAnnotationLevel AnnotationLevel => annotation.AnnotationLevel; + + /// + /// Gets the name of the check suite. + /// + public string CheckSuiteName => checkSuite.ApplicationName; + + /// + /// Gets the name of the check run. + /// + public string CheckRunName => checkRun.Name; + + /// + /// Gets the annotation title. + /// + public string Title => annotation.Title; + + /// + /// Gets the annotation message. + /// + public string Message => annotation.Message; + + /// + /// Gets the sha the check run was created on. + /// + public string HeadSha => checkSuite.HeadSha; + + /// + /// Gets the a descriptor for the line(s) reported. + /// + public string LineDescription => $"{StartLine}:{EndLine}"; } } \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs b/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs index ec5e274422..954f0a6e34 100644 --- a/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs +++ b/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs @@ -37,8 +37,8 @@ public interface IPullRequestEditorService Task OpenDiff(IPullRequestSession session, string relativePath, string headSha = null, bool scrollToFirstDiff = true); /// - /// Opens an diff viewer for a file in a pull request with the specified inline comment - /// thread open. + /// Opens an diff viewer for a file in a pull request with the specified inline review + /// comment thread open. /// /// The pull request session. /// The path to the file, relative to the repository. @@ -46,6 +46,19 @@ public interface IPullRequestEditorService /// The opened diff viewer. Task OpenDiff(IPullRequestSession session, string relativePath, IInlineCommentThreadModel thread); + /// + /// Opens an diff viewer for a file in a pull request with the specified inline review line open. + /// + /// The pull request session. + /// The path to the file, relative to the repository. + /// + /// The commit SHA of the right hand side of the diff. Pass null to compare with the + /// working directory, or "HEAD" to compare with the HEAD commit of the pull request. + /// + /// The line number to open + /// The opened diff viewer. + Task OpenDiff(IPullRequestSession session, string relativePath, string headSha, int fromLine); + /// /// Find the active text view. /// diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs index 15d6a6f491..d24a9c7212 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Reactive; -using System.Windows.Media.Imaging; using GitHub.Models; using ReactiveUI; diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs index 7f2eda69c8..ebef31bf39 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs @@ -51,6 +51,24 @@ public interface IPullRequestFilesViewModel : IViewModel, IDisposable /// ReactiveCommand OpenFirstComment { get; } + /// + /// Gets a command that opens the first annotation notice for a in + /// the diff viewer. + /// + ReactiveCommand OpenFirstAnnotationNotice { get; } + + /// + /// Gets a command that opens the first annotation warning for a in + /// the diff viewer. + /// + ReactiveCommand OpenFirstAnnotationWarning { get; } + + /// + /// Gets a command that opens the first annotation failure for a in + /// the diff viewer. + /// + ReactiveCommand OpenFirstAnnotationFailure { get; } + /// /// Initializes the view model. /// diff --git a/src/GitHub.Exports.Reactive/ViewModels/IInlineAnnotationViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/IInlineAnnotationViewModel.cs new file mode 100644 index 0000000000..1ed533c306 --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/IInlineAnnotationViewModel.cs @@ -0,0 +1,15 @@ +using GitHub.Models; + +namespace GitHub.ViewModels +{ + /// + /// A view model that represents a single inline annotation. + /// + public interface IInlineAnnotationViewModel + { + /// + /// Gets the inline annotation model. + /// + InlineAnnotationModel Model { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs index 0a74e78b56..0afeb7ba12 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using GitHub.Models; using GitHub.Services; @@ -48,10 +49,9 @@ public interface IPullRequestReviewCommentThreadViewModel : ICommentThreadViewMo /// The file that the comment is on. /// The thread. /// - /// Whether to add a placeholder comment at the end of the thread. + /// Whether to add a placeholder comment at the end of the thread. /// - Task InitializeAsync( - IPullRequestSession session, + Task InitializeAsync(IPullRequestSession session, IPullRequestSessionFile file, IInlineCommentThreadModel thread, bool addPlaceholder); @@ -64,8 +64,7 @@ Task InitializeAsync( /// The 0-based line number of the thread. /// The side of the diff. /// Whether to start the placeholder in edit state. - Task InitializeNewAsync( - IPullRequestSession session, + Task InitializeNewAsync(IPullRequestSession session, IPullRequestSessionFile file, int lineNumber, DiffSide side, diff --git a/src/GitHub.Exports/Models/CheckRunModel.cs b/src/GitHub.Exports/Models/CheckRunModel.cs index c66f22b61c..de6ec46d9d 100644 --- a/src/GitHub.Exports/Models/CheckRunModel.cs +++ b/src/GitHub.Exports/Models/CheckRunModel.cs @@ -32,7 +32,7 @@ public class CheckRunModel /// The check run's annotations. /// public List Annotations { get; set; } - + /// /// The name of the check for this check run. /// diff --git a/src/GitHub.Exports/Models/CheckSuiteModel.cs b/src/GitHub.Exports/Models/CheckSuiteModel.cs index cf5249a728..43ad354910 100644 --- a/src/GitHub.Exports/Models/CheckSuiteModel.cs +++ b/src/GitHub.Exports/Models/CheckSuiteModel.cs @@ -8,6 +8,11 @@ namespace GitHub.Models /// public class CheckSuiteModel { + /// + /// The head sha of a Check Suite. + /// + public string HeadSha { get; set; } + /// /// The check runs associated with a check suite. /// diff --git a/src/GitHub.Exports/Settings/generated/IPackageSettings.cs b/src/GitHub.Exports/Settings/generated/IPackageSettings.cs index fa8d50b008..aec212bed9 100644 --- a/src/GitHub.Exports/Settings/generated/IPackageSettings.cs +++ b/src/GitHub.Exports/Settings/generated/IPackageSettings.cs @@ -1,4 +1,4 @@ -// This is an automatically generated file, based on settings.json and PackageSettingsGen.tt +// This is an automatically generated file, based on settings.json and PackageSettingsGen.tt /* settings.json content: { "settings": [ @@ -51,4 +51,4 @@ public interface IPackageSettings : INotifyPropertyChanged bool HideTeamExplorerWelcomeMessage { get; set; } bool EnableTraceLogging { get; set; } } -} +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs b/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs index 42ea92d69a..e6b4c35f50 100644 --- a/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs +++ b/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs @@ -53,7 +53,7 @@ public override Task Execute(InlineCommentNavigationParams parameter) if (tags.Count > 0) { var cursorPoint = GetCursorPoint(textViews[0], parameter); - var next = tags.FirstOrDefault(x => x.Point > cursorPoint) ?? tags.First(); + var next = tags.FirstOrDefault(x => x.Point >= cursorPoint) ?? tags.First(); ShowPeekComments(parameter, next.TextView, next.Tag, textViews); } diff --git a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj index a8874536a9..6938de120a 100644 --- a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj +++ b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj @@ -441,9 +441,7 @@ PreserveNewest - - - + diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs index d51ea98d62..5043b96d8c 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; @@ -101,7 +101,7 @@ public IReadOnlyList BuildAnnotations( .SelectMany(arg => arg.checkRun.Annotations .Where(annotation => annotation.Path == relativePath) - .Select(annotation => new InlineAnnotationModel(arg.checkRun, annotation))) + .Select(annotation => new InlineAnnotationModel(arg.checkSuite, arg.checkRun, annotation))) .OrderBy(tuple => tuple.StartLine) .ToArray(); } @@ -366,6 +366,10 @@ public virtual async Task ReadPullRequestDetail(HostAddr result.Statuses = lastCommitModel.Statuses; result.CheckSuites = lastCommitModel.CheckSuites; + foreach (var checkSuite in result.CheckSuites) + { + checkSuite.HeadSha = lastCommitModel.HeadSha; + } result.ChangedFiles = files.Select(file => new PullRequestFileModel { @@ -773,6 +777,7 @@ async Task GetPullRequestLastCommitAdapter(HostAddress addres .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( commit => new LastCommitAdapter { + HeadSha = commit.Commit.Oid, CheckSuites = commit.Commit.CheckSuites(null, null, null, null, null).AllPages(10) .Select(suite => new CheckSuiteModel { @@ -806,7 +811,7 @@ async Task GetPullRequestLastCommitAdapter(HostAddress addres State = statusContext.State.FromGraphQl(), Context = statusContext.Context, TargetUrl = statusContext.TargetUrl, - Description = statusContext.Description, + Description = statusContext.Description }).ToList() ).SingleOrDefault() } @@ -943,6 +948,8 @@ class LastCommitAdapter public List CheckSuites { get; set; } public List Statuses { get; set; } + + public string HeadSha { get; set; } } } } diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs b/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs index 6d5c4ac613..f43c3265ea 100644 --- a/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs +++ b/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs @@ -56,12 +56,10 @@ static UserControl CreateGlyph(InlineCommentTag tag) { return new AddInlineCommentGlyph(); } - else if (showTag != null) + + if (showTag != null) { - return new ShowInlineCommentGlyph() - { - Opacity = showTag.Thread.IsStale ? 0.5 : 1, - }; + return new ShowInlineCommentGlyph(); } throw new ArgumentException($"Unknown 'InlineCommentTag' type '{tag}'"); diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs index 3891003d63..d3e5d651b3 100644 --- a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs +++ b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs @@ -84,24 +84,58 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanCol { var startLine = span.Start.GetContainingLine().LineNumber; var endLine = span.End.GetContainingLine().LineNumber; - var linesWithComments = new BitArray((endLine - startLine) + 1); + var linesWithTags = new BitArray((endLine - startLine) + 1); var spanThreads = file.InlineCommentThreads.Where(x => x.LineNumber >= startLine && - x.LineNumber <= endLine); + x.LineNumber <= endLine) + .ToArray(); - foreach (var thread in spanThreads) + var spanThreadsByLine = spanThreads.ToDictionary(model => model.LineNumber); + + Dictionary spanAnnotationsByLine = null; + if (side == DiffSide.Right) + { + var spanAnnotations = file.InlineAnnotations.Where(x => + x.EndLine - 1 >= startLine && + x.EndLine - 1 <= endLine); + + spanAnnotationsByLine = spanAnnotations + .GroupBy(model => model.EndLine) + .ToDictionary(models => models.Key - 1, models => models.ToArray()); + } + + var lines = spanThreadsByLine.Keys.Union(spanAnnotationsByLine?.Keys ?? Enumerable.Empty()); + foreach (var line in lines) { var snapshot = span.Snapshot; - var line = snapshot.GetLineFromLineNumber(thread.LineNumber); + var snapshotLine = snapshot.GetLineFromLineNumber(line); - if ((side == DiffSide.Left && thread.DiffLineType == DiffChangeType.Delete) || - (side == DiffSide.Right && thread.DiffLineType != DiffChangeType.Delete)) + if (spanThreadsByLine.TryGetValue(line, out var thread)) { - linesWithComments[thread.LineNumber - startLine] = true; + var isThreadDeleteSide = thread.DiffLineType == DiffChangeType.Delete; + var sidesMatch = side == DiffSide.Left && isThreadDeleteSide || side == DiffSide.Right && !isThreadDeleteSide; + if (!sidesMatch) + { + thread = null; + } + } + + InlineAnnotationModel[] annotations = null; + spanAnnotationsByLine?.TryGetValue(line, out annotations); + + if (thread != null || annotations != null) + { + linesWithTags[line - startLine] = true; + + var showInlineTag = new ShowInlineCommentTag(currentSession, line, thread?.DiffLineType ?? DiffChangeType.Add) + { + Thread = thread, + Annotations = annotations + }; result.Add(new TagSpan( - new SnapshotSpan(line.Start, line.End), - new ShowInlineCommentTag(currentSession, thread))); + new SnapshotSpan(snapshotLine.Start, snapshotLine.End), + showInlineTag)); } } @@ -113,7 +147,7 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanCol if (lineNumber >= startLine && lineNumber <= endLine && - !linesWithComments[lineNumber - startLine] + !linesWithTags[lineNumber - startLine] && (side == DiffSide.Right || line.Type == DiffChangeType.Delete)) { var snapshotLine = span.Snapshot.GetLineFromLineNumber(lineNumber); diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml index 77e7386777..1e0add5448 100644 --- a/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml +++ b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml @@ -9,16 +9,25 @@ - - - + + + + + + + diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs b/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs index b1071754cf..ab72d05db3 100644 --- a/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs +++ b/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GitHub.Extensions; using GitHub.Models; using GitHub.Services; @@ -14,20 +15,21 @@ public class ShowInlineCommentTag : InlineCommentTag /// Initializes a new instance of the class. /// /// The pull request session. - /// A model holding the details of the thread. - public ShowInlineCommentTag( - IPullRequestSession session, - IInlineCommentThreadModel thread) - : base(session, thread.LineNumber, thread.DiffLineType) + /// 0-based index of the inline tag + /// The diff type for the inline comment + public ShowInlineCommentTag(IPullRequestSession session, int lineNumber, DiffChangeType diffLineType) + : base(session, lineNumber, diffLineType) { - Guard.ArgumentNotNull(thread, nameof(thread)); - - Thread = thread; } /// /// Gets a model holding details of the thread at the tagged line. /// - public IInlineCommentThreadModel Thread { get; } + public IInlineCommentThreadModel Thread { get; set; } + + /// + /// Gets a list of models holding details of the annotations at the tagged line. + /// + public IReadOnlyList Annotations { get; set; } } } diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml new file mode 100644 index 0000000000..cf5726b99d --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml.cs b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml.cs new file mode 100644 index 0000000000..e8b5ac4a68 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml.cs @@ -0,0 +1,14 @@ +using System; +using System.Windows.Controls; + +namespace GitHub.InlineReviews.Tags +{ + public partial class ShowInlineGlyph : UserControl + { + public ShowInlineGlyph() + { + InitializeComponent(); + } + + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs index efaf81971d..50431ece39 100644 --- a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs @@ -33,12 +33,14 @@ public sealed class InlineCommentPeekViewModel : ReactiveObject, IDisposable IPullRequestSession session; IPullRequestSessionFile file; IPullRequestReviewCommentThreadViewModel thread; + IReadOnlyList annotations; IDisposable fileSubscription; IDisposable sessionSubscription; IDisposable threadSubscription; ITrackingPoint triggerPoint; string relativePath; DiffSide side; + bool availableForComment; /// /// Initializes a new instance of the class. @@ -86,6 +88,21 @@ public InlineCommentPeekViewModel(IInlineCommentPeekService peekService, Observable.Return(previousCommentCommand.Enabled)); } + public bool AvailableForComment + { + get { return availableForComment; } + private set { this.RaiseAndSetIfChanged(ref availableForComment, value); } + } + + /// + /// Gets the annotations displayed. + /// + public IReadOnlyList Annotations + { + get { return annotations; } + private set { this.RaiseAndSetIfChanged(ref annotations, value); } + } + /// /// Gets the thread of comments to display. /// @@ -168,27 +185,39 @@ async Task UpdateThread() Thread = null; threadSubscription?.Dispose(); + Annotations = null; + if (file == null) return; var lineAndLeftBuffer = peekService.GetLineNumber(peekSession, triggerPoint); var lineNumber = lineAndLeftBuffer.Item1; var leftBuffer = lineAndLeftBuffer.Item2; + + AvailableForComment = + file.Diff.Any(chunk => chunk.Lines + .Any(line => line.NewLineNumber == lineNumber)); + var thread = file.InlineCommentThreads?.FirstOrDefault(x => x.LineNumber == lineNumber && ((leftBuffer && x.DiffLineType == DiffChangeType.Delete) || (!leftBuffer && x.DiffLineType != DiffChangeType.Delete))); - var vm = factory.CreateViewModel(); + + Annotations = file.InlineAnnotations?.Where(model => model.EndLine - 1 == lineNumber) + .Select(model => new InlineAnnotationViewModel(model)) + .ToArray(); + + var threadModel = factory.CreateViewModel(); if (thread?.Comments.Count > 0) { - await vm.InitializeAsync(session, file, thread, true); + await threadModel.InitializeAsync(session, file, thread, true); } else { - await vm.InitializeNewAsync(session, file, lineNumber, side, true); + await threadModel.InitializeNewAsync(session, file, lineNumber, side, true); } - Thread = vm; + Thread = threadModel; } async Task SessionChanged(IPullRequestSession pullRequestSession) diff --git a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml index bab63611b7..86dbe2cc8f 100644 --- a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml +++ b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml @@ -104,9 +104,19 @@ - - - + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs index 77e1511a6d..10cc2c5e06 100644 --- a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs +++ b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs @@ -15,17 +15,19 @@ public InlineCommentPeekView() InitializeComponent(); desiredHeight = new Subject(); - threadView.LayoutUpdated += ThreadViewLayoutUpdated; + threadView.LayoutUpdated += ChildLayoutUpdated; + annotationsView.LayoutUpdated += ChildLayoutUpdated; threadScroller.PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll; } public IObservable DesiredHeight => desiredHeight; - void ThreadViewLayoutUpdated(object sender, EventArgs e) + void ChildLayoutUpdated(object sender, EventArgs e) { var otherControlsHeight = ActualHeight - threadScroller.ActualHeight; var threadViewHeight = threadView.DesiredSize.Height + threadView.Margin.Top + threadView.Margin.Bottom; - desiredHeight.OnNext(threadViewHeight + otherControlsHeight); + var annotationsViewHeight = annotationsView.DesiredSize.Height + annotationsView.Margin.Top + annotationsView.Margin.Bottom; + desiredHeight.OnNext(threadViewHeight + annotationsViewHeight + otherControlsHeight); } } } diff --git a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml index 94cf0504cc..ec7257d64b 100644 --- a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml @@ -13,8 +13,9 @@ - + + diff --git a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs index 4fb4fc78d5..f92c491604 100644 --- a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs +++ b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs @@ -9,7 +9,6 @@ public partial class CommentThreadView : UserControl public CommentThreadView() { InitializeComponent(); - PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll; } } } diff --git a/src/GitHub.VisualStudio.UI/Views/CommentView.xaml b/src/GitHub.VisualStudio.UI/Views/CommentView.xaml index 527b36c50f..4a997d1223 100644 --- a/src/GitHub.VisualStudio.UI/Views/CommentView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/CommentView.xaml @@ -37,80 +37,97 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -149,7 +166,7 @@ AcceptsReturn="True" AcceptsTab="True" IsReadOnly="{Binding IsReadOnly}" - Margin="4 0" + Margin="4 0 4 4" Text="{Binding Body, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" VerticalAlignment="Center" diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml index ace105f941..f31c2bb315 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml @@ -64,13 +64,13 @@ - - + diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml index 38613a4bb2..d67839c532 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml @@ -24,12 +24,10 @@ - - - + - - + + @@ -39,21 +37,20 @@ - - diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml index 50119e213a..cdfdae874e 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml @@ -252,7 +252,7 @@ - + diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml index 4eb819d1f1..806e58d2a1 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml @@ -109,9 +109,12 @@ - - + + + @@ -124,9 +127,12 @@ - - + + + @@ -139,9 +145,12 @@ - - + + + diff --git a/src/GitHub.VisualStudio.UI/Views/InlineAnnotationView.xaml b/src/GitHub.VisualStudio.UI/Views/InlineAnnotationView.xaml new file mode 100644 index 0000000000..ce9ab86bde --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/InlineAnnotationView.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/Views/InlineAnnotationView.xaml.cs b/src/GitHub.VisualStudio.UI/Views/InlineAnnotationView.xaml.cs new file mode 100644 index 0000000000..46bb968391 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/InlineAnnotationView.xaml.cs @@ -0,0 +1,14 @@ +using System; +using System.Windows.Controls; +using GitHub.VisualStudio.UI.Helpers; + +namespace GitHub.VisualStudio.Views +{ + public partial class InlineAnnotationView : UserControl + { + public InlineAnnotationView() + { + InitializeComponent(); + } + } +} diff --git a/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs b/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs index 01f71a7626..a16c6d643c 100644 --- a/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs @@ -21,10 +21,11 @@ public class WithTextBufferInfo [Test] public void FirstPassShouldReturnEmptyTags() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager(DiffSide.Right)); + CreateSessionManager(file, DiffSide.Right)); var result = target.GetTags(CreateSpan(10)); @@ -32,12 +33,13 @@ public void FirstPassShouldReturnEmptyTags() } [Test] - public void ShouldReturnShowCommentTagForRhs() + public void ShouldReturnShowInlineTagForRhs() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager(DiffSide.Right)); + CreateSessionManager(file, DiffSide.Right)); // Line 10 has an existing RHS comment. var span = CreateSpan(10); @@ -48,13 +50,32 @@ public void ShouldReturnShowCommentTagForRhs() Assert.That(result[0].Tag, Is.InstanceOf()); } + [Test] + public void ShouldReturnShowAnnotationTagForRhs() + { + var file = CreateSessionFile(withComments: false, withAnnotations:true); + var target = new InlineCommentTagger( + Substitute.For(), + Substitute.For(), + CreateSessionManager(file, DiffSide.Right)); + + // Line 10 has an existing Annotation comment. + var span = CreateSpan(10); + var firstPass = target.GetTags(span); + var result = target.GetTags(span).ToList(); + + Assert.That(result, Has.One.Items); + Assert.That(result[0].Tag, Is.InstanceOf()); + } + [Test] public void ShouldReturnAddNewCommentTagForAddedLineOnRhs() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager(DiffSide.Right)); + CreateSessionManager(file, DiffSide.Right)); // Line 11 has an add diff entry. var span = CreateSpan(11); @@ -68,10 +89,11 @@ public void ShouldReturnAddNewCommentTagForAddedLineOnRhs() [Test] public void ShouldNotReturnAddNewCommentTagForDeletedLineOnRhs() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager(DiffSide.Right)); + CreateSessionManager(file, DiffSide.Right)); // Line 13 has an delete diff entry. var span = CreateSpan(13); @@ -82,12 +104,13 @@ public void ShouldNotReturnAddNewCommentTagForDeletedLineOnRhs() } [Test] - public void ShouldReturnShowCommentTagForLhs() + public void ShouldReturnShowInlineTagForLhs() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager(DiffSide.Left)); + CreateSessionManager(file, DiffSide.Left)); // Line 12 has an existing LHS comment. var span = CreateSpan(12); @@ -101,10 +124,11 @@ public void ShouldReturnShowCommentTagForLhs() [Test] public void ShouldReturnAddCommentTagForLhs() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager(DiffSide.Left)); + CreateSessionManager(file, DiffSide.Left)); // Line 13 has an delete diff entry. var span = CreateSpan(13); @@ -182,46 +206,6 @@ public void ShouldAlwaysCallSessionGetFileWithHeadCommitShaForLeftHandSide() session.Received(1).GetFile("file.cs", "HEAD"); } - static IPullRequestSessionFile CreateSessionFile() - { - var diffChunk = new DiffChunk - { - Lines = - { - // Line numbers here are 1-based. There is an add diff entry on line 11 - // and a delete entry on line 13. - new DiffLine { Type = DiffChangeType.Add, NewLineNumber = 11 + 1 }, - new DiffLine { Type = DiffChangeType.Delete, OldLineNumber = 13 + 1 }, - } - }; - var diff = new List { diffChunk }; - - var rhsThread = Substitute.For(); - rhsThread.DiffLineType.Returns(DiffChangeType.Add); - rhsThread.LineNumber.Returns(10); - - var lhsThread = Substitute.For(); - lhsThread.DiffLineType.Returns(DiffChangeType.Delete); - lhsThread.LineNumber.Returns(12); - - // We have a comment to display on the right-hand-side of the diff view on line - // 11 and a comment to display on line 13 on the left-hand-side. - var threads = new List { rhsThread, lhsThread }; - - var file = Substitute.For(); - file.Diff.Returns(diff); - file.InlineCommentThreads.Returns(threads); - file.LinesChanged.Returns(new Subject>>()); - - return file; - } - - static IPullRequestSessionManager CreateSessionManager(DiffSide side) - { - var file = CreateSessionFile(); - return CreateSessionManager(file, side); - } - static IPullRequestSessionManager CreateSessionManager( IPullRequestSessionFile file, DiffSide side, @@ -243,22 +227,24 @@ public class WithoutTextBufferInfo [Test] public void FirstPassShouldReturnEmptyTags() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager()); + CreateSessionManager(file)); var result = target.GetTags(CreateSpan(10)); Assert.That(result, Is.Empty); } [Test] - public void ShouldReturnShowCommentTag() + public void ShouldReturnShowInlineTagForComment() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager()); + CreateSessionManager(file)); // Line 10 has an existing RHS comment. var span = CreateSpan(10); @@ -266,16 +252,90 @@ public void ShouldReturnShowCommentTag() var result = target.GetTags(span).ToList(); Assert.That(result, Has.One.Items); - Assert.That(result[0].Tag, Is.InstanceOf()); + + var showInlineTag = result[0].Tag as ShowInlineCommentTag; + Assert.That(showInlineTag, Is.Not.Null); + Assert.That(showInlineTag.Thread, Is.Not.Null); + Assert.That(showInlineTag.Annotations, Is.Null); + } + + [Test] + public void ShouldReturnShowInlineTagForAnnotation() + { + var file = CreateSessionFile(false, true); + var target = new InlineCommentTagger( + Substitute.For(), + Substitute.For(), + CreateSessionManager(file)); + + // Line 10 has an existing RHS annotation. + var span = CreateSpan(10); + var firstPass = target.GetTags(span); + var result = target.GetTags(span).ToList(); + + Assert.That(result, Has.One.Items); + + var showInlineTag = result[0].Tag as ShowInlineCommentTag; + Assert.That(showInlineTag, Is.Not.Null); + Assert.That(showInlineTag.Thread, Is.Null); + Assert.That(showInlineTag.Annotations, Is.Not.Null); + Assert.That(showInlineTag.Annotations.Count, Is.EqualTo(1)); + } + + [Test] + public void ShouldReturnShowInlineTagForTwoAnnotations() + { + var file = CreateSessionFile(false, true); + var target = new InlineCommentTagger( + Substitute.For(), + Substitute.For(), + CreateSessionManager(file)); + + // Line 20 has an existing RHS annotation. + var span = CreateSpan(20); + var firstPass = target.GetTags(span); + var result = target.GetTags(span).ToList(); + + Assert.That(result, Has.One.Items); + + var showInlineTag = result[0].Tag as ShowInlineCommentTag; + Assert.That(showInlineTag, Is.Not.Null); + Assert.That(showInlineTag.Thread, Is.Null); + Assert.That(showInlineTag.Annotations, Is.Not.Null); + Assert.That(showInlineTag.Annotations.Count, Is.EqualTo(2)); + } + + [Test] + public void ShouldReturnShowOneInlineTagForCommentAndAnnotation() + { + var file = CreateSessionFile(true, true); + var target = new InlineCommentTagger( + Substitute.For(), + Substitute.For(), + CreateSessionManager(file)); + + // Line 10 has an existing RHS comment. + var span = CreateSpan(10); + var firstPass = target.GetTags(span); + var result = target.GetTags(span).ToList(); + + Assert.That(result, Has.One.Items); + + var showInlineTag = result[0].Tag as ShowInlineCommentTag; + Assert.That(showInlineTag, Is.Not.Null); + Assert.That(showInlineTag.Thread, Is.Not.Null); + Assert.That(showInlineTag.Annotations, Is.Not.Null); + Assert.That(showInlineTag.Annotations.Count, Is.EqualTo(1)); } [Test] public void ShouldReturnAddNewCommentTagForAddedLine() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager()); + CreateSessionManager(file)); // Line 11 has an add diff entry. var span = CreateSpan(11); @@ -289,10 +349,11 @@ public void ShouldReturnAddNewCommentTagForAddedLine() [Test] public void ShouldNotReturnAddNewCommentTagForDeletedLineOnRhs() { + var file = CreateSessionFile(); var target = new InlineCommentTagger( Substitute.For(), Substitute.For(), - CreateSessionManager()); + CreateSessionManager(file)); // Line 13 has an delete diff entry. var span = CreateSpan(13); @@ -357,46 +418,6 @@ static ITextView CreateTextView(bool inlineCommentMarginVisible = true) return textView; } - static IPullRequestSessionFile CreateSessionFile() - { - var diffChunk = new DiffChunk - { - Lines = - { - // Line numbers here are 1-based. There is an add diff entry on line 11 - // and a delete entry on line 13. - new DiffLine { Type = DiffChangeType.Add, NewLineNumber = 11 + 1 }, - new DiffLine { Type = DiffChangeType.Delete, OldLineNumber = 13 + 1 }, - } - }; - var diff = new List { diffChunk }; - - var rhsThread = Substitute.For(); - rhsThread.DiffLineType.Returns(DiffChangeType.Add); - rhsThread.LineNumber.Returns(10); - - var lhsThread = Substitute.For(); - lhsThread.DiffLineType.Returns(DiffChangeType.Delete); - lhsThread.LineNumber.Returns(12); - - // We have a comment to display on the right-hand-side of the diff view on line - // 11 and a comment to display on line 13 on the left-hand-side. - var threads = new List { rhsThread, lhsThread }; - - var file = Substitute.For(); - file.Diff.Returns(diff); - file.InlineCommentThreads.Returns(threads); - file.LinesChanged.Returns(new Subject>>()); - - return file; - } - - static IPullRequestSessionManager CreateSessionManager() - { - var file = CreateSessionFile(); - return CreateSessionManager(file); - } - static IPullRequestSessionManager CreateSessionManager(IPullRequestSessionFile file) { var result = Substitute.For(); @@ -411,7 +432,7 @@ static ITextSnapshot CreateSnapshot() { // We pretend that each line has 10 chars and there are 20 lines. var result = Substitute.For(); - result.Length.Returns(200); + result.Length.Returns(300); result.GetLineFromPosition(0).ReturnsForAnyArgs(x => CreateLine(result, x.Arg() / 10)); result.GetLineFromLineNumber(0).ReturnsForAnyArgs(x => CreateLine(result, x.Arg())); return result; @@ -442,5 +463,59 @@ static ITextSnapshotLine CreateLine(ITextSnapshot snapshot, int lineNumber) result.End.Returns(end); return result; } + + static IPullRequestSessionFile CreateSessionFile(bool withComments = true, bool withAnnotations = false) + { + var diffChunk = new DiffChunk + { + Lines = + { + // Line numbers here are 1-based. There is an add diff entry on lines 11 and 21 + // and a delete entry on line 13. + new DiffLine { Type = DiffChangeType.Add, NewLineNumber = 11 + 1 }, + new DiffLine { Type = DiffChangeType.Delete, OldLineNumber = 13 + 1 }, + new DiffLine { Type = DiffChangeType.Add, NewLineNumber = 21 + 1 }, + } + }; + var diff = new List { diffChunk }; + + var file = Substitute.For(); + file.Diff.Returns(diff); + + if (withComments) + { + var rhsThread = Substitute.For(); + rhsThread.DiffLineType.Returns(DiffChangeType.Add); + rhsThread.LineNumber.Returns(10); + + var lhsThread = Substitute.For(); + lhsThread.DiffLineType.Returns(DiffChangeType.Delete); + lhsThread.LineNumber.Returns(12); + + // We have a comment to display on the right-hand-side of the diff view on line + // 11 and a comment to display on line 13 on the left-hand-side. + var threads = new List { rhsThread, lhsThread }; + + file.InlineCommentThreads.Returns(threads); + } + + if (withAnnotations) + { + var annotation1 = new InlineAnnotationModel(new CheckSuiteModel(), new CheckRunModel(), new CheckRunAnnotationModel(){EndLine = 11}); + + var annotation2 = new InlineAnnotationModel(new CheckSuiteModel(), new CheckRunModel(), new CheckRunAnnotationModel() { EndLine = 21 }); + + var annotation3 = new InlineAnnotationModel(new CheckSuiteModel(), new CheckRunModel(), new CheckRunAnnotationModel() { EndLine = 21 }); + + var annotations = new List { annotation1, annotation2, annotation3 }; + + file.InlineAnnotations.Returns(annotations); + } + + file.LinesChanged.Returns(new Subject>>()); + + return file; + } + } }