forked from OSchip/llvm-project
456 lines
18 KiB
C#
456 lines
18 KiB
C#
//===-- 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;
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs clang-format on the current selection
|
|
/// </summary>
|
|
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;
|
|
int length = end - start;
|
|
|
|
// 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, length, path, filePath, options, view);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs clang-format on the current document
|
|
/// </summary>
|
|
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 offset, int length, string path, string filePath, OptionPageGrid options, IWpfTextView view)
|
|
{
|
|
try
|
|
{
|
|
string replacements = RunClangFormat(text, offset, length, 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs the given text through clang-format and returns the replacements as XML.
|
|
///
|
|
/// Formats the text range starting at offset of the given length.
|
|
/// </summary>
|
|
private static string RunClangFormat(string text, int offset, int length, 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";
|
|
// 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.
|
|
process.StandardInput.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.
|
|
process.StandardInput.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the clang-format replacements (xml) to the current view
|
|
/// </summary>
|
|
private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
|
|
{
|
|
// clang-format returns no replacements if input text is empty
|
|
if (replacements.Length == 0)
|
|
return;
|
|
|
|
var root = XElement.Parse(replacements);
|
|
var edit = view.TextBuffer.CreateEdit();
|
|
foreach (XElement replacement in root.Descendants("replacement"))
|
|
{
|
|
var span = new Span(
|
|
int.Parse(replacement.Attribute("offset").Value),
|
|
int.Parse(replacement.Attribute("length").Value));
|
|
edit.Replace(span, replacement.Value);
|
|
}
|
|
edit.Apply();
|
|
}
|
|
}
|
|
}
|