//===-- ClangFormatPackages.cs - VSPackage for clang-format ------*- C# -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // This class contains a VS extension package that runs clang-format over a // selection in a VS text editor. // //===----------------------------------------------------------------------===// using EnvDTE; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.IO; using System.Runtime.InteropServices; using System.Xml.Linq; using System.Linq; using System.Text; namespace LLVM.ClangFormat { [ClassInterface(ClassInterfaceType.AutoDual)] [CLSCompliant(false), ComVisible(true)] public class OptionPageGrid : DialogPage { private string assumeFilename = ""; private string fallbackStyle = "LLVM"; private bool sortIncludes = false; private string style = "file"; private bool formatOnSave = false; private string formatOnSaveFileExtensions = ".c;.cpp;.cxx;.cc;.tli;.tlh;.h;.hh;.hpp;.hxx;.hh;.inl;" + ".java;.js;.ts;.m;.mm;.proto;.protodevel;.td"; public OptionPageGrid Clone() { // Use MemberwiseClone to copy value types. var clone = (OptionPageGrid)MemberwiseClone(); return clone; } public class StyleConverter : TypeConverter { protected ArrayList values; public StyleConverter() { // Initializes the standard values list with defaults. values = new ArrayList(new string[] { "file", "Chromium", "Google", "LLVM", "Mozilla", "WebKit" }); } public override bool GetStandardValuesSupported(ITypeDescriptorContext context) { return true; } public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) { return new StandardValuesCollection(values); } public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) return true; return base.CanConvertFrom(context, sourceType); } public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { string s = value as string; if (s == null) return base.ConvertFrom(context, culture, value); return value; } } [Category("Format Options")] [DisplayName("Style")] [Description("Coding style, currently supports:\n" + " - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" + " - 'file' to search for a YAML .clang-format or _clang-format\n" + " configuration file.\n" + " - A YAML configuration snippet.\n\n" + "'File':\n" + " Searches for a .clang-format or _clang-format configuration file\n" + " in the source file's directory and its parents.\n\n" + "YAML configuration snippet:\n" + " The content of a .clang-format configuration file, as string.\n" + " Example: '{BasedOnStyle: \"LLVM\", IndentWidth: 8}'\n\n" + "See also: http://clang.llvm.org/docs/ClangFormatStyleOptions.html.")] [TypeConverter(typeof(StyleConverter))] public string Style { get { return style; } set { style = value; } } public sealed class FilenameConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) return true; return base.CanConvertFrom(context, sourceType); } public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { string s = value as string; if (s == null) return base.ConvertFrom(context, culture, value); // Check if string contains quotes. On Windows, file names cannot contain quotes. // We do not accept them however to avoid hard-to-debug problems. // A quote in user input would end the parameter quote and so break the command invocation. if (s.IndexOf('\"') != -1) throw new NotSupportedException("Filename cannot contain quotes"); return value; } } [Category("Format Options")] [DisplayName("Assume Filename")] [Description("When reading from stdin, clang-format assumes this " + "filename to look for a style config file (with 'file' style) " + "and to determine the language.")] [TypeConverter(typeof(FilenameConverter))] public string AssumeFilename { get { return assumeFilename; } set { assumeFilename = value; } } public sealed class FallbackStyleConverter : StyleConverter { public FallbackStyleConverter() { // Add "none" to the list of styles. values.Insert(0, "none"); } } [Category("Format Options")] [DisplayName("Fallback Style")] [Description("The name of the predefined style used as a fallback in case clang-format " + "is invoked with 'file' style, but can not find the configuration file.\n" + "Use 'none' fallback style to skip formatting.")] [TypeConverter(typeof(FallbackStyleConverter))] public string FallbackStyle { get { return fallbackStyle; } set { fallbackStyle = value; } } [Category("Format Options")] [DisplayName("Sort includes")] [Description("Sort touched include lines.\n\n" + "See also: http://clang.llvm.org/docs/ClangFormat.html.")] public bool SortIncludes { get { return sortIncludes; } set { sortIncludes = value; } } [Category("Format On Save")] [DisplayName("Enable")] [Description("Enable running clang-format when modified files are saved. " + "Will only format if Style is found (ignores Fallback Style)." )] public bool FormatOnSave { get { return formatOnSave; } set { formatOnSave = value; } } [Category("Format On Save")] [DisplayName("File extensions")] [Description("When formatting on save, clang-format will be applied only to " + "files with these extensions.")] public string FormatOnSaveFileExtensions { get { return formatOnSaveFileExtensions; } set { formatOnSaveFileExtensions = value; } } } [PackageRegistration(UseManagedResourcesOnly = true)] [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] [ProvideMenuResource("Menus.ctmenu", 1)] [ProvideAutoLoad(UIContextGuids80.SolutionExists)] // Load package on solution load [Guid(GuidList.guidClangFormatPkgString)] [ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)] public sealed class ClangFormatPackage : Package { #region Package Members RunningDocTableEventsDispatcher _runningDocTableEventsDispatcher; protected override void Initialize() { base.Initialize(); _runningDocTableEventsDispatcher = new RunningDocTableEventsDispatcher(this); _runningDocTableEventsDispatcher.BeforeSave += OnBeforeSave; var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService; if (commandService != null) { { var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatSelection); var menuItem = new MenuCommand(MenuItemCallback, menuCommandID); commandService.AddCommand(menuItem); } { var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatDocument); var menuItem = new MenuCommand(MenuItemCallback, menuCommandID); commandService.AddCommand(menuItem); } } } #endregion OptionPageGrid GetUserOptions() { return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); } private void MenuItemCallback(object sender, EventArgs args) { var mc = sender as System.ComponentModel.Design.MenuCommand; if (mc == null) return; switch (mc.CommandID.ID) { case (int)PkgCmdIDList.cmdidClangFormatSelection: FormatSelection(GetUserOptions()); break; case (int)PkgCmdIDList.cmdidClangFormatDocument: FormatDocument(GetUserOptions()); break; } } private static bool FileHasExtension(string filePath, string fileExtensions) { var extensions = fileExtensions.ToLower().Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); return extensions.Contains(Path.GetExtension(filePath).ToLower()); } private void OnBeforeSave(object sender, Document document) { var options = GetUserOptions(); if (!options.FormatOnSave) return; if (!FileHasExtension(document.FullName, options.FormatOnSaveFileExtensions)) return; if (!Vsix.IsDocumentDirty(document)) return; var optionsWithNoFallbackStyle = GetUserOptions().Clone(); optionsWithNoFallbackStyle.FallbackStyle = "none"; FormatDocument(document, optionsWithNoFallbackStyle); } /// /// Runs clang-format on the current selection /// private void FormatSelection(OptionPageGrid options) { IWpfTextView view = Vsix.GetCurrentView(); if (view == null) // We're not in a text view. return; string text = view.TextBuffer.CurrentSnapshot.GetText(); int start = view.Selection.Start.Position.GetContainingLine().Start.Position; int end = view.Selection.End.Position.GetContainingLine().End.Position; // clang-format doesn't support formatting a range that starts at the end // of the file. if (start >= text.Length && text.Length > 0) start = text.Length - 1; string path = Vsix.GetDocumentParent(view); string filePath = Vsix.GetDocumentPath(view); RunClangFormatAndApplyReplacements(text, start, end, path, filePath, options, view); } /// /// Runs clang-format on the current document /// private void FormatDocument(OptionPageGrid options) { FormatView(Vsix.GetCurrentView(), options); } private void FormatDocument(Document document, OptionPageGrid options) { FormatView(Vsix.GetDocumentView(document), options); } private void FormatView(IWpfTextView view, OptionPageGrid options) { if (view == null) // We're not in a text view. return; string filePath = Vsix.GetDocumentPath(view); var path = Path.GetDirectoryName(filePath); string text = view.TextBuffer.CurrentSnapshot.GetText(); if (!text.EndsWith(Environment.NewLine)) { view.TextBuffer.Insert(view.TextBuffer.CurrentSnapshot.Length, Environment.NewLine); text += Environment.NewLine; } RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, options, view); } private void RunClangFormatAndApplyReplacements(string text, int start, int end, string path, string filePath, OptionPageGrid options, IWpfTextView view) { try { string replacements = RunClangFormat(text, start, end, path, filePath, options); ApplyClangFormatReplacements(replacements, view); } catch (Exception e) { var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); var id = Guid.Empty; int result; uiShell.ShowMessageBox( 0, ref id, "Error while running clang-format:", e.Message, string.Empty, 0, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, OLEMSGICON.OLEMSGICON_INFO, 0, out result); } } /// /// Runs the given text through clang-format and returns the replacements as XML. /// /// Formats the text in range start and end. /// private static string RunClangFormat(string text, int start, int end, string path, string filePath, OptionPageGrid options) { string vsixPath = Path.GetDirectoryName( typeof(ClangFormatPackage).Assembly.Location); System.Diagnostics.Process process = new System.Diagnostics.Process(); process.StartInfo.UseShellExecute = false; process.StartInfo.FileName = vsixPath + "\\clang-format.exe"; char[] chars = text.ToCharArray(); int offset = Encoding.UTF8.GetByteCount(chars, 0, start); int length = Encoding.UTF8.GetByteCount(chars, 0, end) - offset; // Poor man's escaping - this will not work when quotes are already escaped // in the input (but we don't need more). string style = options.Style.Replace("\"", "\\\""); string fallbackStyle = options.FallbackStyle.Replace("\"", "\\\""); process.StartInfo.Arguments = " -offset " + offset + " -length " + length + " -output-replacements-xml " + " -style \"" + style + "\"" + " -fallback-style \"" + fallbackStyle + "\""; if (options.SortIncludes) process.StartInfo.Arguments += " -sort-includes "; string assumeFilename = options.AssumeFilename; if (string.IsNullOrEmpty(assumeFilename)) assumeFilename = filePath; if (!string.IsNullOrEmpty(assumeFilename)) process.StartInfo.Arguments += " -assume-filename \"" + assumeFilename + "\""; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardInput = true; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; if (path != null) process.StartInfo.WorkingDirectory = path; // We have to be careful when communicating via standard input / output, // as writes to the buffers will block until they are read from the other side. // Thus, we: // 1. Start the process - clang-format.exe will start to read the input from the // standard input. try { process.Start(); } catch (Exception e) { throw new Exception( "Cannot execute " + process.StartInfo.FileName + ".\n\"" + e.Message + "\".\nPlease make sure it is on the PATH."); } // 2. We write everything to the standard output - this cannot block, as clang-format // reads the full standard input before analyzing it without writing anything to the // standard output. StreamWriter utf8Writer = new StreamWriter(process.StandardInput.BaseStream, new UTF8Encoding(false)); utf8Writer.Write(text); // 3. We notify clang-format that the input is done - after this point clang-format // will start analyzing the input and eventually write the output. utf8Writer.Close(); // 4. We must read clang-format's output before waiting for it to exit; clang-format // will close the channel by exiting. string output = process.StandardOutput.ReadToEnd(); // 5. clang-format is done, wait until it is fully shut down. process.WaitForExit(); if (process.ExitCode != 0) { // FIXME: If clang-format writes enough to the standard error stream to block, // we will never reach this point; instead, read the standard error asynchronously. throw new Exception(process.StandardError.ReadToEnd()); } return output; } /// /// Applies the clang-format replacements (xml) to the current view /// private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view) { // clang-format returns no replacements if input text is empty if (replacements.Length == 0) return; string text = view.TextBuffer.CurrentSnapshot.GetText(); byte[] bytes = Encoding.UTF8.GetBytes(text); var root = XElement.Parse(replacements); var edit = view.TextBuffer.CreateEdit(); foreach (XElement replacement in root.Descendants("replacement")) { int offset = int.Parse(replacement.Attribute("offset").Value); int length = int.Parse(replacement.Attribute("length").Value); var span = new Span( Encoding.UTF8.GetCharCount(bytes, 0, offset), Encoding.UTF8.GetCharCount(bytes, offset, length)); edit.Replace(span, replacement.Value); } edit.Apply(); } } }