├── resources ├── ue4_smarter_macro_indenting_off.gif └── ue4_smarter_macro_indenting_on.gif ├── LICENSE ├── README.md ├── ue4_smarter_macro_indenting_vs2013-2015.vcmd └── ue4_smarter_macro_indenting_vs2017-2019.vcmd /resources/ue4_smarter_macro_indenting_off.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackalyze/ue4-vs-extensions/HEAD/resources/ue4_smarter_macro_indenting_off.gif -------------------------------------------------------------------------------- /resources/ue4_smarter_macro_indenting_on.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackalyze/ue4-vs-extensions/HEAD/resources/ue4_smarter_macro_indenting_on.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 hackalyze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### UE4 Smarter Macro Indenting 2 | This extension was designed to fix the unnecessary and annoying "smart" indenting that Visual Studio likes to do around various UE4 macros. It attempts to fix these indents when your current line proceeds any of the following UE4 macros: 3 | * UPROPERTY 4 | * UFUNCTION 5 | * GENERATED_BODY 6 | * GENERATED_UCLASS_BODY 7 | * GENERATED_USTRUCT_BODY 8 | * GENERATED_UINTERFACE_BODY 9 | * GENERATED_IINTERFACE_BODY 10 | 11 | ### Demo 12 | | Before | After | 13 | | --- | --- | 14 | | ![Example](resources/ue4_smarter_macro_indenting_off.gif "Normal Smart Indention in Visual Studio") | ![Example](resources/ue4_smarter_macro_indenting_on.gif "Extension enabled") | 15 | 16 | ### Installation 17 | 18 | 1. Download and install the Visual Commander extension for Visual Studio from here: https://vlasovstudio.com/visual-commander/ 19 | 2. Go to VCmd -> Import and import the ue4_smarter_macro_indenting*.vcmd that corresponds to your version of visual studio. For visual studio 2017/2019+, use ue4_smarter_macro_indenting_vs2017-2019.vcmd, for earlier versions use ue4_smarter_macro_indenting_vs2013-2015.vcmd. 20 | 3. Go to VCmd -> Extensions to open the Extensions panel 21 | 4. Check the "Enabled" checkbox for the "UE4 Fix Indent" extension. 22 | 5. Make sure Indenting is set to "Smart" in Tools -> Options -> Text Editor -> C/C++ -> Tabs. 23 | 24 | > If "VCmd" doesn't appear on your Visual Studio toolbar after installing Visual Commander, you may need to restart Visual Studio. 25 | -------------------------------------------------------------------------------- /ue4_smarter_macro_indenting_vs2013-2015.vcmd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1 7 | UE4 Smarter Macro Indenting 8 | 9 | using EnvDTE; 10 | using EnvDTE80; 11 | using Microsoft.VisualStudio; 12 | using System; 13 | using System.Text.RegularExpressions; 14 | using System.Windows.Forms; 15 | 16 | public class E : VisualCommanderExt.IExtension 17 | { 18 | private EnvDTE80.DTE2 _DTE; 19 | private EnvDTE80.TextDocumentKeyPressEvents _textDocumentKeyPressEvents; 20 | private static readonly string MACRO_REGEX = @"^(?<leading_whitespace>[\s]*)(UPROPERTY|UFUNCTION|GENERATED_(USTRUCT_|UCLASS_|(U|I)INTERFACE_)?BODY)\(.*"; 21 | private static readonly string TEXT_WITH_WS_REGEX = @"(?<leading_whitespace>[\s]*)\S+"; 22 | 23 | private CommandEvents _pasteEvent; 24 | private string _beforeText; 25 | private int _numSelectedLines; 26 | 27 | public void SetSite(DTE2 DTE, Microsoft.VisualStudio.Shell.Package package) { 28 | _DTE = DTE; 29 | EnvDTE80.Events2 events2 = (EnvDTE80.Events2)DTE.Events; 30 | _textDocumentKeyPressEvents = events2.get_TextDocumentKeyPressEvents(null); 31 | _textDocumentKeyPressEvents.AfterKeyPress += AfterKeyPress; 32 | 33 | var pasteGuid = typeof(VSConstants.VSStd97CmdID).GUID.ToString("B"); 34 | var pasteID = (int)VSConstants.VSStd97CmdID.Paste; 35 | _pasteEvent = _DTE.Events.CommandEvents[pasteGuid, pasteID]; 36 | _pasteEvent.BeforeExecute += BeforePaste; 37 | _pasteEvent.AfterExecute += AfterPaste; 38 | } 39 | 40 | public void Close() { 41 | _textDocumentKeyPressEvents.AfterKeyPress -= AfterKeyPress; 42 | _pasteEvent.AfterExecute -= AfterPaste; 43 | _pasteEvent.BeforeExecute -= BeforePaste; 44 | } 45 | 46 | private string GetActiveDocumentText() { 47 | TextDocument doc = (TextDocument)(_DTE.ActiveDocument.Object("TextDocument")); 48 | var editPoint = doc.StartPoint.CreateEditPoint(); 49 | return editPoint.GetText(doc.EndPoint); 50 | } 51 | 52 | private void BeforePaste(string Guid, int ID, Object CustomIn, Object CustomOut, ref bool CancelDefault) { 53 | _beforeText = GetActiveDocumentText(); 54 | TextSelection sel = (TextSelection)_DTE.ActiveDocument.Selection; 55 | _numSelectedLines = sel.Text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Length; 56 | } 57 | 58 | private void AfterPaste(string Guid, int ID, Object CustomIn, Object CustomOut) { 59 | string afterText = GetActiveDocumentText(); 60 | string[] beforeLines = _beforeText.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); 61 | string[] afterLines = afterText.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); 62 | int pasteLength = (afterLines.Length - beforeLines.Length) + _numSelectedLines; 63 | TextSelection sel = (TextSelection)_DTE.ActiveDocument.Selection; 64 | var editPoint = sel.ActivePoint.CreateEditPoint(); 65 | bool underMacro = false; 66 | string lastWhiteSpace = ""; 67 | bool openedUndoContext = false; 68 | int startPasteLine = sel.ActivePoint.Line - pasteLength; 69 | int macroLineNum = startPasteLine + 1; 70 | // Search up the document from the start of our paste until we find a line with text on it 71 | // so we can then determine if this line contains a macro we care about 72 | while (--macroLineNum >= 1) { 73 | string macroLine = editPoint.GetLines(macroLineNum, macroLineNum + 1); 74 | if (!Regex.IsMatch(macroLine, @"\S+")) { 75 | continue; 76 | } 77 | var macroMatch = Regex.Match(macroLine, MACRO_REGEX); 78 | if (macroMatch.Success) { 79 | underMacro = true; 80 | lastWhiteSpace = macroMatch.Groups["leading_whitespace"].ToString(); 81 | } 82 | break; 83 | } 84 | 85 | if (!_DTE.UndoContext.IsOpen) { 86 | openedUndoContext = true; 87 | // Open the UndoContext so all of our changes can be undone with a single undo 88 | _DTE.UndoContext.Open("FixUE4MacroIndents", false); 89 | } 90 | try { 91 | for (int onLine = startPasteLine; onLine < afterLines.Length; onLine++) { 92 | var line = afterLines[onLine]; 93 | // We only need to potentially fix the newly pasted lines 94 | if (onLine >= startPasteLine + pasteLength) { 95 | sel.GotoLine(onLine, false); 96 | sel.MoveToPoint(editPoint); 97 | break; 98 | } 99 | if (underMacro) { 100 | var varMatch = Regex.Match(line, TEXT_WITH_WS_REGEX); 101 | if (varMatch.Success && varMatch.Groups["leading_whitespace"].ToString() != lastWhiteSpace) { 102 | sel.GotoLine(onLine + 1, false); 103 | sel.DeleteWhitespace(EnvDTE.vsWhitespaceOptions.vsWhitespaceOptionsHorizontal); 104 | sel.Insert(lastWhiteSpace); 105 | underMacro = false; 106 | // This line has been modified, so change it for the MACRO_REGEX matching below 107 | line = lastWhiteSpace + line.Trim(); 108 | } 109 | } 110 | // Its important to check if the current line is a macro 111 | // even if we just fixed the spacing of the current line 112 | var macroMatch = Regex.Match(line, MACRO_REGEX); 113 | if (macroMatch.Success) { 114 | underMacro = true; 115 | lastWhiteSpace = macroMatch.Groups["leading_whitespace"].ToString(); 116 | } else if (Regex.Match(line, @"\S+").Success) { 117 | underMacro = false; 118 | } 119 | } 120 | } finally { 121 | if (openedUndoContext) { 122 | _DTE.UndoContext.Close(); 123 | } 124 | } 125 | } 126 | 127 | private void AfterKeyPress(string key, TextSelection sel, bool completion) { 128 | // Only semicolons or carriage returns should cause an indentation that need to be fixed 129 | if (key != ";" && key != "\r") { 130 | return; 131 | } 132 | // Make sure we're using smart indent 133 | EnvDTE.Properties textEditorC = _DTE.get_Properties("TextEditor", "C/C++"); 134 | if ((int)textEditorC.Item("IndentStyle").Value != 2) { 135 | return; 136 | } 137 | if (key == ";") { 138 | // Make sure we're auto-formatting on semicolons 139 | EnvDTE.Properties textEditorCSpecific = _DTE.get_Properties("TextEditor", "C/C++ Specific"); 140 | if (!(bool)textEditorCSpecific.Item("AutoFormatOnSemicolon").Value) { 141 | return; 142 | } 143 | } 144 | 145 | var doc = _DTE.ActiveDocument; 146 | var editPoint = sel.ActivePoint.CreateEditPoint(); 147 | string macroLine = null; 148 | var macroLineNum = sel.ActivePoint.Line; 149 | // Search up the document from our current line until we find a line with text on it 150 | // so we can then determine if this line contains a macro we care about 151 | var found = false; 152 | while (--macroLineNum >= 1) { 153 | macroLine = editPoint.GetLines(macroLineNum, macroLineNum + 1); 154 | if (!Regex.IsMatch(macroLine, @"\S+")) { 155 | continue; 156 | } 157 | found = true; 158 | break; 159 | } 160 | if (!found) { 161 | return; 162 | } 163 | var macroMatch = Regex.Match(macroLine, MACRO_REGEX); 164 | if (macroMatch.Success) { 165 | // Goto and select our current line, can't do this in a single GotoLine call for some reason 166 | sel.GotoLine(sel.ActivePoint.Line, false); 167 | sel.SelectLine(); 168 | if (Regex.IsMatch(sel.Text, @"\S+")) { 169 | // If the line below the macro has text, undo the indent it just did 170 | doc.Undo(); 171 | } else { 172 | // If the line below the macro is empty, add matching whitespace to the beginning of this line to match it up with the macro 173 | sel.MoveToPoint(editPoint); 174 | sel.DeleteWhitespace(EnvDTE.vsWhitespaceOptions.vsWhitespaceOptionsHorizontal); 175 | sel.Insert(macroMatch.Groups["leading_whitespace"].ToString()); 176 | } 177 | } 178 | } 179 | } 180 | 181 | Extension 182 | CS 183 | v4.0 184 | false 185 | false 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /ue4_smarter_macro_indenting_vs2017-2019.vcmd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1 7 | UE4 Smarter Macro Indenting 8 | 9 | using EnvDTE; 10 | using EnvDTE80; 11 | using Microsoft.VisualStudio; 12 | using System; 13 | using System.Text.RegularExpressions; 14 | using System.Windows.Forms; 15 | 16 | public class E : VisualCommanderExt.IExtension 17 | { 18 | private EnvDTE80.DTE2 _DTE; 19 | private EnvDTE80.TextDocumentKeyPressEvents _textDocumentKeyPressEvents; 20 | private static readonly string MACRO_REGEX = @"^(?<leading_whitespace>[\s]*)(UPROPERTY|UFUNCTION|GENERATED_(USTRUCT_|UCLASS_|(U|I)INTERFACE_)?BODY)\(.*"; 21 | private static readonly string TEXT_WITH_WS_REGEX = @"(?<leading_whitespace>[\s]*)\S+"; 22 | 23 | private CommandEvents _pasteEvent; 24 | private string _beforeText; 25 | private int _numSelectedLines; 26 | 27 | public void SetSite(DTE2 DTE, Microsoft.VisualStudio.Shell.Package package) { 28 | _DTE = DTE; 29 | EnvDTE80.Events2 events2 = (EnvDTE80.Events2)DTE.Events; 30 | _textDocumentKeyPressEvents = events2.get_TextDocumentKeyPressEvents(null); 31 | _textDocumentKeyPressEvents.AfterKeyPress += AfterKeyPress; 32 | 33 | var pasteGuid = typeof(VSConstants.VSStd97CmdID).GUID.ToString("B"); 34 | var pasteID = (int)VSConstants.VSStd97CmdID.Paste; 35 | _pasteEvent = _DTE.Events.CommandEvents[pasteGuid, pasteID]; 36 | _pasteEvent.BeforeExecute += BeforePaste; 37 | _pasteEvent.AfterExecute += AfterPaste; 38 | } 39 | 40 | public void Close() { 41 | _textDocumentKeyPressEvents.AfterKeyPress -= AfterKeyPress; 42 | _pasteEvent.AfterExecute -= AfterPaste; 43 | _pasteEvent.BeforeExecute -= BeforePaste; 44 | } 45 | 46 | private string GetActiveDocumentText() { 47 | TextDocument doc = (TextDocument)(_DTE.ActiveDocument.Object("TextDocument")); 48 | var editPoint = doc.StartPoint.CreateEditPoint(); 49 | return editPoint.GetText(doc.EndPoint); 50 | } 51 | 52 | private void BeforePaste(string Guid, int ID, Object CustomIn, Object CustomOut, ref bool CancelDefault) { 53 | _beforeText = GetActiveDocumentText(); 54 | TextSelection sel = (TextSelection)_DTE.ActiveDocument.Selection; 55 | _numSelectedLines = sel.Text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Length; 56 | } 57 | 58 | private void AfterPaste(string Guid, int ID, Object CustomIn, Object CustomOut) { 59 | string afterText = GetActiveDocumentText(); 60 | string[] beforeLines = _beforeText.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); 61 | string[] afterLines = afterText.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); 62 | int pasteLength = (afterLines.Length - beforeLines.Length) + _numSelectedLines; 63 | TextSelection sel = (TextSelection)_DTE.ActiveDocument.Selection; 64 | var editPoint = sel.ActivePoint.CreateEditPoint(); 65 | bool underMacro = false; 66 | string lastWhiteSpace = ""; 67 | bool openedUndoContext = false; 68 | int startPasteLine = sel.ActivePoint.Line - pasteLength; 69 | int macroLineNum = startPasteLine + 1; 70 | // Search up the document from the start of our paste until we find a line with text on it 71 | // so we can then determine if this line contains a macro we care about 72 | while (--macroLineNum >= 1) { 73 | string macroLine = editPoint.GetLines(macroLineNum, macroLineNum + 1); 74 | if (!Regex.IsMatch(macroLine, @"\S+")) { 75 | continue; 76 | } 77 | var macroMatch = Regex.Match(macroLine, MACRO_REGEX); 78 | if (macroMatch.Success) { 79 | underMacro = true; 80 | lastWhiteSpace = macroMatch.Groups["leading_whitespace"].ToString(); 81 | } 82 | break; 83 | } 84 | 85 | if (!_DTE.UndoContext.IsOpen) { 86 | openedUndoContext = true; 87 | // Open the UndoContext so all of our changes can be undone with a single undo 88 | _DTE.UndoContext.Open("FixUE4MacroIndents", false); 89 | } 90 | try { 91 | for (int onLine = startPasteLine; onLine < afterLines.Length; onLine++) { 92 | var line = afterLines[onLine]; 93 | // We only need to potentially fix the newly pasted lines 94 | if (onLine >= startPasteLine + pasteLength) { 95 | sel.GotoLine(onLine, false); 96 | sel.MoveToPoint(editPoint); 97 | break; 98 | } 99 | if (underMacro) { 100 | var varMatch = Regex.Match(line, TEXT_WITH_WS_REGEX); 101 | if (varMatch.Success && varMatch.Groups["leading_whitespace"].ToString() != lastWhiteSpace) { 102 | sel.GotoLine(onLine + 1, false); 103 | sel.DeleteWhitespace(EnvDTE.vsWhitespaceOptions.vsWhitespaceOptionsHorizontal); 104 | sel.Insert(lastWhiteSpace); 105 | underMacro = false; 106 | // This line has been modified, so change it for the MACRO_REGEX matching below 107 | line = lastWhiteSpace + line.Trim(); 108 | } 109 | } 110 | // Its important to check if the current line is a macro 111 | // even if we just fixed the spacing of the current line 112 | var macroMatch = Regex.Match(line, MACRO_REGEX); 113 | if (macroMatch.Success) { 114 | underMacro = true; 115 | lastWhiteSpace = macroMatch.Groups["leading_whitespace"].ToString(); 116 | } else if (Regex.Match(line, @"\S+").Success) { 117 | underMacro = false; 118 | } 119 | } 120 | } finally { 121 | if (openedUndoContext) { 122 | _DTE.UndoContext.Close(); 123 | } 124 | } 125 | } 126 | 127 | private void AfterKeyPress(string key, TextSelection sel, bool completion) { 128 | // Only semicolons or carriage returns should cause an indentation that need to be fixed 129 | if (key != ";" && key != "\r") { 130 | return; 131 | } 132 | // Make sure we're using smart indent 133 | EnvDTE.Properties textEditorC = _DTE.get_Properties("TextEditor", "C/C++"); 134 | if ((int)textEditorC.Item("IndentStyle").Value != 2) { 135 | return; 136 | } 137 | if (key == ";") { 138 | // Make sure we're auto-formatting on semicolons 139 | EnvDTE.Properties textEditorCSpecific = _DTE.get_Properties("TextEditor", "C/C++ Specific"); 140 | if (!(bool)textEditorCSpecific.Item("AutoFormatOnSemicolon").Value) { 141 | return; 142 | } 143 | } 144 | 145 | var doc = _DTE.ActiveDocument; 146 | var editPoint = sel.ActivePoint.CreateEditPoint(); 147 | string macroLine = null; 148 | var macroLineNum = sel.ActivePoint.Line; 149 | // Search up the document from our current line until we find a line with text on it 150 | // so we can then determine if this line contains a macro we care about 151 | var found = false; 152 | while (--macroLineNum >= 1) { 153 | macroLine = editPoint.GetLines(macroLineNum, macroLineNum + 1); 154 | if (!Regex.IsMatch(macroLine, @"\S+")) { 155 | continue; 156 | } 157 | found = true; 158 | break; 159 | } 160 | if (!found) { 161 | return; 162 | } 163 | var macroMatch = Regex.Match(macroLine, MACRO_REGEX); 164 | if (macroMatch.Success) { 165 | // Goto and select our current line, can't do this in a single GotoLine call for some reason 166 | sel.GotoLine(sel.ActivePoint.Line, false); 167 | sel.SelectLine(); 168 | if (Regex.IsMatch(sel.Text, @"\S+")) { 169 | // If the line below the macro has text, undo the indent it just did 170 | doc.Undo(); 171 | } else { 172 | // If the line below the macro is empty, add matching whitespace to the beginning of this line to match it up with the macro 173 | sel.MoveToPoint(editPoint); 174 | sel.DeleteWhitespace(EnvDTE.vsWhitespaceOptions.vsWhitespaceOptionsHorizontal); 175 | sel.Insert(macroMatch.Groups["leading_whitespace"].ToString()); 176 | } 177 | } 178 | } 179 | } 180 | 181 | Microsoft.VisualStudio.Shell.Framework 182 | 183 | Extension 184 | CS 185 | v4.0 186 | true 187 | false 188 | 189 | 190 | 191 | --------------------------------------------------------------------------------