Home | History | Annotate | Line # | Download | only in ClangFormat
      1 //===-- ClangFormatPackages.cs - VSPackage for clang-format ------*- C# -*-===//
      2 //
      3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
      4 // See https://llvm.org/LICENSE.txt for license information.
      5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
      6 //
      7 //===----------------------------------------------------------------------===//
      8 //
      9 // This class contains a VS extension package that runs clang-format over a
     10 // selection in a VS text editor.
     11 //
     12 //===----------------------------------------------------------------------===//
     13 
     14 using EnvDTE;
     15 using Microsoft.VisualStudio.Shell;
     16 using Microsoft.VisualStudio.Shell.Interop;
     17 using Microsoft.VisualStudio.Text;
     18 using Microsoft.VisualStudio.Text.Editor;
     19 using System;
     20 using System.Collections;
     21 using System.ComponentModel;
     22 using System.ComponentModel.Design;
     23 using System.IO;
     24 using System.Runtime.InteropServices;
     25 using System.Xml.Linq;
     26 using System.Linq;
     27 using System.Text;
     28 
     29 namespace LLVM.ClangFormat
     30 {
     31     [ClassInterface(ClassInterfaceType.AutoDual)]
     32     [CLSCompliant(false), ComVisible(true)]
     33     public class OptionPageGrid : DialogPage
     34     {
     35         private string assumeFilename = "";
     36         private string fallbackStyle = "LLVM";
     37         private bool sortIncludes = false;
     38         private string style = "file";
     39         private bool formatOnSave = false;
     40         private string formatOnSaveFileExtensions =
     41             ".c;.cpp;.cxx;.cc;.tli;.tlh;.h;.hh;.hpp;.hxx;.hh;.inl;" +
     42             ".java;.js;.ts;.m;.mm;.proto;.protodevel;.td";
     43 
     44         public OptionPageGrid Clone()
     45         {
     46             // Use MemberwiseClone to copy value types.
     47             var clone = (OptionPageGrid)MemberwiseClone();
     48             return clone;
     49         }
     50 
     51         public class StyleConverter : TypeConverter
     52         {
     53             protected ArrayList values;
     54             public StyleConverter()
     55             {
     56                 // Initializes the standard values list with defaults.
     57                 values = new ArrayList(new string[] { "file", "Chromium", "Google", "LLVM", "Mozilla", "WebKit" });
     58             }
     59 
     60             public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
     61             {
     62                 return true;
     63             }
     64 
     65             public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
     66             {
     67                 return new StandardValuesCollection(values);
     68             }
     69 
     70             public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
     71             {
     72                 if (sourceType == typeof(string))
     73                     return true;
     74 
     75                 return base.CanConvertFrom(context, sourceType);
     76             }
     77 
     78             public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
     79             {
     80                 string s = value as string;
     81                 if (s == null)
     82                     return base.ConvertFrom(context, culture, value);
     83 
     84                 return value;
     85             }
     86         }
     87 
     88         [Category("Format Options")]
     89         [DisplayName("Style")]
     90         [Description("Coding style, currently supports:\n" +
     91                      "  - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" +
     92                      "  - 'file' to search for a YAML .clang-format or _clang-format\n" +
     93                      "    configuration file.\n" +
     94                      "  - A YAML configuration snippet.\n\n" +
     95                      "'File':\n" +
     96                      "  Searches for a .clang-format or _clang-format configuration file\n" +
     97                      "  in the source file's directory and its parents.\n\n" +
     98                      "YAML configuration snippet:\n" +
     99                      "  The content of a .clang-format configuration file, as string.\n" +
    100                      "  Example: '{BasedOnStyle: \"LLVM\", IndentWidth: 8}'\n\n" +
    101                      "See also: http://clang.llvm.org/docs/ClangFormatStyleOptions.html.")]
    102         [TypeConverter(typeof(StyleConverter))]
    103         public string Style
    104         {
    105             get { return style; }
    106             set { style = value; }
    107         }
    108 
    109         public sealed class FilenameConverter : TypeConverter
    110         {
    111             public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    112             {
    113                 if (sourceType == typeof(string))
    114                     return true;
    115 
    116                 return base.CanConvertFrom(context, sourceType);
    117             }
    118 
    119             public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    120             {
    121                 string s = value as string;
    122                 if (s == null)
    123                     return base.ConvertFrom(context, culture, value);
    124 
    125                 // Check if string contains quotes. On Windows, file names cannot contain quotes.
    126                 // We do not accept them however to avoid hard-to-debug problems.
    127                 // A quote in user input would end the parameter quote and so break the command invocation.
    128                 if (s.IndexOf('\"') != -1)
    129                     throw new NotSupportedException("Filename cannot contain quotes");
    130 
    131                 return value;
    132             }
    133         }
    134 
    135         [Category("Format Options")]
    136         [DisplayName("Assume Filename")]
    137         [Description("When reading from stdin, clang-format assumes this " +
    138                      "filename to look for a style config file (with 'file' style) " +
    139                      "and to determine the language.")]
    140         [TypeConverter(typeof(FilenameConverter))]
    141         public string AssumeFilename
    142         {
    143             get { return assumeFilename; }
    144             set { assumeFilename = value; }
    145         }
    146 
    147         public sealed class FallbackStyleConverter : StyleConverter
    148         {
    149             public FallbackStyleConverter()
    150             {
    151                 // Add "none" to the list of styles.
    152                 values.Insert(0, "none");
    153             }
    154         }
    155 
    156         [Category("Format Options")]
    157         [DisplayName("Fallback Style")]
    158         [Description("The name of the predefined style used as a fallback in case clang-format " +
    159                      "is invoked with 'file' style, but can not find the configuration file.\n" +
    160                      "Use 'none' fallback style to skip formatting.")]
    161         [TypeConverter(typeof(FallbackStyleConverter))]
    162         public string FallbackStyle
    163         {
    164             get { return fallbackStyle; }
    165             set { fallbackStyle = value; }
    166         }
    167 
    168         [Category("Format Options")]
    169         [DisplayName("Sort includes")]
    170         [Description("Sort touched include lines.\n\n" +
    171                      "See also: http://clang.llvm.org/docs/ClangFormat.html.")]
    172         public bool SortIncludes
    173         {
    174             get { return sortIncludes; }
    175             set { sortIncludes = value; }
    176         }
    177 
    178         [Category("Format On Save")]
    179         [DisplayName("Enable")]
    180         [Description("Enable running clang-format when modified files are saved. " +
    181                      "Will only format if Style is found (ignores Fallback Style)."
    182             )]
    183         public bool FormatOnSave
    184         {
    185             get { return formatOnSave; }
    186             set { formatOnSave = value; }
    187         }
    188 
    189         [Category("Format On Save")]
    190         [DisplayName("File extensions")]
    191         [Description("When formatting on save, clang-format will be applied only to " +
    192                      "files with these extensions.")]
    193         public string FormatOnSaveFileExtensions
    194         {
    195             get { return formatOnSaveFileExtensions; }
    196             set { formatOnSaveFileExtensions = value; }
    197         }
    198     }
    199 
    200     [PackageRegistration(UseManagedResourcesOnly = true)]
    201     [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    202     [ProvideMenuResource("Menus.ctmenu", 1)]
    203     [ProvideAutoLoad(UIContextGuids80.SolutionExists)] // Load package on solution load
    204     [Guid(GuidList.guidClangFormatPkgString)]
    205     [ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)]
    206     public sealed class ClangFormatPackage : Package
    207     {
    208         #region Package Members
    209 
    210         RunningDocTableEventsDispatcher _runningDocTableEventsDispatcher;
    211 
    212         protected override void Initialize()
    213         {
    214             base.Initialize();
    215 
    216             _runningDocTableEventsDispatcher = new RunningDocTableEventsDispatcher(this);
    217             _runningDocTableEventsDispatcher.BeforeSave += OnBeforeSave;
    218 
    219             var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
    220             if (commandService != null)
    221             {
    222                 {
    223                     var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatSelection);
    224                     var menuItem = new MenuCommand(MenuItemCallback, menuCommandID);
    225                     commandService.AddCommand(menuItem);
    226                 }
    227 
    228                 {
    229                     var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatDocument);
    230                     var menuItem = new MenuCommand(MenuItemCallback, menuCommandID);
    231                     commandService.AddCommand(menuItem);
    232                 }
    233             }
    234         }
    235         #endregion
    236 
    237         OptionPageGrid GetUserOptions()
    238         {
    239             return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
    240         }
    241 
    242         private void MenuItemCallback(object sender, EventArgs args)
    243         {
    244             var mc = sender as System.ComponentModel.Design.MenuCommand;
    245             if (mc == null)
    246                 return;
    247 
    248             switch (mc.CommandID.ID)
    249             {
    250                 case (int)PkgCmdIDList.cmdidClangFormatSelection:
    251                     FormatSelection(GetUserOptions());
    252                     break;
    253 
    254                 case (int)PkgCmdIDList.cmdidClangFormatDocument:
    255                     FormatDocument(GetUserOptions());
    256                     break;
    257             }
    258         }
    259 
    260         private static bool FileHasExtension(string filePath, string fileExtensions)
    261         {
    262             var extensions = fileExtensions.ToLower().Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
    263             return extensions.Contains(Path.GetExtension(filePath).ToLower());
    264         }
    265 
    266         private void OnBeforeSave(object sender, Document document)
    267         {
    268             var options = GetUserOptions();
    269 
    270             if (!options.FormatOnSave)
    271                 return;
    272 
    273             if (!FileHasExtension(document.FullName, options.FormatOnSaveFileExtensions))
    274                 return;
    275 
    276             if (!Vsix.IsDocumentDirty(document))
    277                 return;
    278 
    279             var optionsWithNoFallbackStyle = GetUserOptions().Clone();
    280             optionsWithNoFallbackStyle.FallbackStyle = "none";
    281             FormatDocument(document, optionsWithNoFallbackStyle);
    282         }
    283 
    284         /// <summary>
    285         /// Runs clang-format on the current selection
    286         /// </summary>
    287         private void FormatSelection(OptionPageGrid options)
    288         {
    289             IWpfTextView view = Vsix.GetCurrentView();
    290             if (view == null)
    291                 // We're not in a text view.
    292                 return;
    293             string text = view.TextBuffer.CurrentSnapshot.GetText();
    294             int start = view.Selection.Start.Position.GetContainingLine().Start.Position;
    295             int end = view.Selection.End.Position.GetContainingLine().End.Position;
    296 
    297             // clang-format doesn't support formatting a range that starts at the end
    298             // of the file.
    299             if (start >= text.Length && text.Length > 0)
    300                 start = text.Length - 1;
    301             string path = Vsix.GetDocumentParent(view);
    302             string filePath = Vsix.GetDocumentPath(view);
    303 
    304             RunClangFormatAndApplyReplacements(text, start, end, path, filePath, options, view);
    305         }
    306 
    307         /// <summary>
    308         /// Runs clang-format on the current document
    309         /// </summary>
    310         private void FormatDocument(OptionPageGrid options)
    311         {
    312             FormatView(Vsix.GetCurrentView(), options);
    313         }
    314 
    315         private void FormatDocument(Document document, OptionPageGrid options)
    316         {
    317             FormatView(Vsix.GetDocumentView(document), options);
    318         }
    319 
    320         private void FormatView(IWpfTextView view, OptionPageGrid options)
    321         {
    322             if (view == null)
    323                 // We're not in a text view.
    324                 return;
    325 
    326             string filePath = Vsix.GetDocumentPath(view);
    327             var path = Path.GetDirectoryName(filePath);
    328 
    329             string text = view.TextBuffer.CurrentSnapshot.GetText();
    330             if (!text.EndsWith(Environment.NewLine))
    331             {
    332                 view.TextBuffer.Insert(view.TextBuffer.CurrentSnapshot.Length, Environment.NewLine);
    333                 text += Environment.NewLine;
    334             }
    335 
    336             RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, options, view);
    337         }
    338 
    339         private void RunClangFormatAndApplyReplacements(string text, int start, int end, string path, string filePath, OptionPageGrid options, IWpfTextView view)
    340         {
    341             try
    342             {
    343                 string replacements = RunClangFormat(text, start, end, path, filePath, options);
    344                 ApplyClangFormatReplacements(replacements, view);
    345             }
    346             catch (Exception e)
    347             {
    348                 var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
    349                 var id = Guid.Empty;
    350                 int result;
    351                 uiShell.ShowMessageBox(
    352                         0, ref id,
    353                         "Error while running clang-format:",
    354                         e.Message,
    355                         string.Empty, 0,
    356                         OLEMSGBUTTON.OLEMSGBUTTON_OK,
    357                         OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,
    358                         OLEMSGICON.OLEMSGICON_INFO,
    359                         0, out result);
    360             }
    361         }
    362 
    363         /// <summary>
    364         /// Runs the given text through clang-format and returns the replacements as XML.
    365         ///
    366         /// Formats the text in range start and end.
    367         /// </summary>
    368         private static string RunClangFormat(string text, int start, int end, string path, string filePath, OptionPageGrid options)
    369         {
    370             string vsixPath = Path.GetDirectoryName(
    371                 typeof(ClangFormatPackage).Assembly.Location);
    372 
    373             System.Diagnostics.Process process = new System.Diagnostics.Process();
    374             process.StartInfo.UseShellExecute = false;
    375             process.StartInfo.FileName = vsixPath + "\\clang-format.exe";
    376             char[] chars = text.ToCharArray();
    377             int offset = Encoding.UTF8.GetByteCount(chars, 0, start);
    378             int length = Encoding.UTF8.GetByteCount(chars, 0, end) - offset;
    379             // Poor man's escaping - this will not work when quotes are already escaped
    380             // in the input (but we don't need more).
    381             string style = options.Style.Replace("\"", "\\\"");
    382             string fallbackStyle = options.FallbackStyle.Replace("\"", "\\\"");
    383             process.StartInfo.Arguments = " -offset " + offset +
    384                                           " -length " + length +
    385                                           " -output-replacements-xml " +
    386                                           " -style \"" + style + "\"" +
    387                                           " -fallback-style \"" + fallbackStyle + "\"";
    388             if (options.SortIncludes)
    389               process.StartInfo.Arguments += " -sort-includes ";
    390             string assumeFilename = options.AssumeFilename;
    391             if (string.IsNullOrEmpty(assumeFilename))
    392                 assumeFilename = filePath;
    393             if (!string.IsNullOrEmpty(assumeFilename))
    394               process.StartInfo.Arguments += " -assume-filename \"" + assumeFilename + "\"";
    395             process.StartInfo.CreateNoWindow = true;
    396             process.StartInfo.RedirectStandardInput = true;
    397             process.StartInfo.RedirectStandardOutput = true;
    398             process.StartInfo.RedirectStandardError = true;
    399             if (path != null)
    400                 process.StartInfo.WorkingDirectory = path;
    401             // We have to be careful when communicating via standard input / output,
    402             // as writes to the buffers will block until they are read from the other side.
    403             // Thus, we:
    404             // 1. Start the process - clang-format.exe will start to read the input from the
    405             //    standard input.
    406             try
    407             {
    408                 process.Start();
    409             }
    410             catch (Exception e)
    411             {
    412                 throw new Exception(
    413                     "Cannot execute " + process.StartInfo.FileName + ".\n\"" +
    414                     e.Message + "\".\nPlease make sure it is on the PATH.");
    415             }
    416             // 2. We write everything to the standard output - this cannot block, as clang-format
    417             //    reads the full standard input before analyzing it without writing anything to the
    418             //    standard output.
    419             StreamWriter utf8Writer = new StreamWriter(process.StandardInput.BaseStream, new UTF8Encoding(false));
    420             utf8Writer.Write(text);
    421             // 3. We notify clang-format that the input is done - after this point clang-format
    422             //    will start analyzing the input and eventually write the output.
    423             utf8Writer.Close();
    424             // 4. We must read clang-format's output before waiting for it to exit; clang-format
    425             //    will close the channel by exiting.
    426             string output = process.StandardOutput.ReadToEnd();
    427             // 5. clang-format is done, wait until it is fully shut down.
    428             process.WaitForExit();
    429             if (process.ExitCode != 0)
    430             {
    431                 // FIXME: If clang-format writes enough to the standard error stream to block,
    432                 // we will never reach this point; instead, read the standard error asynchronously.
    433                 throw new Exception(process.StandardError.ReadToEnd());
    434             }
    435             return output;
    436         }
    437 
    438         /// <summary>
    439         /// Applies the clang-format replacements (xml) to the current view
    440         /// </summary>
    441         private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
    442         {
    443             // clang-format returns no replacements if input text is empty
    444             if (replacements.Length == 0)
    445                 return;
    446 
    447             string text = view.TextBuffer.CurrentSnapshot.GetText();
    448             byte[] bytes = Encoding.UTF8.GetBytes(text);
    449 
    450             var root = XElement.Parse(replacements);
    451             var edit = view.TextBuffer.CreateEdit();
    452             foreach (XElement replacement in root.Descendants("replacement"))
    453             {
    454                 int offset = int.Parse(replacement.Attribute("offset").Value);
    455                 int length = int.Parse(replacement.Attribute("length").Value);
    456                 var span = new Span(
    457                     Encoding.UTF8.GetCharCount(bytes, 0, offset),
    458                     Encoding.UTF8.GetCharCount(bytes, offset, length));
    459                 edit.Replace(span, replacement.Value);
    460             }
    461             edit.Apply();
    462         }
    463     }
    464 }
    465