ClangFormatPackage.cs revision 15852fc162eb6f77c28b67c868fdeeffed8c57d4
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 15using Microsoft.VisualStudio.Editor; 16using Microsoft.VisualStudio.Shell; 17using Microsoft.VisualStudio.Shell.Interop; 18using Microsoft.VisualStudio.Text; 19using Microsoft.VisualStudio.Text.Editor; 20using Microsoft.VisualStudio.TextManager.Interop; 21using System; 22using System.ComponentModel; 23using System.ComponentModel.Design; 24using System.IO; 25using System.Runtime.InteropServices; 26using System.Xml.Linq; 27 28namespace LLVM.ClangFormat 29{ 30 [ClassInterface(ClassInterfaceType.AutoDual)] 31 [CLSCompliant(false), ComVisible(true)] 32 public class OptionPageGrid : DialogPage 33 { 34 private string style = "File"; 35 36 [Category("LLVM/Clang")] 37 [DisplayName("Style")] 38 [Description("Coding style, currently supports:\n" + 39 " - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla').\n" + 40 " - 'File' to search for a YAML .clang-format configuration.\n" + 41 " - A YAML configuration snippet.\n\n" + 42 "'File':\n" + 43 " Searches for a .clang-format configuration in the source file's\n" + 44 " directory and its parents.\n\n" + 45 "YAML configuration snippet:\n" + 46 " The content of a .clang-format configuration file, as string.\n" + 47 " Example: '{BasedOnStyle: \"LLVM\", IndentWidth: 8}'\n\n" + 48 "See also: http://clang.llvm.org/docs/ClangFormatStyleOptions.html.")] 49 public string Style 50 { 51 get { return style; } 52 set { style = value; } 53 } 54 } 55 56 [PackageRegistration(UseManagedResourcesOnly = true)] 57 [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] 58 [ProvideMenuResource("Menus.ctmenu", 1)] 59 [Guid(GuidList.guidClangFormatPkgString)] 60 [ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)] 61 public sealed class ClangFormatPackage : Package 62 { 63 #region Package Members 64 protected override void Initialize() 65 { 66 base.Initialize(); 67 68 var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService; 69 if (commandService != null) 70 { 71 var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormat); 72 var menuItem = new MenuCommand(MenuItemCallback, menuCommandID); 73 commandService.AddCommand(menuItem); 74 } 75 } 76 #endregion 77 78 private void MenuItemCallback(object sender, EventArgs args) 79 { 80 IWpfTextView view = GetCurrentView(); 81 if (view == null) 82 // We're not in a text view. 83 return; 84 string text = view.TextBuffer.CurrentSnapshot.GetText(); 85 int start = view.Selection.Start.Position.GetContainingLine().Start.Position; 86 int end = view.Selection.End.Position.GetContainingLine().End.Position; 87 int length = end - start; 88 // clang-format doesn't support formatting a range that starts at the end 89 // of the file. 90 if (start >= text.Length && text.Length > 0) 91 start = text.Length - 1; 92 string path = GetDocumentParent(view); 93 try 94 { 95 var root = XElement.Parse(RunClangFormat(text, start, length, path)); 96 var edit = view.TextBuffer.CreateEdit(); 97 foreach (XElement replacement in root.Descendants("replacement")) 98 { 99 var span = new Span( 100 int.Parse(replacement.Attribute("offset").Value), 101 int.Parse(replacement.Attribute("length").Value)); 102 edit.Replace(span, replacement.Value); 103 } 104 edit.Apply(); 105 } 106 catch (Exception e) 107 { 108 var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); 109 var id = Guid.Empty; 110 int result; 111 uiShell.ShowMessageBox( 112 0, ref id, 113 "Error while running clang-format:", 114 e.Message, 115 string.Empty, 0, 116 OLEMSGBUTTON.OLEMSGBUTTON_OK, 117 OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, 118 OLEMSGICON.OLEMSGICON_INFO, 119 0, out result); 120 } 121 } 122 123 /// <summary> 124 /// Runs the given text through clang-format and returns the replacements as XML. 125 /// 126 /// Formats the text range starting at offset of the given length. 127 /// </summary> 128 private string RunClangFormat(string text, int offset, int length, string path) 129 { 130 System.Diagnostics.Process process = new System.Diagnostics.Process(); 131 process.StartInfo.UseShellExecute = false; 132 process.StartInfo.FileName = "clang-format.exe"; 133 // Poor man's escaping - this will not work when quotes are already escaped 134 // in the input (but we don't need more). 135 string style = GetStyle().Replace("\"", "\\\""); 136 process.StartInfo.Arguments = " -offset " + offset + 137 " -length " + length + 138 " -output-replacements-xml " + 139 " -style \"" + style + "\""; 140 process.StartInfo.CreateNoWindow = true; 141 process.StartInfo.RedirectStandardInput = true; 142 process.StartInfo.RedirectStandardOutput = true; 143 process.StartInfo.RedirectStandardError = true; 144 if (path != null) 145 process.StartInfo.WorkingDirectory = path; 146 // We have to be careful when communicating via standard input / output, 147 // as writes to the buffers will block until they are read from the other side. 148 // Thus, we: 149 // 1. Start the process - clang-format.exe will start to read the input from the 150 // standard input. 151 process.Start(); 152 // 2. We write everything to the standard output - this cannot block, as clang-format 153 // reads the full standard input before analyzing it without writing anything to the 154 // standard output. 155 process.StandardInput.Write(text); 156 // 3. We notify clang-format that the input is done - after this point clang-format 157 // will start analyzing the input and eventually write the output. 158 process.StandardInput.Close(); 159 // 4. We must read clang-format's output before waiting for it to exit; clang-format 160 // will close the channel by exiting. 161 string output = process.StandardOutput.ReadToEnd(); 162 // 5. clang-format is done, wait until it is fully shut down. 163 process.WaitForExit(); 164 if (process.ExitCode != 0) 165 { 166 // FIXME: If clang-format writes enough to the standard error stream to block, 167 // we will never reach this point; instead, read the standard error asynchronously. 168 throw new Exception(process.StandardError.ReadToEnd()); 169 } 170 return output; 171 } 172 173 /// <summary> 174 /// Returns the currently active view if it is a IWpfTextView. 175 /// </summary> 176 private IWpfTextView GetCurrentView() 177 { 178 // The SVsTextManager is a service through which we can get the active view. 179 var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager)); 180 IVsTextView textView; 181 textManager.GetActiveView(1, null, out textView); 182 183 // Now we have the active view as IVsTextView, but the text interfaces we need 184 // are in the IWpfTextView. 185 var userData = (IVsUserData)textView; 186 if (userData == null) 187 return null; 188 Guid guidWpfViewHost = DefGuidList.guidIWpfTextViewHost; 189 object host; 190 userData.GetData(ref guidWpfViewHost, out host); 191 return ((IWpfTextViewHost)host).TextView; 192 } 193 194 private string GetStyle() 195 { 196 var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); 197 return page.Style; 198 } 199 200 private string GetDocumentParent(IWpfTextView view) 201 { 202 ITextDocument document; 203 if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document)) 204 { 205 return Directory.GetParent(document.FilePath).ToString(); 206 } 207 return null; 208 } 209 } 210} 211