2

I am currently experimenting with Roslyn and Code Actions, more specific Code Refactorings. It feels kind of easy, but I have a difficulty I cannot solve.

Code actions are executed once against a dummy workspace as a "preview" option, so that you can see the actual changes before you click the action and execute it against the real workspace.

Now I am dealing with some things Roslyn can't really do (yet), so I am doing some changes via EnvDTE. I know, it's bad, but I couldn't find another way.

So the issue here is: When I hover over my code action, the code gets executed as preview, and it should NOT do the EnvDTE changes. Those should only be done when the real execute happens.

I have created a gist with a small example of my code. It doesn't really makes sense, but should show what I want to achieve. Do some modifications via roslyn, then do something via EnvDTE, like changing Cursor position. But of course only on the real execution.

The relevant part for those who can't click the gist:

public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
    var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
    var node = root.FindNode(context.Span);

    var dec = node as MethodDeclarationSyntax;
    if (dec == null)
        return;

    context.RegisterRefactoring(CodeAction.Create("MyAction", c => DoMyAction(context.Document, dec, c)));
}

private static async Task<Solution> DoMyAction(Document document, MethodDeclarationSyntax method, CancellationToken cancellationToken)
{
    var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
    var root = await syntaxTree.GetRootAsync(cancellationToken);

    // some - for the question irrelevant - roslyn changes, like:
    document = document.WithSyntaxRoot(root.ReplaceNode(method, method.WithIdentifier(SyntaxFactory.ParseToken(method.Identifier.Text + "Suffix"))));

    // now the DTE magic
    var preview = false; // <--- TODO: How to check if I am in preview here?
    if (!preview)
    {
        var requestedItem = DTE.Solution.FindProjectItem(document.FilePath);
        var window = requestedItem.Open(Constants.vsViewKindCode);
        window.Activate();

        var position = method.Identifier.GetLocation().GetLineSpan().EndLinePosition;
        var textSelection = (TextSelection) window.Document.Selection;
        textSelection.MoveTo(position.Line, position.Character);
    }

    return document.Project.Solution;
}
Wolfsblvt
  • 1,061
  • 12
  • 27
  • I was looking for this the other day I ended up writing this horrible [hack](https://stackoverflow.com/a/44334973/1938988) – johnny 5 Jun 05 '17 at 17:14
  • I guess that could work, yes. But I feel my solution is the... "cleaner" way, if you could say so. – Wolfsblvt Jun 05 '17 at 21:15
  • Yeah that why I said it was a horrible hack. I was just pissed that I was looking for this code a day before and it wasn't here lol – johnny 5 Jun 05 '17 at 21:16
  • I understand, yeah. I feel like ther is very sparse information about anything in Roslyn out there yet. I have so many problems and hurdles to solve, but rarely I do find useful information out htere. Wonder why this is the case. – Wolfsblvt Jun 05 '17 at 21:19
  • I hear you. I've been dealing with horrible extension this past month, it's like every other thing I'm trying to do there is no documentation – johnny 5 Jun 05 '17 at 21:21
  • It starts with Roslyn things like how you build up a syntax tree, find the name of the namespace where a class is in and gets even worse when Roslyn does not include functionality and you have to fall back to something like `EnvDTE`. This is is literally a pile of thousands of undocumented things, and it does not work nicely in any way with the Roslyn stuff. I have so many small things that take hours to solve for my small extension. It's quite exhausting. Does that mean so few people actually use Roslyn? Despite it being open source? – Wolfsblvt Jun 05 '17 at 21:25
  • Yeah, finding how to make things work is trouble sometime, I don't think roslyn's widely that used. I've only seen 4 or 5 names floating around the message boards. If you look at the tag its only been referenced 1500 times or so – johnny 5 Jun 05 '17 at 21:34
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/145920/discussion-between-wolfsblvt-and-johnny-5). – Wolfsblvt Jun 05 '17 at 21:36

2 Answers2

2

You can choose to override ComputePreviewOperationsAsync to have different behavior for Previews from regular code.

Kevin Pilch
  • 11,485
  • 1
  • 39
  • 41
  • I've seen that this is possible and have even tried to create custom code action with that overriden. But this function should return `IEnumerable`, that's something totally different from what the normal code in my action does. I do not fully understand what I should do there. Do you have an example how such thing could be achieved? – Wolfsblvt May 29 '17 at 21:07
  • Take a look at the overrides in the source browser [here](http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/CodeActions/CodeAction.cs,4a60a4dc2ea63ba0,references) for where Roslyn itself uses this. – Kevin Pilch May 30 '17 at 22:45
  • I've followed your suggestion and found a way to implement it like I wanted. Thank you for this tip. I've added an answer with the full solution I found to my question. If you could take a look, that would be great. – Wolfsblvt May 31 '17 at 11:06
2

I've found the solution to my problem by digging deeper and trial and error after Keven Pilch's answer. He bumped me in the right direction.

The solution was to override both the ComputePreviewOperationsAsync and the GetChangedSolutionAsync methods in my own CodeAction.

Here the relevant part of my CustomCodeAction, or full gist here.

private readonly Func<CancellationToken, bool, Task<Solution>> _createChangedSolution;

protected override async Task<IEnumerable<CodeActionOperation>> ComputePreviewOperationsAsync(CancellationToken cancellationToken)
{
    const bool isPreview = true;
    // Content copied from http://sourceroslyn.io/#Microsoft.CodeAnalysis.Workspaces/CodeActions/CodeAction.cs,81b0a0866b894b0e,references
    var changedSolution = await GetChangedSolutionWithPreviewAsync(cancellationToken, isPreview).ConfigureAwait(false);
    if (changedSolution == null)
        return null;

    return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) };
}

protected override Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
    const bool isPreview = false;
    return GetChangedSolutionWithPreviewAsync(cancellationToken, isPreview);
}

protected virtual Task<Solution> GetChangedSolutionWithPreviewAsync(CancellationToken cancellationToken, bool isPreview)
{
    return _createChangedSolution(cancellationToken, isPreview);
}

The code to create the action stays quite similar, except the bool is added and I can check against it then:

public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
    // [...]

    context.RegisterRefactoring(CustomCodeAction.Create("MyAction",
        (c, isPreview) => DoMyAction(context.Document, dec, c, isPreview)));
}

private static async Task<Solution> DoMyAction(Document document, MethodDeclarationSyntax method, CancellationToken cancellationToken, bool isPreview)
{
    // some - for the question irrelevant - roslyn changes, like:
    // [...]

    // now the DTE magic
    if (!isPreview)
    {
        // [...]
    }

    return document.Project.Solution;
}

Why those two?
The ComputePreviewOperationsAsync calls the the normal ComputeOperationsAsync, which internally calls ComputeOperationsAsync. This computation executes GetChangedSolutionAsync. So both ways - preview and not - end up at GetChangedSolutionAsync. That's what I actually want, calling the same code, getting a very similar solution, but giving a bool flag if it is preview or not too.
So I've written my own GetChangedSolutionWithPreviewAsync which I use instead. I have overriden the default GetChangedSolutionAsync using my custom Get function, and then ComputePreviewOperationsAsync with a fully customized body. Instead of calling ComputeOperationsAsync, which the default one does, I've copied the code of that function, and modified it to use my GetChangedSolutionWithPreviewAsync instead.
Sounds rather complicated in written from, but I guess the code above should explain it quite well.

Hope this helps other people.

Kirill Osenkov
  • 8,786
  • 2
  • 33
  • 37
Wolfsblvt
  • 1,061
  • 12
  • 27
  • I don't know if this solution will work for when you're getting change set to the whole document – johnny 5 Jun 07 '17 at 21:03
  • I don't understand. What do you mean exactly? – Wolfsblvt Jun 08 '17 at 08:25
  • this code action you've created in assuming is for a fix provider. When you're in vs and you hit the code action light bulb. You are given the option of whether you want to just fix the issue or fix the issue for the document, project or solution. When you choose document, or project or solution. Preview changes is called before the fix is fully applied. Meaning the is preview does not work properly – johnny 5 Jun 08 '17 at 12:44
  • This is no CodeFix, it is a CodeRefactoring, indicated by the `context.RegisterRefactoring()` call to add the action. I thought refactorings just target the current thing and can't be executed against document/solution? There is no Analyzer for my action, so it can't even find all actions where it could do that thing.. Or do I understand something wrong? – Wolfsblvt Jun 08 '17 at 13:07
  • interesting I didn't know that existed. So when is you're code called from, is it under the right click refactor menu? – johnny 5 Jun 08 '17 at 14:07
  • I don't know if it exists there, I call it via the LightBulb that pops up when I have the caret at a matching token. But I am pretty sure those refactoring actions never have a "In File" or "In Project" option. At least I don't see that in some other Roslyn default actions that I checked right now. Isn't this project/solution option a ReSharper feature for the ReSharper actions? – Wolfsblvt Jun 08 '17 at 14:51
  • Here you can checkout this image of it, it should be in that menu https://i.stack.imgur.com/oQGi3.png Let me know I may have a solution for you if that's the case. I'm working on fixing today – johnny 5 Jun 08 '17 at 14:57
  • Ah okay, interesting. I mean for my current situation I don't need multi-actions. It's for generating a test method (stub) for a given method, so I don't want/need the option to do it in file or project, but would be cool to know how this works. – Wolfsblvt Jun 08 '17 at 22:22