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 try 206 { 207 var root = XElement.Parse(RunClangFormat(text, start, length, path)); 208 var edit = view.TextBuffer.CreateEdit(); 209 foreach (XElement replacement in root.Descendants("replacement")) 210 { 211 var span = new Span( 212 int.Parse(replacement.Attribute("offset").Value), 213 int.Parse(replacement.Attribute("length").Value)); 214 edit.Replace(span, replacement.Value); 215 } 216 edit.Apply(); 217 } 218 catch (Exception e) 219 { 220 var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); 221 var id = Guid.Empty; 222 int result; 223 uiShell.ShowMessageBox( 224 0, ref id, 225 "Error while running clang-format:", 226 e.Message, 227 string.Empty, 0, 228 OLEMSGBUTTON.OLEMSGBUTTON_OK, 229 OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, 230 OLEMSGICON.OLEMSGICON_INFO, 231 0, out result); 232 } 233 } 234 235 /// <summary> 236 /// Runs the given text through clang-format and returns the replacements as XML. 237 /// 238 /// Formats the text range starting at offset of the given length. 239 /// </summary> RunClangFormat(string text, int offset, int length, string path)240 private string RunClangFormat(string text, int offset, int length, string path) 241 { 242 string vsixPath = Path.GetDirectoryName( 243 typeof(ClangFormatPackage).Assembly.Location); 244 245 System.Diagnostics.Process process = new System.Diagnostics.Process(); 246 process.StartInfo.UseShellExecute = false; 247 process.StartInfo.FileName = vsixPath + "\\clang-format.exe"; 248 // Poor man's escaping - this will not work when quotes are already escaped 249 // in the input (but we don't need more). 250 string style = GetStyle().Replace("\"", "\\\""); 251 string fallbackStyle = GetFallbackStyle().Replace("\"", "\\\""); 252 process.StartInfo.Arguments = " -offset " + offset + 253 " -length " + length + 254 " -output-replacements-xml " + 255 " -style \"" + style + "\"" + 256 " -fallback-style \"" + fallbackStyle + "\""; 257 if (GetSortIncludes()) 258 process.StartInfo.Arguments += " -sort-includes "; 259 string assumeFilename = GetAssumeFilename(); 260 if (!string.IsNullOrEmpty(assumeFilename)) 261 process.StartInfo.Arguments += " -assume-filename \"" + assumeFilename + "\""; 262 process.StartInfo.CreateNoWindow = true; 263 process.StartInfo.RedirectStandardInput = true; 264 process.StartInfo.RedirectStandardOutput = true; 265 process.StartInfo.RedirectStandardError = true; 266 if (path != null) 267 process.StartInfo.WorkingDirectory = path; 268 // We have to be careful when communicating via standard input / output, 269 // as writes to the buffers will block until they are read from the other side. 270 // Thus, we: 271 // 1. Start the process - clang-format.exe will start to read the input from the 272 // standard input. 273 try 274 { 275 process.Start(); 276 } 277 catch (Exception e) 278 { 279 throw new Exception( 280 "Cannot execute " + process.StartInfo.FileName + ".\n\"" + 281 e.Message + "\".\nPlease make sure it is on the PATH."); 282 } 283 // 2. We write everything to the standard output - this cannot block, as clang-format 284 // reads the full standard input before analyzing it without writing anything to the 285 // standard output. 286 process.StandardInput.Write(text); 287 // 3. We notify clang-format that the input is done - after this point clang-format 288 // will start analyzing the input and eventually write the output. 289 process.StandardInput.Close(); 290 // 4. We must read clang-format's output before waiting for it to exit; clang-format 291 // will close the channel by exiting. 292 string output = process.StandardOutput.ReadToEnd(); 293 // 5. clang-format is done, wait until it is fully shut down. 294 process.WaitForExit(); 295 if (process.ExitCode != 0) 296 { 297 // FIXME: If clang-format writes enough to the standard error stream to block, 298 // we will never reach this point; instead, read the standard error asynchronously. 299 throw new Exception(process.StandardError.ReadToEnd()); 300 } 301 return output; 302 } 303 304 /// <summary> 305 /// Returns the currently active view if it is a IWpfTextView. 306 /// </summary> GetCurrentView()307 private IWpfTextView GetCurrentView() 308 { 309 // The SVsTextManager is a service through which we can get the active view. 310 var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager)); 311 IVsTextView textView; 312 textManager.GetActiveView(1, null, out textView); 313 314 // Now we have the active view as IVsTextView, but the text interfaces we need 315 // are in the IWpfTextView. 316 var userData = (IVsUserData)textView; 317 if (userData == null) 318 return null; 319 Guid guidWpfViewHost = DefGuidList.guidIWpfTextViewHost; 320 object host; 321 userData.GetData(ref guidWpfViewHost, out host); 322 return ((IWpfTextViewHost)host).TextView; 323 } 324 GetStyle()325 private string GetStyle() 326 { 327 var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); 328 return page.Style; 329 } 330 GetAssumeFilename()331 private string GetAssumeFilename() 332 { 333 var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); 334 return page.AssumeFilename; 335 } 336 GetFallbackStyle()337 private string GetFallbackStyle() 338 { 339 var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); 340 return page.FallbackStyle; 341 } 342 GetSortIncludes()343 private bool GetSortIncludes() 344 { 345 var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); 346 return page.SortIncludes; 347 } 348 GetDocumentParent(IWpfTextView view)349 private string GetDocumentParent(IWpfTextView view) 350 { 351 ITextDocument document; 352 if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document)) 353 { 354 return Directory.GetParent(document.FilePath).ToString(); 355 } 356 return null; 357 } 358 } 359 } 360