internal static CodeWriters.IndentingWriter GetWriterForClass(string destFile, string classAttribute) { (string codeBlock, bool fileScopedNamespace) = GetCodeBlock(destFile, classAttribute); var w = new CodeWriters.IndentingWriter(startingIndent: fileScopedNamespace ? 0 : 1, fileScopedNamespace); w.AppendRawString(codeBlock); w.WL("{"); return(w); }
static void WriteBody(CodeWriters.IndentingWriter w, List <string> list, bool addQuotes = false) { string quote = addQuotes ? "\"" : ""; w.WL("{"); for (int i = 0; i < list.Count; i++) { string item = list[i]; string suffix = i < list.Count - 1 ? "," : ""; w.WL(quote + item + quote + suffix); } w.WL("};"); w.WL(); }
/* * TODO(FenGen/DesignerGen): * -Somehow deal with icons/images wanting to load from Resources but we want them from Images etc. * (we now do this for AngelLoader app icon only as a hack, but we should generalize it) * -Have some way to manually specify things, like specify lines not to overwrite because they're manually * set up. */ private static void GenerateDesignerFile(string designerFile) { var controlTypes = new Dictionary <string, string>(); var controlAttributes = new Dictionary <string, string>(); var controlsInFlowLayoutPanels = new HashSet <string>(); var controlProperties = new Dictionary <string, CProps>(); var destNodes = new List <NodeCustom>(); string formFileName = Path.GetFileName(designerFile); string destFile = Path.Combine(Path.GetDirectoryName(designerFile) !, formFileName.Substring(0, formFileName.Length - ".Designer.cs".Length) + "_InitSlim.Generated.cs"); string code = File.ReadAllText(designerFile); List <string> sourceLines = code.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).ToList(); #region Remove existing ifdefs // Hack: Remove existing ifdefs because otherwise the parser just ignores all code inside them... for (int i = 0; i < sourceLines.Count; i++) { string lineT = sourceLines[i].Trim(); if (lineT.StartsWith("#if DEBUG") || lineT.StartsWith("#endif")) { sourceLines.RemoveAt(i); i--; } } code = string.Join("\r\n", sourceLines); #endregion SyntaxTree tree = ParseTextFast(code); BaseNamespaceDeclarationSyntax?namespaceDeclaration = null; ClassDeclarationSyntax? formClass = null; bool fileScopedNamespace = false; #region Find namespace and form class declarations foreach (SyntaxNode node in tree.GetRoot().DescendantNodes()) { if (node is BaseNamespaceDeclarationSyntax nds) { namespaceDeclaration = nds; fileScopedNamespace = node is FileScopedNamespaceDeclarationSyntax; } else if (node is ClassDeclarationSyntax cds) { formClass = cds; break; } } if (namespaceDeclaration == null) { ThrowErrorAndTerminate("Namespace declaration (either normal or file-scoped) not found in:\r\n" + designerFile); return; } var namespaceDeclarationLineSpan = namespaceDeclaration.GetLocation().GetLineSpan(); int namespaceDeclarationStartLine = namespaceDeclarationLineSpan.StartLinePosition.Line; if (formClass == null) { ThrowErrorAndTerminate("Form class declaration not found in:\r\n" + designerFile); return; } #endregion #region Find start/end line indexes of form class and InitializeComponent() method var formClassLineSpan = formClass.GetLocation().GetLineSpan(); int formClassStartLine = formClassLineSpan.StartLinePosition.Line; int formClassEndLine = formClassLineSpan.EndLinePosition.Line; var initializeComponentMethod = (MethodDeclarationSyntax?)formClass.DescendantNodes() .FirstOrDefault( x => x is MethodDeclarationSyntax mds && mds.Identifier.Value?.ToString() == "InitializeComponent"); if (initializeComponentMethod == null) { ThrowErrorAndTerminate("InitializeComponent() method not found in:\r\n" + designerFile); return; } var initComponentLineSpan = initializeComponentMethod.GetLocation().GetLineSpan(); int initComponentStartLine = initComponentLineSpan.StartLinePosition.Line; int initComponentEndLine = initComponentLineSpan.EndLinePosition.Line; #endregion SyntaxNode?block = initializeComponentMethod.Body; if (block == null) { ThrowErrorAndTerminate("Body of InitializeComponent() method not found found in:\r\n" + designerFile); return; } #region Store control names, types, and attributes from field declarations foreach (SyntaxNode node in formClass.ChildNodes()) { if (node is FieldDeclarationSyntax fds) { string name = fds.Declaration.Variables.First().Identifier.ToString(); string type = fds.Declaration.Type.ToString(); int lastIndexOfDot = type.LastIndexOf('.'); string typeShort = lastIndexOfDot > -1 ? type.Substring(lastIndexOfDot + 1) : type; if (typeShort != "IContainer") { controlTypes[name] = typeShort; } // We should allow multiple attributes... if (HasAttribute(fds, GenAttributes.FenGenDoNotRemoveTextAttribute)) { controlAttributes[name] = GenAttributes.FenGenDoNotRemoveTextAttribute; } else if (HasAttribute(fds, GenAttributes.FenGenForceRemoveSizeAttribute)) { controlAttributes[name] = GenAttributes.FenGenForceRemoveSizeAttribute; } } } #endregion #region Find start line index of property-set section bool pastConstructorSection = false; int pastConstructorStartLine = -1; // Cheap way to know we're past the construction portion - we get the first header comment: // // ControlName // foreach (SyntaxTrivia tn in block.DescendantTrivia()) { if (tn.IsKind(SyntaxKind.SingleLineCommentTrivia) && tn.ToString().Trim() == "//") { pastConstructorStartLine = tn.GetLocation().GetLineSpan().StartLinePosition.Line; break; } } if (pastConstructorStartLine == -1) { ThrowErrorAndTerminate("Post-construction-section control comment header not found in:\r\n" + designerFile); return; } #endregion #region Process nodes foreach (SyntaxNode node in block.ChildNodes()) { if (!pastConstructorSection && node.GetLocation().GetLineSpan().StartLinePosition.Line >= pastConstructorStartLine) { pastConstructorSection = true; } var curNode = new NodeCustom(node); destNodes.Add(curNode); if (!pastConstructorSection || node is not ExpressionStatementSyntax exp) { continue; } #region Determine if a control is being added to a FlowLayoutPanel // If it is, we can remove an additional couple of layout properties (because it will be auto- // laid-out inside the FlowLayoutPanel). var ies = (InvocationExpressionSyntax?)exp.DescendantNodes().FirstOrDefault(x => x is InvocationExpressionSyntax); if (ies != null) { foreach (SyntaxNode mesN in ies.DescendantNodes()) { if (mesN is MemberAccessExpressionSyntax mes) { if (mes.Name.ToString() == "Add") { string nodeStr = mesN.ToString().Trim(); if (nodeStr.StartsWith("this.")) { nodeStr = nodeStr.Substring(5); } string nodeControlName = nodeStr.Substring(0, nodeStr.IndexOf('.')); if (nodeStr == nodeControlName + ".Controls.Add") { if (controlTypes.TryGetValue(nodeControlName, out string nodeType) && nodeType == "FlowLayoutPanel" && ies.ArgumentList.Arguments.Count == 1) { var arg = ies.ArgumentList.Arguments[0]; string argStr = arg.ToString().Trim(); if (argStr.StartsWith("this.")) { argStr = argStr.Substring(5); } controlsInFlowLayoutPanels.Add(argStr); } curNode.ControlName = ies.DescendantNodes().First(x => x is IdentifierNameSyntax).ToString(); CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.HasChildren = true; break; } } else if (mes.Name.ToString() == "SetToolTip") { string nodeStr = mesN.ToString().Trim(); if (nodeStr.StartsWith("this.")) { nodeStr = nodeStr.Substring(5); } string nodeControlName = nodeStr.Substring(0, nodeStr.IndexOf('.')); if (nodeStr == nodeControlName + ".SetToolTip" && controlTypes.TryGetValue(nodeControlName, out string nodeType) && nodeType == "ToolTip" && ies.ArgumentList.Arguments.Count == 2) { curNode.ControlName = ies.DescendantNodes().First(x => x is IdentifierNameSyntax).ToString(); curNode.PropName = "SetToolTip"; CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.HasSetToolTip = true; break; } } } } } #endregion var aes = (AssignmentExpressionSyntax?)exp.DescendantNodes().FirstOrDefault(x => x is AssignmentExpressionSyntax); if (aes?.Left is not MemberAccessExpressionSyntax left) { continue; } curNode.ControlName = left.DescendantNodes().First(x => x is IdentifierNameSyntax).ToString(); curNode.PropName = left.Name.ToString(); if (left.DescendantNodes().Any( n => (n is ThisExpressionSyntax && left.DescendantNodes().Count() == 2) || (n is not ThisExpressionSyntax && left.DescendantNodes().Count() == 1))) { CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.IsFormProperty = true; } #region Set control property switch (curNode.PropName) { case "AutoSize": case "Checked": { if (aes.Right is LiteralExpressionSyntax les) { string value = les.ToString(); CProps props = controlProperties.GetOrAddProps(curNode.ControlName); bool?boolVal = value switch { "true" => true, "false" => false, _ => null }; switch (curNode.PropName) { case "AutoSize": props.AutoSize = boolVal; break; case "Checked": props.Checked = boolVal; break; } } break; } case "Size": case "MinimumSize": { if (aes.Right is ObjectCreationExpressionSyntax oce && oce.Type.ToString() == "System.Drawing.Size" && oce.ArgumentList?.Arguments.Count == 2 && int.TryParse(oce.ArgumentList.Arguments[0].ToString(), out int width) && int.TryParse(oce.ArgumentList.Arguments[1].ToString(), out int height)) { CProps props = controlProperties.GetOrAddProps(curNode.ControlName); switch (curNode.PropName) { case "Size": props.Size = new Size(width, height); break; case "MinimumSize": props.MinimumSize = new Size(width, height); break; } } break; } case "Icon": { if (aes.Right is MemberAccessExpressionSyntax maes) { string val = maes.ToString(); if (val.TrimEnd(';').EndsWith(".Resources.AngelLoader")) { CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.ExplicitAppIcon = true; } } break; } case "Location": { if (aes.Right is ObjectCreationExpressionSyntax oce && oce.Type.ToString() == "System.Drawing.Point" && oce.ArgumentList?.Arguments.Count == 2 && int.TryParse(oce.ArgumentList.Arguments[0].ToString(), out int x) && int.TryParse(oce.ArgumentList.Arguments[1].ToString(), out int y)) { CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.Location = new Point(x, y); } break; } case "Name": case "Text": case "HeaderText": case "ToolTipText": { CProps props = controlProperties.GetOrAddProps(curNode.ControlName); switch (curNode.PropName) { case "Name": props.HasName = true; break; case "Text": props.HasText = true; break; case "HeaderText": props.HasHeaderText = true; break; case "ToolTipText": props.HasToolTipText = true; break; } break; } case "Anchor": { SyntaxNode[] anchorStyles = aes.Right.DescendantNodesAndSelf() .Where(x => x is MemberAccessExpressionSyntax && x.ToString().StartsWith("System.Windows.Forms.AnchorStyles.")) .ToArray(); CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.HasDefaultAnchor = anchorStyles.Length == 2 && Array.Find(anchorStyles, x => x.ToString() == "System.Windows.Forms.AnchorStyles.Top") != null && Array.Find(anchorStyles, x => x.ToString() == "System.Windows.Forms.AnchorStyles.Left") != null; break; } case "Dock": { if (aes.Right is MemberAccessExpressionSyntax mae) { CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.DockIsFill = mae.ToString() == "System.Windows.Forms.DockStyle.Fill"; } break; } case "CheckState": { if (aes.Right is MemberAccessExpressionSyntax les) { CProps props = controlProperties.GetOrAddProps(curNode.ControlName); props.CheckState = les.ToString() switch { "System.Windows.Forms.CheckState.Checked" => CheckState.Checked, "System.Windows.Forms.CheckState.Unchecked" => CheckState.Unchecked, "System.Windows.Forms.CheckState.Indeterminate" => CheckState.Indeterminate, _ => null }; } break; } } #endregion } #endregion #region Edit or elide property sets based on control's other properties for (int i = 0; i < destNodes.Count; i++) { var destNode = destNodes[i]; if (destNode.ControlName.IsEmpty() || !controlProperties.TryGetValue(destNode.ControlName, out CProps props)) { continue; } foreach (SyntaxTrivia t in destNode.Node.DescendantTrivia()) { if (t.Kind() == SyntaxKind.SingleLineCommentTrivia) { destNode.Comments.Add(t.ToString()); } } if (destNode.PropName == "Name" && props.HasName) { destNode.IgnoreExceptForComments = true; } else if (destNode.PropName == "Text" && props.HasText && (!controlAttributes.TryGetValue(destNode.ControlName, out string attr) || attr != GenAttributes.FenGenDoNotRemoveTextAttribute)) { if (props.IsFormProperty) { // Hack to avoid knowing the black art of how to construct code with Roslyn... // Copied from SettingsForm old manual init: // Un-obvious hack: If we DON'T set Text to something, anything, here, then first render (if paths tab // is the startup tab) is really slow. We just set a one-char blank space to prevent that(?!) Probably // something to do with this activating some kind of render routine beforehand... I guess... who knows... destNode.OverrideLine = " // Hack to prevent slow first render on some forms if Text is blank\r\n" + " this.Text = \" \";"; } else { destNode.IgnoreExceptForComments = true; } } else if (destNode.PropName == "HeaderText" && props.HasHeaderText && (!controlAttributes.TryGetValue(destNode.ControlName, out attr) || attr != GenAttributes.FenGenDoNotRemoveHeaderTextAttribute)) { destNode.IgnoreExceptForComments = true; } else if (destNode.PropName == "ToolTipText" && props.HasToolTipText && (!controlAttributes.TryGetValue(destNode.ControlName, out attr) || attr != GenAttributes.FenGenDoNotRemoveToolTipTextAttribute)) { destNode.IgnoreExceptForComments = true; } else if (destNode.PropName == "SetToolTip" && props.HasSetToolTip && (!controlAttributes.TryGetValue(destNode.ControlName, out attr) || attr != GenAttributes.FenGenDoNotRemoveToolTipTextAttribute)) { destNode.IgnoreExceptForComments = true; } else if (destNode.PropName == "Anchor" && ( props.HasDefaultAnchor == true || controlsInFlowLayoutPanels.Contains(destNode.ControlName) ) ) { destNode.IgnoreExceptForComments = true; } else if (destNode.PropName == "Icon" && props.ExplicitAppIcon && props.IsFormProperty) { destNode.OverrideLine = " this.Icon = AngelLoader.Forms.AL_Icon.AngelLoader;"; } else if (destNode.PropName == "Location" && ( (props.Location?.X == 0 && props.Location?.Y == 0) || props.DockIsFill || controlsInFlowLayoutPanels.Contains(destNode.ControlName) ) ) { destNode.IgnoreExceptForComments = true; } else if (destNode.PropName == "Size" && // Keep size if a control has subcontrols, because they might depend on it for layout // (eg. if they're anchored right or bottom) !props.HasChildren && ( (controlAttributes.TryGetValue(destNode.ControlName, out attr) && attr == GenAttributes.FenGenForceRemoveSizeAttribute) || // If anchor is anything other than top-left, we need to keep the size, for reasons I'm // unable to think how to explain well at the moment but you can figure it out, it's like // when we go to position it, it needs to know its size so it can properly position itself // relative to its anchor... you know. ((props.HasDefaultAnchor != false) && ( (props.Size != null && props.MinimumSize != null && props.Size == props.MinimumSize) || (props.Size != null && props.AutoSize == true) || props.AutoSize == true )) ) ) { destNode.IgnoreExceptForComments = true; } else if (destNode.PropName == "CheckState" && ((props.Checked == true && props.CheckState == CheckState.Checked) || (props.Checked == false && props.CheckState == CheckState.Unchecked))) { destNode.IgnoreExceptForComments = true; } } #endregion // TODO: Auto-gen the ifdeffed method call switch var w = new CodeWriters.IndentingWriter(fileScopedNamespace ? 0 : 1, fileScopedNamespace); #region Create final destination file lines // By starting at the namespace declaration, we skip copying the #define FenGen_* thing which would // throw us an exception for not being in a .Designer.cs file for (int i = namespaceDeclarationStartLine; i <= formClassStartLine; i++) { w.AppendRawString(sourceLines[i] + "\r\n"); } w.WL("{"); w.WL("/// <summary>"); w.WL("/// Custom generated component initializer with cruft removed."); w.WL("/// </summary>"); w.WL("private void InitializeComponentSlim()"); w.WL("{"); foreach (NodeCustom node in destNodes) { if (node.IgnoreExceptForComments) { foreach (string line in node.Comments) { w.WL(line); } } else { // We might be multiple lines so just write each out individually, so we're affected by the // writer's formatting string finalLine = !node.OverrideLine.IsEmpty() ? node.OverrideLine : node.Node.ToFullString().TrimEnd('\r', '\n'); string[] finalLineSplit = finalLine.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); for (int i = 0; i < finalLineSplit.Length; i++) { w.WL(finalLineSplit[i]); } } } w.WL("}"); w.CloseClassAndNamespace(); #endregion // UTF8 with BOM or else Visual Studio complains about "different" encoding File.WriteAllText(destFile, w.ToString(), Encoding.UTF8); #region Add ifdefs around original InitializeComponent() method bool foundIfDEBUG = false; bool foundEndIfDEBUG = false; for (int i = initComponentStartLine; i > formClassStartLine; i--) { if (sourceLines[i].Trim() == "#if DEBUG") { foundIfDEBUG = true; break; } } if (foundIfDEBUG) { for (int i = initComponentEndLine; i < formClassEndLine; i++) { if (sourceLines[i].Trim() == "#endif") { foundEndIfDEBUG = true; break; } } } if (!foundIfDEBUG && !foundEndIfDEBUG) { sourceLines.Insert(initComponentStartLine - 4, "#if DEBUG"); sourceLines.Insert(initComponentEndLine + 2, "#endif"); } #endregion while (sourceLines[sourceLines.Count - 1].IsWhiteSpace()) { sourceLines.RemoveAt(sourceLines.Count - 1); } // Designer.cs files want UTF8 with BOM I guess... File.WriteAllLines(designerFile, sourceLines, Encoding.UTF8); } }