Reputation: 149
How can I insert a variable declaration after a region directive in Roslyn? I'd like to be able to do something like going from this:
class MyClass
{
#region myRegion
#endregion
}
to this:
class MyClass
{
#region myRegion
private const string myData = "somedata";
#endregion
}
I can't seem to find any examples that deal with trivia in this manner.
Upvotes: 3
Views: 1331
Reputation: 2049
m0sa's answer works for empty regions but it cannot replace existing code which is the likely reason to need to this i.e. rerunning a code-generation tool.
Achieving this requires finding the full extent of the region. This is also made difficult because the target file can contain multiple nested regions. To do this I process all directives and build a hierarchy of regions:
public class RegionInfo
{
public RegionDirectiveTriviaSyntax Begin;
public EndRegionDirectiveTriviaSyntax End;
public RegionInfo Parent;
public List<RegionInfo> Children = new List<RegionInfo>();
public string Name => this.Begin.EndOfDirectiveToken.ToFullString().Trim();
}
public static class CodeMutator
{
public static string ReplaceRegion(string existingCode, string regionName, string newCode)
{
var syntaxTree = CSharpSyntaxTree.ParseText(existingCode);
var region = CodeMutator.GetRegion(syntaxTree, regionName);
if (region == null)
{
throw new Exception($"Cannot find region named {regionName}");
}
return
existingCode
.Substring(0, region.Begin.FullSpan.End) +
newCode +
Environment.NewLine +
existingCode
.Substring(region.End.FullSpan.Start);
}
static RegionInfo GetRegion(SyntaxTree syntaxTree, string regionName) =>
CodeMutator.GetRegions(syntaxTree)
.FirstOrDefault(x => x.Name == regionName);
static List<RegionInfo> GetRegions(SyntaxTree syntaxTree)
{
var directives = syntaxTree
.GetRoot()
.DescendantNodes(descendIntoTrivia: true)
.OfType<DirectiveTriviaSyntax>()
.Select(x => (x.GetLocation().SourceSpan.Start, x))
.OrderBy(x => x.Item1)
.ToList();
var allRegions = new List<RegionInfo>();
RegionInfo parent = null;
foreach (var directive in directives)
{
if (directive.Item2 is RegionDirectiveTriviaSyntax begin)
{
var next = new RegionInfo() {Begin = begin, Parent = parent};
allRegions.Add(next);
parent?.Children.Add(next);
parent = next;
}
else if (directive.Item2 is EndRegionDirectiveTriviaSyntax end)
{
if (parent == null)
{
Log.Error("Unmatched end region");
}
else
{
parent.End = end;
parent = parent.Parent;
}
}
}
return allRegions;
}
}
Upvotes: 2
Reputation: 10940
That's quite tricky to do with a CSharpSyntaxRewriter, because both the #region <name>
and #endregion
end up in the same SyntaxTriviaList
, which you'd have to split out and figure out what to create instead. The simplest way to not bother with all the intricacies, is to create the corresponding TextChange
and modify the SourceText
.
var tree = SyntaxFactory.ParseSyntaxTree(
@"class MyClass
{
#region myRegion
#endregion
}");
// get the region trivia
var region = tree.GetRoot()
.DescendantNodes(descendIntoTrivia: true)
.OfType<RegionDirectiveTriviaSyntax>()
.Single();
// modify the source text
tree = tree.WithChangedText(
tree.GetText().WithChanges(
new TextChange(
region.GetLocation().SourceSpan,
region.ToFullString() + "private const string myData = \"somedata\";")));
After that, tree
is:
class MyClass
{
#region myRegion
private const string myData = "somedata";
#endregion
}
Upvotes: 4