• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //===-- ClangFormatPackages.cs - VSPackage for clang-format ------*- C# -*-===//
2 //
3 //                     The LLVM Compiler Infrastructure
4 //
5 // This file is distributed under the University of Illinois Open Source
6 // License. See LICENSE.TXT for details.
7 //
8 //===----------------------------------------------------------------------===//
9 //
10 // This class contains a VS extension package that runs clang-format over a
11 // selection in a VS text editor.
12 //
13 //===----------------------------------------------------------------------===//
14 
15 using Microsoft.VisualStudio.Editor;
16 using Microsoft.VisualStudio.Shell;
17 using Microsoft.VisualStudio.Shell.Interop;
18 using Microsoft.VisualStudio.Text;
19 using Microsoft.VisualStudio.Text.Editor;
20 using Microsoft.VisualStudio.TextManager.Interop;
21 using System;
22 using System.Collections;
23 using System.ComponentModel;
24 using System.ComponentModel.Design;
25 using System.IO;
26 using System.Runtime.InteropServices;
27 using System.Xml.Linq;
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 
40         public class StyleConverter : TypeConverter
41         {
42             protected ArrayList values;
StyleConverter()43             public StyleConverter()
44             {
45                 // Initializes the standard values list with defaults.
46                 values = new ArrayList(new string[] { "file", "Chromium", "Google", "LLVM", "Mozilla", "WebKit" });
47             }
48 
GetStandardValuesSupported(ITypeDescriptorContext context)49             public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
50             {
51                 return true;
52             }
53 
GetStandardValues(ITypeDescriptorContext context)54             public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
55             {
56                 return new StandardValuesCollection(values);
57             }
58 
CanConvertFrom(ITypeDescriptorContext context, Type sourceType)59             public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
60             {
61                 if (sourceType == typeof(string))
62                     return true;
63 
64                 return base.CanConvertFrom(context, sourceType);
65             }
66 
ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)67             public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
68             {
69                 string s = value as string;
70                 if (s == null)
71                     return base.ConvertFrom(context, culture, value);
72 
73                 return value;
74             }
75         }
76 
77         [Category("LLVM/Clang")]
78         [DisplayName("Style")]
79         [Description("Coding style, currently supports:\n" +
80                      "  - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" +
81                      "  - 'file' to search for a YAML .clang-format or _clang-format\n" +
82                      "    configuration file.\n" +
83                      "  - A YAML configuration snippet.\n\n" +
84                      "'File':\n" +
85                      "  Searches for a .clang-format or _clang-format configuration file\n" +
86                      "  in the source file's directory and its parents.\n\n" +
87                      "YAML configuration snippet:\n" +
88                      "  The content of a .clang-format configuration file, as string.\n" +
89                      "  Example: '{BasedOnStyle: \"LLVM\", IndentWidth: 8}'\n\n" +
90                      "See also: http://clang.llvm.org/docs/ClangFormatStyleOptions.html.")]
91         [TypeConverter(typeof(StyleConverter))]
92         public string Style
93         {
94             get { return style; }
95             set { style = value; }
96         }
97 
98         public sealed class FilenameConverter : TypeConverter
99         {
CanConvertFrom(ITypeDescriptorContext context, Type sourceType)100             public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
101             {
102                 if (sourceType == typeof(string))
103                     return true;
104 
105                 return base.CanConvertFrom(context, sourceType);
106             }
107 
ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)108             public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
109             {
110                 string s = value as string;
111                 if (s == null)
112                     return base.ConvertFrom(context, culture, value);
113 
114                 // Check if string contains quotes. On Windows, file names cannot contain quotes.
115                 // We do not accept them however to avoid hard-to-debug problems.
116                 // A quote in user input would end the parameter quote and so break the command invocation.
117                 if (s.IndexOf('\"') != -1)
118                     throw new NotSupportedException("Filename cannot contain quotes");
119 
120                 return value;
121             }
122         }
123 
124         [Category("LLVM/Clang")]
125         [DisplayName("Assume Filename")]
126         [Description("When reading from stdin, clang-format assumes this " +
127                      "filename to look for a style config file (with 'file' style) " +
128                      "and to determine the language.")]
129         [TypeConverter(typeof(FilenameConverter))]
130         public string AssumeFilename
131         {
132             get { return assumeFilename; }
133             set { assumeFilename = value; }
134         }
135 
136         public sealed class FallbackStyleConverter : StyleConverter
137         {
FallbackStyleConverter()138             public FallbackStyleConverter()
139             {
140                 // Add "none" to the list of styles.
141                 values.Insert(0, "none");
142             }
143         }
144 
145         [Category("LLVM/Clang")]
146         [DisplayName("Fallback Style")]
147         [Description("The name of the predefined style used as a fallback in case clang-format " +
148                      "is invoked with 'file' style, but can not find the configuration file.\n" +
149                      "Use 'none' fallback style to skip formatting.")]
150         [TypeConverter(typeof(FallbackStyleConverter))]
151         public string FallbackStyle
152         {
153             get { return fallbackStyle; }
154             set { fallbackStyle = value; }
155         }
156 
157         [Category("LLVM/Clang")]
158         [DisplayName("Sort includes")]
159         [Description("Sort touched include lines.\n\n" +
160                      "See also: http://clang.llvm.org/docs/ClangFormat.html.")]
161         public bool SortIncludes
162         {
163             get { return sortIncludes; }
164             set { sortIncludes = value; }
165         }
166     }
167 
168     [PackageRegistration(UseManagedResourcesOnly = true)]
169     [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
170     [ProvideMenuResource("Menus.ctmenu", 1)]
171     [Guid(GuidList.guidClangFormatPkgString)]
172     [ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)]
173     public sealed class ClangFormatPackage : Package
174     {
175         #region Package Members
Initialize()176         protected override void Initialize()
177         {
178             base.Initialize();
179 
180             var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
181             if (commandService != null)
182             {
183                 var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormat);
184                 var menuItem = new MenuCommand(MenuItemCallback, menuCommandID);
185                 commandService.AddCommand(menuItem);
186             }
187         }
188         #endregion
189 
MenuItemCallback(object sender, EventArgs args)190         private void MenuItemCallback(object sender, EventArgs args)
191         {
192             IWpfTextView view = GetCurrentView();
193             if (view == null)
194                 // We're not in a text view.
195                 return;
196             string text = view.TextBuffer.CurrentSnapshot.GetText();
197             int start = view.Selection.Start.Position.GetContainingLine().Start.Position;
198             int end = view.Selection.End.Position.GetContainingLine().End.Position;
199             int length = end - start;
200             // clang-format doesn't support formatting a range that starts at the end
201             // of the file.
202             if (start >= text.Length && text.Length > 0)
203                 start = text.Length - 1;
204             string path = GetDocumentParent(view);
205             string filePath = GetDocumentPath(view);
206             try
207             {
208                 var root = XElement.Parse(RunClangFormat(text, start, length, path, filePath));
209                 var edit = view.TextBuffer.CreateEdit();
210                 foreach (XElement replacement in root.Descendants("replacement"))
211                 {
212                     var span = new Span(
213                         int.Parse(replacement.Attribute("offset").Value),
214                         int.Parse(replacement.Attribute("length").Value));
215                     edit.Replace(span, replacement.Value);
216                 }
217                 edit.Apply();
218             }
219             catch (Exception e)
220             {
221                 var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
222                 var id = Guid.Empty;
223                 int result;
224                 uiShell.ShowMessageBox(
225                         0, ref id,
226                         "Error while running clang-format:",
227                         e.Message,
228                         string.Empty, 0,
229                         OLEMSGBUTTON.OLEMSGBUTTON_OK,
230                         OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,
231                         OLEMSGICON.OLEMSGICON_INFO,
232                         0, out result);
233             }
234         }
235 
236         /// <summary>
237         /// Runs the given text through clang-format and returns the replacements as XML.
238         ///
239         /// Formats the text range starting at offset of the given length.
240         /// </summary>
RunClangFormat(string text, int offset, int length, string path, string filePath)241         private string RunClangFormat(string text, int offset, int length, string path, string filePath)
242         {
243             string vsixPath = Path.GetDirectoryName(
244                 typeof(ClangFormatPackage).Assembly.Location);
245 
246             System.Diagnostics.Process process = new System.Diagnostics.Process();
247             process.StartInfo.UseShellExecute = false;
248             process.StartInfo.FileName = vsixPath + "\\clang-format.exe";
249             // Poor man's escaping - this will not work when quotes are already escaped
250             // in the input (but we don't need more).
251             string style = GetStyle().Replace("\"", "\\\"");
252             string fallbackStyle = GetFallbackStyle().Replace("\"", "\\\"");
253             process.StartInfo.Arguments = " -offset " + offset +
254                                           " -length " + length +
255                                           " -output-replacements-xml " +
256                                           " -style \"" + style + "\"" +
257                                           " -fallback-style \"" + fallbackStyle + "\"";
258             if (GetSortIncludes())
259               process.StartInfo.Arguments += " -sort-includes ";
260             string assumeFilename = GetAssumeFilename();
261             if (string.IsNullOrEmpty(assumeFilename))
262                 assumeFilename = filePath;
263             if (!string.IsNullOrEmpty(assumeFilename))
264               process.StartInfo.Arguments += " -assume-filename \"" + assumeFilename + "\"";
265             process.StartInfo.CreateNoWindow = true;
266             process.StartInfo.RedirectStandardInput = true;
267             process.StartInfo.RedirectStandardOutput = true;
268             process.StartInfo.RedirectStandardError = true;
269             if (path != null)
270                 process.StartInfo.WorkingDirectory = path;
271             // We have to be careful when communicating via standard input / output,
272             // as writes to the buffers will block until they are read from the other side.
273             // Thus, we:
274             // 1. Start the process - clang-format.exe will start to read the input from the
275             //    standard input.
276             try
277             {
278                 process.Start();
279             }
280             catch (Exception e)
281             {
282                 throw new Exception(
283                     "Cannot execute " + process.StartInfo.FileName + ".\n\"" +
284                     e.Message + "\".\nPlease make sure it is on the PATH.");
285             }
286             // 2. We write everything to the standard output - this cannot block, as clang-format
287             //    reads the full standard input before analyzing it without writing anything to the
288             //    standard output.
289             process.StandardInput.Write(text);
290             // 3. We notify clang-format that the input is done - after this point clang-format
291             //    will start analyzing the input and eventually write the output.
292             process.StandardInput.Close();
293             // 4. We must read clang-format's output before waiting for it to exit; clang-format
294             //    will close the channel by exiting.
295             string output = process.StandardOutput.ReadToEnd();
296             // 5. clang-format is done, wait until it is fully shut down.
297             process.WaitForExit();
298             if (process.ExitCode != 0)
299             {
300                 // FIXME: If clang-format writes enough to the standard error stream to block,
301                 // we will never reach this point; instead, read the standard error asynchronously.
302                 throw new Exception(process.StandardError.ReadToEnd());
303             }
304             return output;
305         }
306 
307         /// <summary>
308         /// Returns the currently active view if it is a IWpfTextView.
309         /// </summary>
GetCurrentView()310         private IWpfTextView GetCurrentView()
311         {
312             // The SVsTextManager is a service through which we can get the active view.
313             var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager));
314             IVsTextView textView;
315             textManager.GetActiveView(1, null, out textView);
316 
317             // Now we have the active view as IVsTextView, but the text interfaces we need
318             // are in the IWpfTextView.
319             var userData = (IVsUserData)textView;
320             if (userData == null)
321                 return null;
322             Guid guidWpfViewHost = DefGuidList.guidIWpfTextViewHost;
323             object host;
324             userData.GetData(ref guidWpfViewHost, out host);
325             return ((IWpfTextViewHost)host).TextView;
326         }
327 
GetStyle()328         private string GetStyle()
329         {
330             var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
331             return page.Style;
332         }
333 
GetAssumeFilename()334         private string GetAssumeFilename()
335         {
336             var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
337             return page.AssumeFilename;
338         }
339 
GetFallbackStyle()340         private string GetFallbackStyle()
341         {
342             var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
343             return page.FallbackStyle;
344         }
345 
GetSortIncludes()346         private bool GetSortIncludes()
347         {
348             var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
349             return page.SortIncludes;
350         }
351 
GetDocumentParent(IWpfTextView view)352         private string GetDocumentParent(IWpfTextView view)
353         {
354             ITextDocument document;
355             if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
356             {
357                 return Directory.GetParent(document.FilePath).ToString();
358             }
359             return null;
360         }
361 
GetDocumentPath(IWpfTextView view)362         private string GetDocumentPath(IWpfTextView view)
363         {
364             ITextDocument document;
365             if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
366             {
367                 return document.FilePath;
368             }
369             return null;
370         }
371     }
372 }
373