//===-- 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();
}
}
}