Clang Project

clang_source_code/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs
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
14using EnvDTE;
15using Microsoft.VisualStudio.Shell;
16using Microsoft.VisualStudio.Shell.Interop;
17using Microsoft.VisualStudio.Text;
18using Microsoft.VisualStudio.Text.Editor;
19using System;
20using System.Collections;
21using System.ComponentModel;
22using System.ComponentModel.Design;
23using System.IO;
24using System.Runtime.InteropServices;
25using System.Xml.Linq;
26using System.Linq;
27
28namespace LLVM.ClangFormat
29{
30    [ClassInterface(ClassInterfaceType.AutoDual)]
31    [CLSCompliant(false), ComVisible(true)]
32    public class OptionPageGrid : DialogPage
33    {
34        private string assumeFilename = "";
35        private string fallbackStyle = "LLVM";
36        private bool sortIncludes = false;
37        private string style = "file";
38        private bool formatOnSave = false;
39        private string formatOnSaveFileExtensions =
40            ".c;.cpp;.cxx;.cc;.tli;.tlh;.h;.hh;.hpp;.hxx;.hh;.inl;" +
41            ".java;.js;.ts;.m;.mm;.proto;.protodevel;.td";
42
43        public OptionPageGrid Clone()
44        {
45            // Use MemberwiseClone to copy value types.
46            var clone = (OptionPageGrid)MemberwiseClone();
47            return clone;
48        }
49
50        public class StyleConverter : TypeConverter
51        {
52            protected ArrayList values;
53            public StyleConverter()
54            {
55                // Initializes the standard values list with defaults.
56                values = new ArrayList(new string[] { "file", "Chromium", "Google", "LLVM", "Mozilla", "WebKit" });
57            }
58
59            public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
60            {
61                return true;
62            }
63
64            public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
65            {
66                return new StandardValuesCollection(values);
67            }
68
69            public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
70            {
71                if (sourceType == typeof(string))
72                    return true;
73
74                return base.CanConvertFrom(context, sourceType);
75            }
76
77            public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
78            {
79                string s = value as string;
80                if (s == null)
81                    return base.ConvertFrom(context, culture, value);
82
83                return value;
84            }
85        }
86
87        [Category("Format Options")]
88        [DisplayName("Style")]
89        [Description("Coding style, currently supports:\n" +
90                     "  - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" +
91                     "  - 'file' to search for a YAML .clang-format or _clang-format\n" +
92                     "    configuration file.\n" +
93                     "  - A YAML configuration snippet.\n\n" +
94                     "'File':\n" +
95                     "  Searches for a .clang-format or _clang-format configuration file\n" +
96                     "  in the source file's directory and its parents.\n\n" +
97                     "YAML configuration snippet:\n" +
98                     "  The content of a .clang-format configuration file, as string.\n" +
99                     "  Example: '{BasedOnStyle: \"LLVM\", IndentWidth: 8}'\n\n" +
100                     "See also: http://clang.llvm.org/docs/ClangFormatStyleOptions.html.")]
101        [TypeConverter(typeof(StyleConverter))]
102        public string Style
103        {
104            get { return style; }
105            set { style = value; }
106        }
107
108        public sealed class FilenameConverter : TypeConverter
109        {
110            public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
111            {
112                if (sourceType == typeof(string))
113                    return true;
114
115                return base.CanConvertFrom(context, sourceType);
116            }
117
118            public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
119            {
120                string s = value as string;
121                if (s == null)
122                    return base.ConvertFrom(context, culture, value);
123
124                // Check if string contains quotes. On Windows, file names cannot contain quotes.
125                // We do not accept them however to avoid hard-to-debug problems.
126                // A quote in user input would end the parameter quote and so break the command invocation.
127                if (s.IndexOf('\"') != -1)
128                    throw new NotSupportedException("Filename cannot contain quotes");
129
130                return value;
131            }
132        }
133
134        [Category("Format Options")]
135        [DisplayName("Assume Filename")]
136        [Description("When reading from stdin, clang-format assumes this " +
137                     "filename to look for a style config file (with 'file' style) " +
138                     "and to determine the language.")]
139        [TypeConverter(typeof(FilenameConverter))]
140        public string AssumeFilename
141        {
142            get { return assumeFilename; }
143            set { assumeFilename = value; }
144        }
145
146        public sealed class FallbackStyleConverter : StyleConverter
147        {
148            public FallbackStyleConverter()
149            {
150                // Add "none" to the list of styles.
151                values.Insert(0, "none");
152            }
153        }
154
155        [Category("Format Options")]
156        [DisplayName("Fallback Style")]
157        [Description("The name of the predefined style used as a fallback in case clang-format " +
158                     "is invoked with 'file' style, but can not find the configuration file.\n" +
159                     "Use 'none' fallback style to skip formatting.")]
160        [TypeConverter(typeof(FallbackStyleConverter))]
161        public string FallbackStyle
162        {
163            get { return fallbackStyle; }
164            set { fallbackStyle = value; }
165        }
166
167        [Category("Format Options")]
168        [DisplayName("Sort includes")]
169        [Description("Sort touched include lines.\n\n" +
170                     "See also: http://clang.llvm.org/docs/ClangFormat.html.")]
171        public bool SortIncludes
172        {
173            get { return sortIncludes; }
174            set { sortIncludes = value; }
175        }
176
177        [Category("Format On Save")]
178        [DisplayName("Enable")]
179        [Description("Enable running clang-format when modified files are saved. " +
180                     "Will only format if Style is found (ignores Fallback Style)."
181            )]
182        public bool FormatOnSave
183        {
184            get { return formatOnSave; }
185            set { formatOnSave = value; }
186        }
187
188        [Category("Format On Save")]
189        [DisplayName("File extensions")]
190        [Description("When formatting on save, clang-format will be applied only to " +
191                     "files with these extensions.")]
192        public string FormatOnSaveFileExtensions
193        {
194            get { return formatOnSaveFileExtensions; }
195            set { formatOnSaveFileExtensions = value; }
196        }
197    }
198
199    [PackageRegistration(UseManagedResourcesOnly = true)]
200    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
201    [ProvideMenuResource("Menus.ctmenu", 1)]
202    [ProvideAutoLoad(UIContextGuids80.SolutionExists)] // Load package on solution load
203    [Guid(GuidList.guidClangFormatPkgString)]
204    [ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)]
205    public sealed class ClangFormatPackage : Package
206    {
207        #region Package Members
208
209        RunningDocTableEventsDispatcher _runningDocTableEventsDispatcher;
210
211        protected override void Initialize()
212        {
213            base.Initialize();
214
215            _runningDocTableEventsDispatcher = new RunningDocTableEventsDispatcher(this);
216            _runningDocTableEventsDispatcher.BeforeSave += OnBeforeSave;
217
218            var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
219            if (commandService != null)
220            {
221                {
222                    var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatSelection);
223                    var menuItem = new MenuCommand(MenuItemCallback, menuCommandID);
224                    commandService.AddCommand(menuItem);
225                }
226
227                {
228                    var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatDocument);
229                    var menuItem = new MenuCommand(MenuItemCallback, menuCommandID);
230                    commandService.AddCommand(menuItem);
231                }
232            }
233        }
234        #endregion
235
236        OptionPageGrid GetUserOptions()
237        {
238            return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
239        }
240
241        private void MenuItemCallback(object sender, EventArgs args)
242        {
243            var mc = sender as System.ComponentModel.Design.MenuCommand;
244            if (mc == null)
245                return;
246
247            switch (mc.CommandID.ID)
248            {
249                case (int)PkgCmdIDList.cmdidClangFormatSelection:
250                    FormatSelection(GetUserOptions());
251                    break;
252
253                case (int)PkgCmdIDList.cmdidClangFormatDocument:
254                    FormatDocument(GetUserOptions());
255                    break;
256            }
257        }
258
259        private static bool FileHasExtension(string filePath, string fileExtensions)
260        {
261            var extensions = fileExtensions.ToLower().Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
262            return extensions.Contains(Path.GetExtension(filePath).ToLower());
263        }
264
265        private void OnBeforeSave(object sender, Document document)
266        {
267            var options = GetUserOptions();
268
269            if (!options.FormatOnSave)
270                return;
271
272            if (!FileHasExtension(document.FullName, options.FormatOnSaveFileExtensions))
273                return;
274
275            if (!Vsix.IsDocumentDirty(document))
276                return;
277
278            var optionsWithNoFallbackStyle = GetUserOptions().Clone();
279            optionsWithNoFallbackStyle.FallbackStyle = "none";
280            FormatDocument(document, optionsWithNoFallbackStyle);
281        }
282
283        /// <summary>
284        /// Runs clang-format on the current selection
285        /// </summary>
286        private void FormatSelection(OptionPageGrid options)
287        {
288            IWpfTextView view = Vsix.GetCurrentView();
289            if (view == null)
290                // We're not in a text view.
291                return;
292            string text = view.TextBuffer.CurrentSnapshot.GetText();
293            int start = view.Selection.Start.Position.GetContainingLine().Start.Position;
294            int end = view.Selection.End.Position.GetContainingLine().End.Position;
295            int length = end - start;
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, length, 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 offset, int length, string path, string filePath, OptionPageGrid options, IWpfTextView view)
340        {
341            try
342            {
343                string replacements = RunClangFormat(text, offset, length, 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 range starting at offset of the given length.
367        /// </summary>
368        private static string RunClangFormat(string text, int offset, int length, 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            // Poor man's escaping - this will not work when quotes are already escaped
377            // in the input (but we don't need more).
378            string style = options.Style.Replace("\"", "\\\"");
379            string fallbackStyle = options.FallbackStyle.Replace("\"", "\\\"");
380            process.StartInfo.Arguments = " -offset " + offset +
381                                          " -length " + length +
382                                          " -output-replacements-xml " +
383                                          " -style \"" + style + "\"" +
384                                          " -fallback-style \"" + fallbackStyle + "\"";
385            if (options.SortIncludes)
386              process.StartInfo.Arguments += " -sort-includes ";
387            string assumeFilename = options.AssumeFilename;
388            if (string.IsNullOrEmpty(assumeFilename))
389                assumeFilename = filePath;
390            if (!string.IsNullOrEmpty(assumeFilename))
391              process.StartInfo.Arguments += " -assume-filename \"" + assumeFilename + "\"";
392            process.StartInfo.CreateNoWindow = true;
393            process.StartInfo.RedirectStandardInput = true;
394            process.StartInfo.RedirectStandardOutput = true;
395            process.StartInfo.RedirectStandardError = true;
396            if (path != null)
397                process.StartInfo.WorkingDirectory = path;
398            // We have to be careful when communicating via standard input / output,
399            // as writes to the buffers will block until they are read from the other side.
400            // Thus, we:
401            // 1. Start the process - clang-format.exe will start to read the input from the
402            //    standard input.
403            try
404            {
405                process.Start();
406            }
407            catch (Exception e)
408            {
409                throw new Exception(
410                    "Cannot execute " + process.StartInfo.FileName + ".\n\"" + 
411                    e.Message + "\".\nPlease make sure it is on the PATH.");
412            }
413            // 2. We write everything to the standard output - this cannot block, as clang-format
414            //    reads the full standard input before analyzing it without writing anything to the
415            //    standard output.
416            process.StandardInput.Write(text);
417            // 3. We notify clang-format that the input is done - after this point clang-format
418            //    will start analyzing the input and eventually write the output.
419            process.StandardInput.Close();
420            // 4. We must read clang-format's output before waiting for it to exit; clang-format
421            //    will close the channel by exiting.
422            string output = process.StandardOutput.ReadToEnd();
423            // 5. clang-format is done, wait until it is fully shut down.
424            process.WaitForExit();
425            if (process.ExitCode != 0)
426            {
427                // FIXME: If clang-format writes enough to the standard error stream to block,
428                // we will never reach this point; instead, read the standard error asynchronously.
429                throw new Exception(process.StandardError.ReadToEnd());
430            }
431            return output;
432        }
433
434        /// <summary>
435        /// Applies the clang-format replacements (xml) to the current view
436        /// </summary>
437        private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
438        {
439            // clang-format returns no replacements if input text is empty
440            if (replacements.Length == 0)
441                return;
442
443            var root = XElement.Parse(replacements);
444            var edit = view.TextBuffer.CreateEdit();
445            foreach (XElement replacement in root.Descendants("replacement"))
446            {
447                var span = new Span(
448                    int.Parse(replacement.Attribute("offset").Value),
449                    int.Parse(replacement.Attribute("length").Value));
450                edit.Replace(span, replacement.Value);
451            }
452            edit.Apply();
453        }
454    }
455}
456