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()
{
var clone = (OptionPageGrid)MemberwiseClone();
return clone;
}
public class StyleConverter : TypeConverter
{
protected ArrayList values;
public StyleConverter()
{
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);
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()
{
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)] [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);
}
private void FormatSelection(OptionPageGrid options)
{
IWpfTextView view = Vsix.GetCurrentView();
if (view == null)
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;
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);
}
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)
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);
}
}
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;
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;
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.");
}
StreamWriter utf8Writer = new StreamWriter(process.StandardInput.BaseStream, new UTF8Encoding(false));
utf8Writer.Write(text);
utf8Writer.Close();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new Exception(process.StandardError.ReadToEnd());
}
return output;
}
private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
{
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();
}
}
}