Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RenameCommand #39

Merged
merged 2 commits into from Jan 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 1 addition & 42 deletions VsIntegration/Commands/GoToStepsCommand.cs
Expand Up @@ -3,7 +3,6 @@
using System.Linq;
using System.Windows.Forms;
using EnvDTE;
using TechTalk.SpecFlow.Bindings;
using TechTalk.SpecFlow.Bindings.Reflection;
using TechTalk.SpecFlow.IdeIntegration.Tracing;
using TechTalk.SpecFlow.VsIntegration.Bindings.Discovery;
Expand Down Expand Up @@ -38,18 +37,6 @@ public bool IsEnabled(Document activeDocument)
return bindingMethod != null && IsStepDefinition(bindingMethod, activeDocument);
}

private class StepInstanceWithProjectScope
{
public StepInstance StepInstance { get; private set; }
public VsProjectScope ProjectScope { get; private set; }

public StepInstanceWithProjectScope(StepInstance stepInstance, VsProjectScope projectScope)
{
StepInstance = stepInstance;
ProjectScope = projectScope;
}
}

public void Invoke(Document activeDocument)
{
var bindingMethod = GetSelectedBindingMethod(activeDocument);
Expand Down Expand Up @@ -113,35 +100,7 @@ private static void GoToLine(ProjectItem projectItem, int line)
navigatePoint.TryToShow();
navigatePoint.Parent.Selection.MoveToPoint(navigatePoint);
}

private class StepInstanceComparer : IEqualityComparer<StepInstance>, IComparer<StepInstance>
{
public static readonly StepInstanceComparer Instance = new StepInstanceComparer();

public bool Equals(StepInstance si1, StepInstance si2)
{
var sp1 = (ISourceFilePosition) si1;
var sp2 = (ISourceFilePosition) si2;
return sp1.SourceFile.Equals(sp2.SourceFile, StringComparison.InvariantCultureIgnoreCase) && sp1.FilePosition.Line == sp2.FilePosition.Line;
}

public int GetHashCode(StepInstance obj)
{
return ((ISourceFilePosition) obj).SourceFile.GetHashCode();
}

public int Compare(StepInstance si1, StepInstance si2)
{
var sp1 = (ISourceFilePosition) si1;
var sp2 = (ISourceFilePosition) si2;

int result = StringComparer.InvariantCultureIgnoreCase.Compare(sp1.SourceFile, sp2.SourceFile);
if (result == 0)
result = sp1.FilePosition.Line.CompareTo(sp2.FilePosition.Line);
return result;
}
}


private IEnumerable<VsProjectScope> GetProjectScopes(Document activeDocument)
{
var projectScopes = projectScopeFactory.GetProjectScopesFromBindingProject(activeDocument.ProjectItem.ContainingProject);
Expand Down
12 changes: 9 additions & 3 deletions VsIntegration/EditorCommands/EditorCommandFilter.cs
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
Expand Down Expand Up @@ -30,14 +29,16 @@ internal class EditorCommandFilter
private readonly RunScenariosCommand runScenariosCommand;
private readonly FormatTableCommand formatTableCommand;
private readonly CommentUncommentCommand commentUncommentCommand;
private readonly RenameCommand renameCommand;

public EditorCommandFilter(IIdeTracer tracer, IGoToStepDefinitionCommand goToStepDefinitionCommand, DebugScenariosCommand debugScenariosCommand, RunScenariosCommand runScenariosCommand, FormatTableCommand formatTableCommand, CommentUncommentCommand commentUncommentCommand)
public EditorCommandFilter(IIdeTracer tracer, IGoToStepDefinitionCommand goToStepDefinitionCommand, DebugScenariosCommand debugScenariosCommand, RunScenariosCommand runScenariosCommand, FormatTableCommand formatTableCommand, CommentUncommentCommand commentUncommentCommand, RenameCommand renameCommand)
{
this.goToStepDefinitionCommand = goToStepDefinitionCommand;
this.debugScenariosCommand = debugScenariosCommand;
this.runScenariosCommand = runScenariosCommand;
this.formatTableCommand = formatTableCommand;
this.commentUncommentCommand = commentUncommentCommand;
this.renameCommand = renameCommand;
this.tracer = tracer;
}

Expand Down Expand Up @@ -73,7 +74,8 @@ public bool QueryStatus(GherkinEditorContext editorContext, Guid pguidCmdGroup,
case VSConstants.VSStd2KCmdID.COMMENT_BLOCK:
case VSConstants.VSStd2KCmdID.COMMENTBLOCK:
case VSConstants.VSStd2KCmdID.UNCOMMENT_BLOCK:
case VSConstants.VSStd2KCmdID.UNCOMMENTBLOCK:
case VSConstants.VSStd2KCmdID.UNCOMMENTBLOCK:
case VSConstants.VSStd2KCmdID.RENAME:
return true;
}
}
Expand Down Expand Up @@ -153,6 +155,10 @@ public bool PreExec(GherkinEditorContext editorContext, Guid pguidCmdGroup, uint
case VSConstants.VSStd2KCmdID.UNCOMMENTBLOCK:
if (commentUncommentCommand.CommentOrUncommentSelection(editorContext, CommentUncommentAction.Uncomment))
return true;
break;
case VSConstants.VSStd2KCmdID.RENAME:
if (renameCommand.Rename(editorContext))
return true;
break;
}
}
Expand Down
265 changes: 265 additions & 0 deletions VsIntegration/EditorCommands/RenameCommand.cs
@@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using EnvDTE;
using Microsoft.VisualBasic;
using Microsoft.VisualStudio.Text;
using TechTalk.SpecFlow.Bindings;
using TechTalk.SpecFlow.Bindings.Reflection;
using TechTalk.SpecFlow.Infrastructure;
using TechTalk.SpecFlow.VsIntegration.Bindings.Discovery;
using TechTalk.SpecFlow.VsIntegration.LanguageService;
using TechTalk.SpecFlow.VsIntegration.StepSuggestions;
using TechTalk.SpecFlow.VsIntegration.Utils;

namespace TechTalk.SpecFlow.VsIntegration.EditorCommands
{
internal class RenameCommand
{
private readonly IGherkinLanguageServiceFactory _gherkinLanguageServiceFactory;
private readonly IProjectScopeFactory _projectScopeFactory;

public RenameCommand(IGherkinLanguageServiceFactory gherkinLanguageServiceFactory, IProjectScopeFactory projectScopeFactory)
{
_gherkinLanguageServiceFactory = gherkinLanguageServiceFactory;
_projectScopeFactory = projectScopeFactory;
}

public bool Rename(GherkinEditorContext editorContext)
{
var step = GetCurrentStep(editorContext);
if (step == null)
return false;

var stepBinding = GetSingleStepDefinitionBinding(editorContext, step);
if (stepBinding == null)
return false;

var codeFunction = FindBindingMethodCodeFunction(editorContext, stepBinding);
if (codeFunction == null)
return false;

var newStepRegex = PromptForNewStepRegex(stepBinding.Regex);

if (string.IsNullOrEmpty(newStepRegex))
return false;

var stepInstancesToRename = FindAllStepMatchingStepInstances(codeFunction.DTE.ActiveDocument, stepBinding.Method);
foreach (var stepInstanceToRename in stepInstancesToRename)
{
RenameStep(stepInstanceToRename, newStepRegex, stepBinding);
}

ReplaceStepBindingAttribute(codeFunction, stepBinding, newStepRegex);

return true;
}

private static string PromptForNewStepRegex(Regex currentRegex)
{
var stringRegex = FormatRegexForDisplay(currentRegex);
var newStepRegex = Interaction.InputBox("Give a new name to the step" + Environment.NewLine + stringRegex, "Rename step", stringRegex);
if (newStepRegex != stringRegex)
return newStepRegex;

return string.Empty;
}

private IEnumerable<StepInstanceWithProjectScope> FindAllStepMatchingStepInstances(Document document, IBindingMethod bindingMethod)
{
var projectScopes = GetProjectScopes(document).ToArray();
if (projectScopes.Any(ps => !ps.StepSuggestionProvider.Populated))
{
MessageBox.Show("Step bindings are still being analyzed. Please wait.", "Go to steps");
return new StepInstanceWithProjectScope[0];
}
return projectScopes.SelectMany(ps => GetMatchingSteps(bindingMethod, ps)).ToArray();
}

private void RenameStep(StepInstanceWithProjectScope stepInstance, string newStepRegex, IStepDefinitionBinding binding)
{
var featureFileDocument = JumpToStep(stepInstance);
if (featureFileDocument == null)
return;

var stepEditorContext = GherkinEditorContext.FromDocument(featureFileDocument, _gherkinLanguageServiceFactory);

var stepToRename = GetCurrentStep(stepEditorContext);
if (stepToRename == null)
return;

if (!binding.Regex.IsMatch(stepToRename.Text))
return;

var stepLineStart = stepEditorContext.TextView.Selection.Start.Position.GetContainingLine();
using (var stepNameTextEdit = stepLineStart.Snapshot.TextBuffer.CreateEdit())
{
var line = stepLineStart.Snapshot.GetLineFromLineNumber(stepLineStart.LineNumber);
var lineText = line.GetText();
var trimmedText = lineText.Trim();
var numLeadingWhiteSpaces = lineText.Length - trimmedText.Length;

var actualStepName = trimmedText.Substring(stepToRename.Keyword.Length);
var newStepName = BuildStepNameWithNewRegex(actualStepName, newStepRegex, binding);

var stepNamePosition = line.Start.Position + numLeadingWhiteSpaces + stepToRename.Keyword.Length;
stepNameTextEdit.Replace(stepNamePosition, actualStepName.Length, newStepName);

stepNameTextEdit.Apply();
}
}

private static string BuildStepNameWithNewRegex(string stepName, string newStepRegex, IStepDefinitionBinding binding)
{
var originalMatch = Regex.Match(stepName, FormatRegexForDisplay(binding.Regex));
var newRegexMatch = Regex.Match(newStepRegex, newStepRegex);

var builder = new StringBuilder(newStepRegex);
for (var i = newRegexMatch.Groups.Count - 1; i > 0; i--)
{
builder.Replace(newRegexMatch.Groups[i].Value, originalMatch.Groups[i].Value, newRegexMatch.Groups[i].Index, newRegexMatch.Groups[i].Length);
}

return RemoveDoubleQuotes(builder.ToString());
}

private IEnumerable<VsProjectScope> GetProjectScopes(Document activeDocument)
{
var projectScopes = _projectScopeFactory.GetProjectScopesFromBindingProject(activeDocument.ProjectItem.ContainingProject);
return projectScopes.OfType<VsProjectScope>();
}

private static IEnumerable<StepInstanceWithProjectScope> GetMatchingSteps(IBindingMethod bindingMethod, VsProjectScope projectScope)
{
return projectScope.StepSuggestionProvider.GetMatchingInstances(bindingMethod)
.Where(si => si is ISourceFilePosition)
.Distinct(StepInstanceComparer.Instance)
.OrderBy(si => si, StepInstanceComparer.Instance)
.Select(si => new StepInstanceWithProjectScope(si, projectScope));
}

private static IStepDefinitionBinding GetSingleStepDefinitionBinding(GherkinEditorContext editorContext, GherkinStep step)
{
var bindingMatchService = editorContext.LanguageService.ProjectScope.BindingMatchService;
if (bindingMatchService == null)
return null;

if (!bindingMatchService.Ready)
{
MessageBox.Show("Step bindings are still being analyzed. Please wait.", "Go to binding");
return null;
}

List<BindingMatch> candidatingMatches;
StepDefinitionAmbiguityReason ambiguityReason;
CultureInfo bindingCulture = editorContext.ProjectScope.SpecFlowConfiguration.BindingCulture ?? step.StepContext.Language;
var match = bindingMatchService.GetBestMatch(step, bindingCulture, out ambiguityReason, out candidatingMatches);

if (candidatingMatches.Count > 1 || !match.Success)
{
MessageBox.Show("Cannot rename automatically. You need to have a single and unique binding for this step.");
return null;
}

return match.StepBinding;
}

private static CodeFunction FindBindingMethodCodeFunction(GherkinEditorContext editorContext, IStepDefinitionBinding binding)
{
return new VsBindingMethodLocator().FindCodeFunction(((VsProjectScope)editorContext.ProjectScope), binding.Method);
}

private void ReplaceStepBindingAttribute(CodeFunction codeFunction, IStepDefinitionBinding binding, string newRegex)
{
if (!codeFunction.ProjectItem.IsOpen)
{
codeFunction.ProjectItem.Open();
}

var formattedOldRegex = FormatRegexForDisplay(binding.Regex);

var navigatePoint = codeFunction.GetStartPoint(vsCMPart.vsCMPartHeader);
navigatePoint.TryToShow();
navigatePoint.Parent.Selection.MoveToPoint(navigatePoint);

var stepBindingEditorContext = GherkinEditorContext.FromDocument(codeFunction.DTE.ActiveDocument, _gherkinLanguageServiceFactory);
var attributeLinesToUpdate = stepBindingEditorContext.TextView.TextViewLines.Where(x => x.Start.GetContainingLine().GetText().Contains("\"" + formattedOldRegex + "\""));

foreach (var attributeLineToUpdate in attributeLinesToUpdate)
{
using (var textEdit = attributeLineToUpdate.Snapshot.TextBuffer.CreateEdit())
{
var regexStart = attributeLineToUpdate.Start.GetContainingLine().GetText().IndexOf(formattedOldRegex);
textEdit.Replace(attributeLineToUpdate.Start.Position + regexStart, formattedOldRegex.Length, newRegex);
textEdit.Apply();
}
}
}

private static GherkinStep GetCurrentStep(GherkinEditorContext editorContext)
{
var fileScope = editorContext.LanguageService.GetFileScope(waitForLatest: true);
if (fileScope == null)
return null;

SnapshotPoint caret = editorContext.TextView.Caret.Position.BufferPosition;
IStepBlock block;
var step = fileScope.GetStepAtPosition(caret.GetContainingLine().LineNumber, out block);

if (step != null && block is IScenarioOutlineBlock)
step = step.GetSubstitutedStep((IScenarioOutlineBlock)block);

return step;
}

private static Document JumpToStep(StepInstanceWithProjectScope stepInstance)
{
var sourceFilePosition = ((ISourceFilePosition)stepInstance.StepInstance);
var featureFile = VsxHelper.GetAllPhysicalFileProjectItem(stepInstance.ProjectScope.Project).FirstOrDefault(
pi => VsxHelper.GetProjectRelativePath(pi).Equals(sourceFilePosition.SourceFile));

if (featureFile == null)
return null;

if (!featureFile.IsOpen)
featureFile.Open();

GoToLine(featureFile, sourceFilePosition.FilePosition.Line);
return featureFile.Document;
}

private static void GoToLine(ProjectItem projectItem, int line)
{
TextDocument codeBehindTextDocument = (TextDocument)projectItem.Document.Object("TextDocument");

var navigatePoint = codeBehindTextDocument.StartPoint.CreateEditPoint();
navigatePoint.MoveToLineAndOffset(line, 1);
navigatePoint.TryToShow();
navigatePoint.Parent.Selection.MoveToPoint(navigatePoint);
}

private static string FormatRegexForDisplay(Regex regex)
{
return RemoveDoubleQuotes(TrimLast(TrimFirst(regex.ToString())));
}

private static string RemoveDoubleQuotes(string value)
{
return value.Replace("\"\"", "\""); ;
}

private static string TrimFirst(string value)
{
return value.Remove(0, 1);
}

private static string TrimLast(string value)
{
return value.Remove(value.Length - 1, 1);
}
}
}
17 changes: 17 additions & 0 deletions VsIntegration/StepSuggestions/StepInstanceWithProjectScope.cs
@@ -0,0 +1,17 @@
using TechTalk.SpecFlow.Bindings;
using TechTalk.SpecFlow.VsIntegration.LanguageService;

namespace TechTalk.SpecFlow.VsIntegration.StepSuggestions
{
internal class StepInstanceWithProjectScope
{
public StepInstance StepInstance { get; private set; }
public VsProjectScope ProjectScope { get; private set; }

public StepInstanceWithProjectScope(StepInstance stepInstance, VsProjectScope projectScope)
{
StepInstance = stepInstance;
ProjectScope = projectScope;
}
}
}