xref: /openbsd-src/gnu/llvm/clang/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs (revision 1a8dbaac879b9f3335ad7fb25429ce63ac1d6bac)
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