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 
Clone()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;
StyleConverter()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 
GetStandardValuesSupported(ITypeDescriptorContext context)60             public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
61             {
62                 return true;
63             }
64 
GetStandardValues(ITypeDescriptorContext context)65             public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
66             {
67                 return new StandardValuesCollection(values);
68             }
69 
CanConvertFrom(ITypeDescriptorContext context, Type sourceType)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 
ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)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         {
CanConvertFrom(ITypeDescriptorContext context, Type sourceType)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 
ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)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         {
FallbackStyleConverter()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 
Initialize()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 
GetUserOptions()237         OptionPageGrid GetUserOptions()
238         {
239             return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
240         }
241 
MenuItemCallback(object sender, EventArgs args)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 
FileHasExtension(string filePath, string fileExtensions)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 
OnBeforeSave(object sender, Document document)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>
FormatSelection(OptionPageGrid options)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>
FormatDocument(OptionPageGrid options)310         private void FormatDocument(OptionPageGrid options)
311         {
312             FormatView(Vsix.GetCurrentView(), options);
313         }
314 
FormatDocument(Document document, OptionPageGrid options)315         private void FormatDocument(Document document, OptionPageGrid options)
316         {
317             FormatView(Vsix.GetDocumentView(document), options);
318         }
319 
FormatView(IWpfTextView view, OptionPageGrid options)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 
RunClangFormatAndApplyReplacements(string text, int start, int end, string path, string filePath, OptionPageGrid options, IWpfTextView view)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>
RunClangFormat(string text, int start, int end, string path, string filePath, OptionPageGrid options)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>
ApplyClangFormatReplacements(string replacements, IWpfTextView view)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