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