using System;
using System.IO;
using System.ComponentModel.Composition;
using GitHub.Services;
using GitHub.Extensions;
using GitHub.VisualStudio;
using GitHub.Services.Vssdk.Commands;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Differencing;
using Microsoft.VisualStudio.TextManager.Interop;
using Task = System.Threading.Tasks.Task;
namespace GitHub.Commands
{
///
/// Navigate from a PR file to the equivalent file and location in the editor (or the reverse).
///
///
/// This command will do one of the following depending on context.
/// Navigate from PR file diff to the working file in the solution.
/// Navigate from the working file in the solution to the PR file diff.
/// Navigate from an editable diff (e.g. 'View Changes in Solution') to the editor view.
///
[Export(typeof(IGoToSolutionOrPullRequestFileCommand))]
public class GoToSolutionOrPullRequestFileCommand : VsCommand, IGoToSolutionOrPullRequestFileCommand
{
///
/// Gets the GUID of the group the command belongs to.
///
public static readonly Guid CommandSet = Guids.guidContextMenuSet;
///
/// Gets the numeric identifier of the command.
///
public const int CommandId = PkgCmdIDList.goToSolutionOrPullRequestFileCommand;
readonly IGitHubServiceProvider serviceProvider;
readonly Lazy editorAdapter;
readonly Lazy sessionManager;
readonly Lazy pullRequestEditorService;
readonly Lazy teamExplorerContext;
readonly Lazy gitHubContextService;
readonly Lazy statusBar;
readonly Lazy usageTracker;
[ImportingConstructor]
public GoToSolutionOrPullRequestFileCommand(
IGitHubServiceProvider serviceProvider,
Lazy editorAdapter,
Lazy sessionManager,
Lazy pullRequestEditorService,
Lazy teamExplorerContext,
Lazy gitHubContextService,
Lazy statusBar,
Lazy usageTracker) : base(CommandSet, CommandId)
{
this.serviceProvider = serviceProvider;
this.editorAdapter = editorAdapter;
this.sessionManager = sessionManager;
this.pullRequestEditorService = pullRequestEditorService;
this.gitHubContextService = gitHubContextService;
this.teamExplorerContext = teamExplorerContext;
this.statusBar = statusBar;
this.usageTracker = usageTracker;
BeforeQueryStatus += OnBeforeQueryStatus;
}
public override async Task Execute()
{
usageTracker.Value.IncrementCounter(x => x.ExecuteGoToSolutionOrPullRequestFileCommand).Forget();
try
{
var sourceView = pullRequestEditorService.Value.FindActiveView();
if (sourceView == null)
{
ShowErrorInStatusBar("Couldn't find active source view");
return;
}
var textView = editorAdapter.Value.GetWpfTextView(sourceView);
bool isEditableDiff = pullRequestEditorService.Value.IsEditableDiff(textView);
if (isEditableDiff)
{
// Navigating from editable diff to code view
await usageTracker.Value.IncrementCounter(x => x.NumberOfNavigateToCodeView);
// Open active document in Code View
pullRequestEditorService.Value.OpenActiveDocumentInCodeView(sourceView);
return;
}
var info = sessionManager.Value.GetTextBufferInfo(textView.TextBuffer);
if (info != null)
{
if (!info.Session.IsCheckedOut)
{
ShowInfoMessage(Resources.NavigateToEditorNotCheckedOutInfoMessage);
return;
}
// Navigate from PR file to solution
await usageTracker.Value.IncrementCounter(x => x.NumberOfPRDetailsNavigateToEditor);
var fileTextView = await pullRequestEditorService.Value.OpenFile(info.Session, info.RelativePath, true);
if (fileTextView == null)
{
ShowErrorInStatusBar("Couldn't find active target view");
return;
}
var fileView = editorAdapter.Value.GetViewAdapter(fileTextView);
pullRequestEditorService.Value.NavigateToEquivalentPosition(sourceView, fileView);
return;
}
if (TryNavigateFromHistoryFile(sourceView))
{
return;
}
var relativePath = sessionManager.Value.GetRelativePath(textView.TextBuffer);
if (relativePath == null)
{
ShowErrorInStatusBar("File isn't part of repository");
return;
}
var session = sessionManager.Value.CurrentSession;
if (session == null)
{
ShowErrorInStatusBar("Couldn't find Pull request session");
return;
}
// Navigate from solution file to PR diff file
await usageTracker.Value.IncrementCounter(x => x.NumberOfNavigateToPullRequestFileDiff);
var diffViewer = await pullRequestEditorService.Value.OpenDiff(session, relativePath, "HEAD", scrollToFirstDiff: false);
if (diffViewer == null)
{
ShowErrorInStatusBar("Couldn't find active diff viewer");
return;
}
var diffTextView = FindActiveTextView(diffViewer);
var diffView = editorAdapter.Value.GetViewAdapter(diffTextView);
pullRequestEditorService.Value.NavigateToEquivalentPosition(sourceView, diffView);
}
catch (Exception e)
{
ShowErrorInStatusBar("Error Navigating", e);
}
}
void OnBeforeQueryStatus(object sender, EventArgs e)
{
try
{
var sourceView = pullRequestEditorService.Value.FindActiveView();
if (sourceView == null)
{
// No active text view
Visible = false;
return;
}
var textView = editorAdapter.Value.GetWpfTextView(sourceView);
if (pullRequestEditorService.Value.IsEditableDiff(textView))
{
// Active text view is editable diff
Text = "Open File in Code View";
Visible = true;
return;
}
var info = sessionManager.Value.GetTextBufferInfo(textView.TextBuffer);
if (info != null)
{
// Active text view is a PR file
Text = "Open File in Solution";
Visible = true;
return;
}
var session = sessionManager.Value.CurrentSession;
if (session != null)
{
var relativePath = sessionManager.Value.GetRelativePath(textView.TextBuffer);
if (relativePath != null)
{
// Active text view is part of a repository
Text = "View Changes in #" + session.PullRequest.Number;
Visible = true;
return;
}
}
if (TryNavigateFromHistoryFileQueryStatus(sourceView))
{
return;
}
}
catch (Exception ex)
{
ShowErrorInStatusBar("Error QueryStatus", ex);
}
Visible = false;
}
// Set command Text/Visible properties and return true when active
bool TryNavigateFromHistoryFileQueryStatus(IVsTextView sourceView)
{
if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string && // Check there is an active repo
FindObjectishForTFSTempFile(sourceView) is string) // Looks like a history file
{
// Navigate from history file is active
Text = "Open File in Solution";
Visible = true;
return true;
}
return false;
}
// Attempt navigation to historical file
bool TryNavigateFromHistoryFile(IVsTextView sourceView)
{
if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string repositoryDir &&
FindObjectishForTFSTempFile(sourceView) is string objectish)
{
var (_, blobPath) = gitHubContextService.Value.ResolveBlobFromHistory(repositoryDir, objectish);
if (blobPath is string)
{
var workingFile = Path.Combine(repositoryDir, blobPath);
VsShellUtilities.OpenDocument(serviceProvider, workingFile, VSConstants.LOGVIEWID.TextView_guid,
out IVsUIHierarchy hierarchy, out uint itemID, out IVsWindowFrame windowFrame, out IVsTextView targetView);
pullRequestEditorService.Value.NavigateToEquivalentPosition(sourceView, targetView);
return true;
}
}
return false;
}
// Find the blob SHA in a file name if any
string FindObjectishForTFSTempFile(IVsTextView sourceView)
{
return
FindPath(sourceView) is string path &&
gitHubContextService.Value.FindObjectishForTFSTempFile(path) is string objectish ?
objectish : null;
}
// See http://microsoft.public.vstudio.extensibility.narkive.com/agfoD1GO/full-pathname-of-file-shown-in-current-view-of-core-editor#post2
static string FindPath(IVsTextView textView)
{
ErrorHandler.ThrowOnFailure(textView.GetBuffer(out IVsTextLines buffer));
var userData = buffer as IVsUserData;
if (userData == null)
{
return null;
}
ErrorHandler.ThrowOnFailure(userData.GetData(typeof(IVsUserData).GUID, out object data));
return data as string;
}
ITextView FindActiveTextView(IDifferenceViewer diffViewer)
{
switch (diffViewer.ActiveViewType)
{
case DifferenceViewType.InlineView:
return diffViewer.InlineView;
case DifferenceViewType.LeftView:
return diffViewer.LeftView;
case DifferenceViewType.RightView:
return diffViewer.RightView;
}
return null;
}
void ShowInfoMessage(string message)
{
ErrorHandler.ThrowOnFailure(VsShellUtilities.ShowMessageBox(
serviceProvider, message, null,
OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST));
}
void ShowErrorInStatusBar(string message)
{
statusBar.Value.ShowMessage(message);
}
void ShowErrorInStatusBar(string message, Exception e)
{
statusBar.Value.ShowMessage(message + ": " + e.Message);
}
}
}