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