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