Skip to content

Commit

Permalink
clang-format-vsix: Add "Format on Save" feature
Browse files Browse the repository at this point in the history
This change adds a feature to the clang-format VS extension that optionally
enables the automatic formatting of documents when saving. Since developers
always need to save their files, this eases the workflow of making sure source
files are properly formatted.

Differential Revision: https://reviews.llvm.org/D29221

llvm-svn: 299543
  • Loading branch information
amaiorano committed Apr 5, 2017
1 parent b2f1621 commit a16ab5a
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 90 deletions.
2 changes: 2 additions & 0 deletions clang/tools/clang-format-vs/ClangFormat/ClangFormat.csproj
Expand Up @@ -214,6 +214,8 @@
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PkgCmdID.cs" />
<Compile Include="RunningDocTableEventsDispatcher.cs" />
<Compile Include="Vsix.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources.resx">
Expand Down
192 changes: 102 additions & 90 deletions clang/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs
Expand Up @@ -12,19 +12,19 @@
//
//===----------------------------------------------------------------------===//

using Microsoft.VisualStudio.Editor;
using EnvDTE;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
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
{
Expand All @@ -36,6 +36,17 @@ public class OptionPageGrid : DialogPage
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
{
Expand Down Expand Up @@ -74,7 +85,7 @@ public override object ConvertFrom(ITypeDescriptorContext context, System.Global
}
}

[Category("LLVM/Clang")]
[Category("Format Options")]
[DisplayName("Style")]
[Description("Coding style, currently supports:\n" +
" - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" +
Expand Down Expand Up @@ -121,7 +132,7 @@ public override object ConvertFrom(ITypeDescriptorContext context, System.Global
}
}

[Category("LLVM/Clang")]
[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) " +
Expand All @@ -142,7 +153,7 @@ public FallbackStyleConverter()
}
}

[Category("LLVM/Clang")]
[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" +
Expand All @@ -154,7 +165,7 @@ public string FallbackStyle
set { fallbackStyle = value; }
}

[Category("LLVM/Clang")]
[Category("Format Options")]
[DisplayName("Sort includes")]
[Description("Sort touched include lines.\n\n" +
"See also: http://clang.llvm.org/docs/ClangFormat.html.")]
Expand All @@ -163,20 +174,48 @@ 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)
{
Expand All @@ -195,6 +234,11 @@ protected override void Initialize()
}
#endregion

OptionPageGrid GetUserOptions()
{
return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
}

private void MenuItemCallback(object sender, EventArgs args)
{
var mc = sender as System.ComponentModel.Design.MenuCommand;
Expand All @@ -204,21 +248,45 @@ private void MenuItemCallback(object sender, EventArgs args)
switch (mc.CommandID.ID)
{
case (int)PkgCmdIDList.cmdidClangFormatSelection:
FormatSelection();
FormatSelection(GetUserOptions());
break;

case (int)PkgCmdIDList.cmdidClangFormatDocument:
FormatDocument();
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()
private void FormatSelection(OptionPageGrid options)
{
IWpfTextView view = GetCurrentView();
IWpfTextView view = Vsix.GetCurrentView();
if (view == null)
// We're not in a text view.
return;
Expand All @@ -231,34 +299,43 @@ private void FormatSelection()
// of the file.
if (start >= text.Length && text.Length > 0)
start = text.Length - 1;
string path = GetDocumentParent(view);
string filePath = GetDocumentPath(view);
string path = Vsix.GetDocumentParent(view);
string filePath = Vsix.GetDocumentPath(view);

RunClangFormatAndApplyReplacements(text, start, length, path, filePath, view);
RunClangFormatAndApplyReplacements(text, start, length, path, filePath, options, view);
}

/// <summary>
/// Runs clang-format on the current document
/// </summary>
private void FormatDocument()
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)
{
IWpfTextView view = GetCurrentView();
if (view == null)
// We're not in a text view.
return;

string filePath = GetDocumentPath(view);
string filePath = Vsix.GetDocumentPath(view);
var path = Path.GetDirectoryName(filePath);
string text = view.TextBuffer.CurrentSnapshot.GetText();

RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, view);
RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, options, view);
}

private void RunClangFormatAndApplyReplacements(string text, int offset, int length, string path, string filePath, IWpfTextView 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);
string replacements = RunClangFormat(text, offset, length, path, filePath, options);
ApplyClangFormatReplacements(replacements, view);
}
catch (Exception e)
Expand All @@ -283,7 +360,7 @@ private void RunClangFormatAndApplyReplacements(string text, int offset, int len
///
/// Formats the text range starting at offset of the given length.
/// </summary>
private string RunClangFormat(string text, int offset, int length, string path, string filePath)
private static string RunClangFormat(string text, int offset, int length, string path, string filePath, OptionPageGrid options)
{
string vsixPath = Path.GetDirectoryName(
typeof(ClangFormatPackage).Assembly.Location);
Expand All @@ -293,16 +370,16 @@ private string RunClangFormat(string text, int offset, int length, string path,
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 = GetStyle().Replace("\"", "\\\"");
string fallbackStyle = GetFallbackStyle().Replace("\"", "\\\"");
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 (GetSortIncludes())
if (options.SortIncludes)
process.StartInfo.Arguments += " -sort-includes ";
string assumeFilename = GetAssumeFilename();
string assumeFilename = options.AssumeFilename;
if (string.IsNullOrEmpty(assumeFilename))
assumeFilename = filePath;
if (!string.IsNullOrEmpty(assumeFilename))
Expand Down Expand Up @@ -352,7 +429,7 @@ private string RunClangFormat(string text, int offset, int length, string path,
/// <summary>
/// Applies the clang-format replacements (xml) to the current view
/// </summary>
private void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
{
// clang-format returns no replacements if input text is empty
if (replacements.Length == 0)
Expand All @@ -369,70 +446,5 @@ private void ApplyClangFormatReplacements(string replacements, IWpfTextView view
}
edit.Apply();
}

/// <summary>
/// Returns the currently active view if it is a IWpfTextView.
/// </summary>
private IWpfTextView GetCurrentView()
{
// The SVsTextManager is a service through which we can get the active view.
var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager));
IVsTextView textView;
textManager.GetActiveView(1, null, out textView);

// Now we have the active view as IVsTextView, but the text interfaces we need
// are in the IWpfTextView.
var userData = (IVsUserData)textView;
if (userData == null)
return null;
Guid guidWpfViewHost = DefGuidList.guidIWpfTextViewHost;
object host;
userData.GetData(ref guidWpfViewHost, out host);
return ((IWpfTextViewHost)host).TextView;
}

private string GetStyle()
{
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
return page.Style;
}

private string GetAssumeFilename()
{
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
return page.AssumeFilename;
}

private string GetFallbackStyle()
{
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
return page.FallbackStyle;
}

private bool GetSortIncludes()
{
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
return page.SortIncludes;
}

private string GetDocumentParent(IWpfTextView view)
{
ITextDocument document;
if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
{
return Directory.GetParent(document.FilePath).ToString();
}
return null;
}

private string GetDocumentPath(IWpfTextView view)
{
ITextDocument document;
if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
{
return document.FilePath;
}
return null;
}
}
}

0 comments on commit a16ab5a

Please sign in to comment.