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