r299543 - clang-format-vsix: Add "Format on Save" feature

Antonio Maiorano via cfe-commits cfe-commits at lists.llvm.org
Wed Apr 5 07:13:45 PDT 2017


Author: amaiorano
Date: Wed Apr  5 09:13:45 2017
New Revision: 299543

URL: http://llvm.org/viewvc/llvm-project?rev=299543&view=rev
Log:
clang-format-vsix: Add "Format on Save" feature

This change adds a feature to the clang-format VS extension that optionally
enables the automatic formatting of documents when saving. Since developers
always need to save their files, this eases the workflow of making sure source
files are properly formatted.

Differential Revision: https://reviews.llvm.org/D29221

Added:
    cfe/trunk/tools/clang-format-vs/ClangFormat/RunningDocTableEventsDispatcher.cs
    cfe/trunk/tools/clang-format-vs/ClangFormat/Vsix.cs
Modified:
    cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormat.csproj
    cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs

Modified: cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormat.csproj
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormat.csproj?rev=299543&r1=299542&r2=299543&view=diff
==============================================================================
--- cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormat.csproj (original)
+++ cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormat.csproj Wed Apr  5 09:13:45 2017
@@ -214,6 +214,8 @@
     </Compile>
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="PkgCmdID.cs" />
+    <Compile Include="RunningDocTableEventsDispatcher.cs" />
+    <Compile Include="Vsix.cs" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Resources.resx">

Modified: cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs?rev=299543&r1=299542&r2=299543&view=diff
==============================================================================
--- cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs (original)
+++ cfe/trunk/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs Wed Apr  5 09:13:45 2017
@@ -12,12 +12,11 @@
 //
 //===----------------------------------------------------------------------===//
 
-using Microsoft.VisualStudio.Editor;
+using EnvDTE;
 using Microsoft.VisualStudio.Shell;
 using Microsoft.VisualStudio.Shell.Interop;
 using Microsoft.VisualStudio.Text;
 using Microsoft.VisualStudio.Text.Editor;
-using Microsoft.VisualStudio.TextManager.Interop;
 using System;
 using System.Collections;
 using System.ComponentModel;
@@ -25,6 +24,7 @@ using System.ComponentModel.Design;
 using System.IO;
 using System.Runtime.InteropServices;
 using System.Xml.Linq;
+using System.Linq;
 
 namespace LLVM.ClangFormat
 {
@@ -36,6 +36,17 @@ namespace LLVM.ClangFormat
         private string fallbackStyle = "LLVM";
         private bool sortIncludes = false;
         private string style = "file";
+        private bool formatOnSave = false;
+        private string formatOnSaveFileExtensions =
+            ".c;.cpp;.cxx;.cc;.tli;.tlh;.h;.hh;.hpp;.hxx;.hh;.inl" +
+            ".java;.js;.ts;.m;.mm;.proto;.protodevel;.td";
+
+        public OptionPageGrid Clone()
+        {
+            // Use MemberwiseClone to copy value types.
+            var clone = (OptionPageGrid)MemberwiseClone();
+            return clone;
+        }
 
         public class StyleConverter : TypeConverter
         {
@@ -74,7 +85,7 @@ namespace LLVM.ClangFormat
             }
         }
 
-        [Category("LLVM/Clang")]
+        [Category("Format Options")]
         [DisplayName("Style")]
         [Description("Coding style, currently supports:\n" +
                      "  - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" +
@@ -121,7 +132,7 @@ namespace LLVM.ClangFormat
             }
         }
 
-        [Category("LLVM/Clang")]
+        [Category("Format Options")]
         [DisplayName("Assume Filename")]
         [Description("When reading from stdin, clang-format assumes this " +
                      "filename to look for a style config file (with 'file' style) " +
@@ -142,7 +153,7 @@ namespace LLVM.ClangFormat
             }
         }
 
-        [Category("LLVM/Clang")]
+        [Category("Format Options")]
         [DisplayName("Fallback Style")]
         [Description("The name of the predefined style used as a fallback in case clang-format " +
                      "is invoked with 'file' style, but can not find the configuration file.\n" +
@@ -154,7 +165,7 @@ namespace LLVM.ClangFormat
             set { fallbackStyle = value; }
         }
 
-        [Category("LLVM/Clang")]
+        [Category("Format Options")]
         [DisplayName("Sort includes")]
         [Description("Sort touched include lines.\n\n" +
                      "See also: http://clang.llvm.org/docs/ClangFormat.html.")]
@@ -163,20 +174,48 @@ namespace LLVM.ClangFormat
             get { return sortIncludes; }
             set { sortIncludes = value; }
         }
+
+        [Category("Format On Save")]
+        [DisplayName("Enable")]
+        [Description("Enable running clang-format when modified files are saved. " +
+                     "Will only format if Style is found (ignores Fallback Style)."
+            )]
+        public bool FormatOnSave
+        {
+            get { return formatOnSave; }
+            set { formatOnSave = value; }
+        }
+
+        [Category("Format On Save")]
+        [DisplayName("File extensions")]
+        [Description("When formatting on save, clang-format will be applied only to " +
+                     "files with these extensions.")]
+        public string FormatOnSaveFileExtensions
+        {
+            get { return formatOnSaveFileExtensions; }
+            set { formatOnSaveFileExtensions = value; }
+        }
     }
 
     [PackageRegistration(UseManagedResourcesOnly = true)]
     [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
     [ProvideMenuResource("Menus.ctmenu", 1)]
+    [ProvideAutoLoad(UIContextGuids80.SolutionExists)] // Load package on solution load
     [Guid(GuidList.guidClangFormatPkgString)]
     [ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)]
     public sealed class ClangFormatPackage : Package
     {
         #region Package Members
+
+        RunningDocTableEventsDispatcher _runningDocTableEventsDispatcher;
+
         protected override void Initialize()
         {
             base.Initialize();
 
+            _runningDocTableEventsDispatcher = new RunningDocTableEventsDispatcher(this);
+            _runningDocTableEventsDispatcher.BeforeSave += OnBeforeSave;
+
             var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
             if (commandService != null)
             {
@@ -195,6 +234,11 @@ namespace LLVM.ClangFormat
         }
         #endregion
 
+        OptionPageGrid GetUserOptions()
+        {
+            return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
+        }
+
         private void MenuItemCallback(object sender, EventArgs args)
         {
             var mc = sender as System.ComponentModel.Design.MenuCommand;
@@ -204,21 +248,45 @@ namespace LLVM.ClangFormat
             switch (mc.CommandID.ID)
             {
                 case (int)PkgCmdIDList.cmdidClangFormatSelection:
-                    FormatSelection();
+                    FormatSelection(GetUserOptions());
                     break;
 
                 case (int)PkgCmdIDList.cmdidClangFormatDocument:
-                    FormatDocument();
+                    FormatDocument(GetUserOptions());
                     break;
             }
         }
 
+        private static bool FileHasExtension(string filePath, string fileExtensions)
+        {
+            var extensions = fileExtensions.ToLower().Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+            return extensions.Contains(Path.GetExtension(filePath).ToLower());
+        }
+
+        private void OnBeforeSave(object sender, Document document)
+        {
+            var options = GetUserOptions();
+
+            if (!options.FormatOnSave)
+                return;
+
+            if (!FileHasExtension(document.FullName, options.FormatOnSaveFileExtensions))
+                return;
+
+            if (!Vsix.IsDocumentDirty(document))
+                return;
+
+            var optionsWithNoFallbackStyle = GetUserOptions().Clone();
+            optionsWithNoFallbackStyle.FallbackStyle = "none";
+            FormatDocument(document, optionsWithNoFallbackStyle);
+        }
+
         /// <summary>
         /// Runs clang-format on the current selection
         /// </summary>
-        private void FormatSelection()
+        private void FormatSelection(OptionPageGrid options)
         {
-            IWpfTextView view = GetCurrentView();
+            IWpfTextView view = Vsix.GetCurrentView();
             if (view == null)
                 // We're not in a text view.
                 return;
@@ -231,34 +299,43 @@ namespace LLVM.ClangFormat
             // of the file.
             if (start >= text.Length && text.Length > 0)
                 start = text.Length - 1;
-            string path = GetDocumentParent(view);
-            string filePath = GetDocumentPath(view);
+            string path = Vsix.GetDocumentParent(view);
+            string filePath = Vsix.GetDocumentPath(view);
 
-            RunClangFormatAndApplyReplacements(text, start, length, path, filePath, view);
+            RunClangFormatAndApplyReplacements(text, start, length, path, filePath, options, view);
         }
 
         /// <summary>
         /// Runs clang-format on the current document
         /// </summary>
-        private void FormatDocument()
+        private void FormatDocument(OptionPageGrid options)
+        {
+            FormatView(Vsix.GetCurrentView(), options);
+        }
+
+        private void FormatDocument(Document document, OptionPageGrid options)
+        {
+            FormatView(Vsix.GetDocumentView(document), options);
+        }
+
+        private void FormatView(IWpfTextView view, OptionPageGrid options)
         {
-            IWpfTextView view = GetCurrentView();
             if (view == null)
                 // We're not in a text view.
                 return;
 
-            string filePath = GetDocumentPath(view);
+            string filePath = Vsix.GetDocumentPath(view);
             var path = Path.GetDirectoryName(filePath);
             string text = view.TextBuffer.CurrentSnapshot.GetText();
 
-            RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, view);
+            RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, options, view);
         }
 
-        private void RunClangFormatAndApplyReplacements(string text, int offset, int length, string path, string filePath, IWpfTextView view)
+        private void RunClangFormatAndApplyReplacements(string text, int offset, int length, string path, string filePath, OptionPageGrid options, IWpfTextView view)
         {
             try
             {
-                string replacements = RunClangFormat(text, offset, length, path, filePath);
+                string replacements = RunClangFormat(text, offset, length, path, filePath, options);
                 ApplyClangFormatReplacements(replacements, view);
             }
             catch (Exception e)
@@ -283,7 +360,7 @@ namespace LLVM.ClangFormat
         /// 
         /// Formats the text range starting at offset of the given length.
         /// </summary>
-        private string RunClangFormat(string text, int offset, int length, string path, string filePath)
+        private static string RunClangFormat(string text, int offset, int length, string path, string filePath, OptionPageGrid options)
         {
             string vsixPath = Path.GetDirectoryName(
                 typeof(ClangFormatPackage).Assembly.Location);
@@ -293,16 +370,16 @@ namespace LLVM.ClangFormat
             process.StartInfo.FileName = vsixPath + "\\clang-format.exe";
             // Poor man's escaping - this will not work when quotes are already escaped
             // in the input (but we don't need more).
-            string style = GetStyle().Replace("\"", "\\\"");
-            string fallbackStyle = GetFallbackStyle().Replace("\"", "\\\"");
+            string style = options.Style.Replace("\"", "\\\"");
+            string fallbackStyle = options.FallbackStyle.Replace("\"", "\\\"");
             process.StartInfo.Arguments = " -offset " + offset +
                                           " -length " + length +
                                           " -output-replacements-xml " +
                                           " -style \"" + style + "\"" +
                                           " -fallback-style \"" + fallbackStyle + "\"";
-            if (GetSortIncludes())
+            if (options.SortIncludes)
               process.StartInfo.Arguments += " -sort-includes ";
-            string assumeFilename = GetAssumeFilename();
+            string assumeFilename = options.AssumeFilename;
             if (string.IsNullOrEmpty(assumeFilename))
                 assumeFilename = filePath;
             if (!string.IsNullOrEmpty(assumeFilename))
@@ -352,7 +429,7 @@ namespace LLVM.ClangFormat
         /// <summary>
         /// Applies the clang-format replacements (xml) to the current view
         /// </summary>
-        private void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
+        private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
         {
             // clang-format returns no replacements if input text is empty
             if (replacements.Length == 0)
@@ -369,70 +446,5 @@ namespace LLVM.ClangFormat
             }
             edit.Apply();
         }
-
-        /// <summary>
-        /// Returns the currently active view if it is a IWpfTextView.
-        /// </summary>
-        private IWpfTextView GetCurrentView()
-        {
-            // The SVsTextManager is a service through which we can get the active view.
-            var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager));
-            IVsTextView textView;
-            textManager.GetActiveView(1, null, out textView);
-
-            // Now we have the active view as IVsTextView, but the text interfaces we need
-            // are in the IWpfTextView.
-            var userData = (IVsUserData)textView;
-            if (userData == null)
-                return null;
-            Guid guidWpfViewHost = DefGuidList.guidIWpfTextViewHost;
-            object host;
-            userData.GetData(ref guidWpfViewHost, out host);
-            return ((IWpfTextViewHost)host).TextView;
-        }
-
-        private string GetStyle()
-        {
-            var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
-            return page.Style;
-        }
-
-        private string GetAssumeFilename()
-        {
-            var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
-            return page.AssumeFilename;
-        }
-
-        private string GetFallbackStyle()
-        {
-            var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
-            return page.FallbackStyle;
-        }
-
-        private bool GetSortIncludes()
-        {
-            var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
-            return page.SortIncludes;
-        }
-
-        private string GetDocumentParent(IWpfTextView view)
-        {
-            ITextDocument document;
-            if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
-            {
-                return Directory.GetParent(document.FilePath).ToString();
-            }
-            return null;
-        }
-
-        private string GetDocumentPath(IWpfTextView view)
-        {
-            ITextDocument document;
-            if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
-            {
-                return document.FilePath;
-            }
-            return null;
-        }
     }
 }

Added: cfe/trunk/tools/clang-format-vs/ClangFormat/RunningDocTableEventsDispatcher.cs
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/tools/clang-format-vs/ClangFormat/RunningDocTableEventsDispatcher.cs?rev=299543&view=auto
==============================================================================
--- cfe/trunk/tools/clang-format-vs/ClangFormat/RunningDocTableEventsDispatcher.cs (added)
+++ cfe/trunk/tools/clang-format-vs/ClangFormat/RunningDocTableEventsDispatcher.cs Wed Apr  5 09:13:45 2017
@@ -0,0 +1,79 @@
+using EnvDTE;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using System.Linq;
+    
+namespace LLVM.ClangFormat
+{
+    // Exposes event sources for IVsRunningDocTableEvents3 events.
+    internal sealed class RunningDocTableEventsDispatcher : IVsRunningDocTableEvents3
+    {
+        private RunningDocumentTable _runningDocumentTable;
+        private DTE _dte;
+
+        public delegate void OnBeforeSaveHander(object sender, Document document);
+        public event OnBeforeSaveHander BeforeSave;
+
+        public RunningDocTableEventsDispatcher(Package package)
+        {
+            _runningDocumentTable = new RunningDocumentTable(package);
+            _runningDocumentTable.Advise(this);
+            _dte = (DTE)Package.GetGlobalService(typeof(DTE));
+        }
+
+        public int OnAfterAttributeChange(uint docCookie, uint grfAttribs)
+        {
+            return VSConstants.S_OK;
+        }
+
+        public int OnAfterAttributeChangeEx(uint docCookie, uint grfAttribs, IVsHierarchy pHierOld, uint itemidOld, string pszMkDocumentOld, IVsHierarchy pHierNew, uint itemidNew, string pszMkDocumentNew)
+        {
+            return VSConstants.S_OK;
+        }
+
+        public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame)
+        {
+            return VSConstants.S_OK;
+        }
+
+        public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
+        {
+            return VSConstants.S_OK;
+        }
+
+        public int OnAfterSave(uint docCookie)
+        {
+            return VSConstants.S_OK;
+        }
+
+        public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame)
+        {
+            return VSConstants.S_OK;
+        }
+
+        public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
+        {
+            return VSConstants.S_OK;
+        }
+
+        public int OnBeforeSave(uint docCookie)
+        {
+            if (BeforeSave != null)
+            {
+                var document = FindDocumentByCookie(docCookie);
+                if (document != null) // Not sure why this happens sometimes
+                {
+                    BeforeSave(this, FindDocumentByCookie(docCookie));
+                }
+            }
+            return VSConstants.S_OK;
+        }
+
+        private Document FindDocumentByCookie(uint docCookie)
+        {
+            var documentInfo = _runningDocumentTable.GetDocumentInfo(docCookie);
+            return _dte.Documents.Cast<Document>().FirstOrDefault(doc => doc.FullName == documentInfo.Moniker);
+        }
+    }
+}

Added: cfe/trunk/tools/clang-format-vs/ClangFormat/Vsix.cs
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/tools/clang-format-vs/ClangFormat/Vsix.cs?rev=299543&view=auto
==============================================================================
--- cfe/trunk/tools/clang-format-vs/ClangFormat/Vsix.cs (added)
+++ cfe/trunk/tools/clang-format-vs/ClangFormat/Vsix.cs Wed Apr  5 09:13:45 2017
@@ -0,0 +1,96 @@
+using EnvDTE;
+using Microsoft.VisualStudio.Editor;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.TextManager.Interop;
+using System;
+using System.IO;
+
+namespace LLVM.ClangFormat
+{
+    internal sealed class Vsix
+    {
+        /// <summary>
+        /// Returns the currently active view if it is a IWpfTextView.
+        /// </summary>
+        public static IWpfTextView GetCurrentView()
+        {
+            // The SVsTextManager is a service through which we can get the active view.
+            var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager));
+            IVsTextView textView;
+            textManager.GetActiveView(1, null, out textView);
+
+            // Now we have the active view as IVsTextView, but the text interfaces we need
+            // are in the IWpfTextView.
+            return VsToWpfTextView(textView);
+        }
+
+        public static bool IsDocumentDirty(Document document)
+        {
+            var textView = GetDocumentView(document);
+            var textDocument = GetTextDocument(textView);
+            return textDocument?.IsDirty == true;
+        }
+
+        public static IWpfTextView GetDocumentView(Document document)
+        {
+            var textView = GetVsTextViewFrompPath(document.FullName);
+            return VsToWpfTextView(textView);
+        }
+
+        public static IWpfTextView VsToWpfTextView(IVsTextView textView)
+        {
+            var userData = (IVsUserData)textView;
+            if (userData == null)
+                return null;
+            Guid guidWpfViewHost = DefGuidList.guidIWpfTextViewHost;
+            object host;
+            userData.GetData(ref guidWpfViewHost, out host);
+            return ((IWpfTextViewHost)host).TextView;
+        }
+
+        public static IVsTextView GetVsTextViewFrompPath(string filePath)
+        {
+            // From http://stackoverflow.com/a/2427368/4039972
+            var dte2 = (EnvDTE80.DTE2)Package.GetGlobalService(typeof(SDTE));
+            var sp = (Microsoft.VisualStudio.OLE.Interop.IServiceProvider)dte2;
+            var serviceProvider = new Microsoft.VisualStudio.Shell.ServiceProvider(sp);
+
+            IVsUIHierarchy uiHierarchy;
+            uint itemID;
+            IVsWindowFrame windowFrame;
+            if (VsShellUtilities.IsDocumentOpen(serviceProvider, filePath, Guid.Empty,
+                out uiHierarchy, out itemID, out windowFrame))
+            {
+                // Get the IVsTextView from the windowFrame.
+                return VsShellUtilities.GetTextView(windowFrame);
+            }
+            return null;
+        }
+
+        public static ITextDocument GetTextDocument(IWpfTextView view)
+        {
+            ITextDocument document;
+            if (view != null && view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
+                return document;
+            return null;
+        }
+
+        public static string GetDocumentParent(IWpfTextView view)
+        {
+            ITextDocument document = GetTextDocument(view);
+            if (document != null)
+            {
+                return Directory.GetParent(document.FilePath).ToString();
+            }
+            return null;
+        }
+
+        public static string GetDocumentPath(IWpfTextView view)
+        {
+            return GetTextDocument(view)?.FilePath;
+        }
+    }
+}




More information about the cfe-commits mailing list