Ogglas
Ogglas

Reputation: 70008

C# -> Roslyn -> DocumentEditor.ReplaceNode -> Code indentation and format

I have a problem that when I use DocumentEditor.ReplaceNode everything works but the generated code is hard to read.

Roslyn - replace node and fix the whitespaces

Output looks like this with several strings on the same line:

using System;
using System.IO;
using System.Linq;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {

            string test5 = @"test symbols \r\n © @ {} [] <> | / \ $£@!\#¤%&/()=?` hello";
            string varTest1 = @"test var hello"; string varTest2 = @"test var hello";
            string test1 = @"test string hello";
            string test2 = @"test String hello"; string test3 = @"test const hello"; string test4 = @"test readonly hello";
            int i = 0;

            var i2 = 0;
        }

    }
}

I can get a new line by adding {System.Environment.NewLine} to the end of the string and remove all formating but then the code is not indented.

What I have tried:

1:

var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = @\"{value + " hello"}\";").WithAdditionalAnnotations(Formatter.Annotation);

newVariable = newVariable.NormalizeWhitespace();

2:

var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = @\"{value + " hello"}\";").WithAdditionalAnnotations(Formatter.Annotation);

3:

var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = @\"{value + " hello"}\";").WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);

newVariable = newVariable.NormalizeWhitespace();

4:

var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = @\"{value + " hello"}\";");

newVariable = newVariable.NormalizeWhitespace();

Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;

namespace CodeAnalysisApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var workspace = new AdhocWorkspace();
            var projectId = ProjectId.CreateNewId();
            var versionStamp = VersionStamp.Create();
            var projectInfo = ProjectInfo.Create(projectId, versionStamp, "NewProject", "projName", LanguageNames.CSharp);
            var newProject = workspace.AddProject(projectInfo);

            var sourceText = SourceText.From(
                @"
                  using System;
                  using System.IO;
                  using System.Linq;
                  using System.Text;

                  namespace HelloWorld
                  {
                      class Program
                      {
                          static void Main(string[] args)
                          {

                              string test5 = ""test symbols \r\n © @ {} [] <> | / \ $£@!\#¤%&/()=?`""; 

                              var varTest1 = ""test var"";

                              var varTest2 = ""test var"";

                              string test1 = ""test string"";

                              String test2 = ""test String"";

                              const string test3 = ""test const""; 

                              readonly string test4 = ""test readonly""; 

                              int i = 0;

                              var i2 = 0;
                          }

                      }
                  }");

            var document = workspace.AddDocument(newProject.Id, "NewFile.cs", sourceText);
            var syntaxRoot = document.GetSyntaxRootAsync().Result;

            var root = (CompilationUnitSyntax)syntaxRoot;

            var editor = DocumentEditor.CreateAsync(document).Result;


            var localDeclaration = new LocalDeclarationVirtualizationVisitor();
            localDeclaration.Visit(root);

            var localDeclarations = localDeclaration.LocalDeclarations;

            foreach (var localDeclarationStatementSyntax in localDeclarations)
            {
                foreach (VariableDeclaratorSyntax variable in localDeclarationStatementSyntax.Declaration.Variables)
                {

                    var stringKind = variable.Initializer.Value.Kind();

                    //Replace string variables
                    if (stringKind == SyntaxKind.StringLiteralExpression)
                    {
                        //Remove " from string
                        var value = variable.Initializer.Value.ToString().Remove(0, 1);
                        value = value.Remove(value.Length - 1, 1);

                        var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = @\"{value + " hello"}\";").WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);

                        newVariable = newVariable.NormalizeWhitespace();

                        editor.ReplaceNode(variable, newVariable);

                        Console.WriteLine($"Key: {variable.Identifier.Value} Value:{variable.Initializer.Value}");
                    }
                }
            }

            var newDocument = editor.GetChangedDocument();

            var text = newDocument.GetTextAsync().Result.ToString();
        }
    }

    class LocalDeclarationVirtualizationVisitor : CSharpSyntaxRewriter
    {
        public LocalDeclarationVirtualizationVisitor()
        {
            LocalDeclarations = new List<LocalDeclarationStatementSyntax>();
        }

        public List<LocalDeclarationStatementSyntax> LocalDeclarations { get; set; }

        public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)
        {
            node = (LocalDeclarationStatementSyntax)base.VisitLocalDeclarationStatement(node);
            LocalDeclarations.Add(node);
            return node;
        }
    }
}

Upvotes: 1

Views: 2175

Answers (1)

SJP
SJP

Reputation: 1010

Normalize Whitespace normalizes the whitespace for the current object - not the object contained within a SyntaxTree.

If you would for instance call normalize whitespace on newVariable with the value

string varTest2 = @"test var hello";

It does not matter that the variable declaration is also within a syntax tree - what matters is the current context. Normalizing the whitespace of the above statement does basically nothing as there are no BlockStatements, Declarations or other elements which would create an indentation.

If you however call normalize whitespace on the containing scope, for instance the method, you would get something along these lines:

static void Main(string[] args)
{
    string test5 = @"test symbols \r\n © @ {} [] <> | / \ $£@!\#¤%&/()=?` hello";
    string varTest1 = @"test var hello";
    string varTest2 = @"test var hello";
    string test1 = @"test string hello";
    string test2 = @"test String hello";
    string test3 = @"test const hello";
    string test4 = @"test readonly hello";
    int i = 0;
    var i2 = 0;
}

As you can see this would provide you with a correctly indented method. So in order to get the correctly formatted document you would have to call NormalizeWhitespace on the SyntaxRoot after everything else is done:

editor.GetChangedRoot().NormalizeWhitespace().ToFullString()

This will of course prevent you from retaining your old formatting if you had some artifact you would like to keep (e.g. the additional line between the DeclarationStatements you have in your sample).

If you want to keep this formatting and the comments you could just try to copy the Trivia(or parts of the Trivia) from the original statement:

foreach (var localDeclarationStatementSyntax in localDeclarations)
{
    foreach (VariableDeclaratorSyntax variable in localDeclarationStatementSyntax.Declaration.Variables)
    {

        var stringKind = variable.Initializer.Value.Kind();

        //Replace string variables
        if (stringKind == SyntaxKind.StringLiteralExpression)
        {
            //Remove " from string
            var value = variable.Initializer.Value.ToString().Remove(0, 1);
            value = value.Remove(value.Length - 1, 1);

            var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = @\"{value + " hello"}\";").WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);

            newVariable = newVariable.NormalizeWhitespace();

            // This is new, copies the trivia (indentations, comments, etc.)
            newVariable = newVariable.WithLeadingTrivia(localDeclarationStatementSyntax.GetLeadingTrivia());
            newVariable = newVariable.WithTrailingTrivia(localDeclarationStatementSyntax.GetTrailingTrivia());


            editor.ReplaceNode(variable, newVariable);

            Console.WriteLine($"Key: {variable.Identifier.Value} Value:{variable.Initializer.Value}");
        }
    }
}

Upvotes: 3

Related Questions