Skip to content

Commit a16ab5a

Browse files
committed
clang-format-vsix: Add "Format on Save" feature
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
1 parent b2f1621 commit a16ab5a

File tree

4 files changed

+279
-90
lines changed

4 files changed

+279
-90
lines changed

clang/tools/clang-format-vs/ClangFormat/ClangFormat.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@
214214
</Compile>
215215
<Compile Include="Properties\AssemblyInfo.cs" />
216216
<Compile Include="PkgCmdID.cs" />
217+
<Compile Include="RunningDocTableEventsDispatcher.cs" />
218+
<Compile Include="Vsix.cs" />
217219
</ItemGroup>
218220
<ItemGroup>
219221
<EmbeddedResource Include="Resources.resx">

clang/tools/clang-format-vs/ClangFormat/ClangFormatPackage.cs

Lines changed: 102 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
using Microsoft.VisualStudio.Editor;
15+
using EnvDTE;
1616
using Microsoft.VisualStudio.Shell;
1717
using Microsoft.VisualStudio.Shell.Interop;
1818
using Microsoft.VisualStudio.Text;
1919
using Microsoft.VisualStudio.Text.Editor;
20-
using Microsoft.VisualStudio.TextManager.Interop;
2120
using System;
2221
using System.Collections;
2322
using System.ComponentModel;
2423
using System.ComponentModel.Design;
2524
using System.IO;
2625
using System.Runtime.InteropServices;
2726
using System.Xml.Linq;
27+
using System.Linq;
2828

2929
namespace LLVM.ClangFormat
3030
{
@@ -36,6 +36,17 @@ public class OptionPageGrid : DialogPage
3636
private string fallbackStyle = "LLVM";
3737
private bool sortIncludes = false;
3838
private string style = "file";
39+
private bool formatOnSave = false;
40+
private string formatOnSaveFileExtensions =
41+
".c;.cpp;.cxx;.cc;.tli;.tlh;.h;.hh;.hpp;.hxx;.hh;.inl" +
42+
".java;.js;.ts;.m;.mm;.proto;.protodevel;.td";
43+
44+
public OptionPageGrid Clone()
45+
{
46+
// Use MemberwiseClone to copy value types.
47+
var clone = (OptionPageGrid)MemberwiseClone();
48+
return clone;
49+
}
3950

4051
public class StyleConverter : TypeConverter
4152
{
@@ -74,7 +85,7 @@ public override object ConvertFrom(ITypeDescriptorContext context, System.Global
7485
}
7586
}
7687

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

124-
[Category("LLVM/Clang")]
135+
[Category("Format Options")]
125136
[DisplayName("Assume Filename")]
126137
[Description("When reading from stdin, clang-format assumes this " +
127138
"filename to look for a style config file (with 'file' style) " +
@@ -142,7 +153,7 @@ public FallbackStyleConverter()
142153
}
143154
}
144155

145-
[Category("LLVM/Clang")]
156+
[Category("Format Options")]
146157
[DisplayName("Fallback Style")]
147158
[Description("The name of the predefined style used as a fallback in case clang-format " +
148159
"is invoked with 'file' style, but can not find the configuration file.\n" +
@@ -154,7 +165,7 @@ public string FallbackStyle
154165
set { fallbackStyle = value; }
155166
}
156167

157-
[Category("LLVM/Clang")]
168+
[Category("Format Options")]
158169
[DisplayName("Sort includes")]
159170
[Description("Sort touched include lines.\n\n" +
160171
"See also: http://clang.llvm.org/docs/ClangFormat.html.")]
@@ -163,20 +174,48 @@ public bool SortIncludes
163174
get { return sortIncludes; }
164175
set { sortIncludes = value; }
165176
}
177+
178+
[Category("Format On Save")]
179+
[DisplayName("Enable")]
180+
[Description("Enable running clang-format when modified files are saved. " +
181+
"Will only format if Style is found (ignores Fallback Style)."
182+
)]
183+
public bool FormatOnSave
184+
{
185+
get { return formatOnSave; }
186+
set { formatOnSave = value; }
187+
}
188+
189+
[Category("Format On Save")]
190+
[DisplayName("File extensions")]
191+
[Description("When formatting on save, clang-format will be applied only to " +
192+
"files with these extensions.")]
193+
public string FormatOnSaveFileExtensions
194+
{
195+
get { return formatOnSaveFileExtensions; }
196+
set { formatOnSaveFileExtensions = value; }
197+
}
166198
}
167199

168200
[PackageRegistration(UseManagedResourcesOnly = true)]
169201
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
170202
[ProvideMenuResource("Menus.ctmenu", 1)]
203+
[ProvideAutoLoad(UIContextGuids80.SolutionExists)] // Load package on solution load
171204
[Guid(GuidList.guidClangFormatPkgString)]
172205
[ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)]
173206
public sealed class ClangFormatPackage : Package
174207
{
175208
#region Package Members
209+
210+
RunningDocTableEventsDispatcher _runningDocTableEventsDispatcher;
211+
176212
protected override void Initialize()
177213
{
178214
base.Initialize();
179215

216+
_runningDocTableEventsDispatcher = new RunningDocTableEventsDispatcher(this);
217+
_runningDocTableEventsDispatcher.BeforeSave += OnBeforeSave;
218+
180219
var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
181220
if (commandService != null)
182221
{
@@ -195,6 +234,11 @@ protected override void Initialize()
195234
}
196235
#endregion
197236

237+
OptionPageGrid GetUserOptions()
238+
{
239+
return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
240+
}
241+
198242
private void MenuItemCallback(object sender, EventArgs args)
199243
{
200244
var mc = sender as System.ComponentModel.Design.MenuCommand;
@@ -204,21 +248,45 @@ private void MenuItemCallback(object sender, EventArgs args)
204248
switch (mc.CommandID.ID)
205249
{
206250
case (int)PkgCmdIDList.cmdidClangFormatSelection:
207-
FormatSelection();
251+
FormatSelection(GetUserOptions());
208252
break;
209253

210254
case (int)PkgCmdIDList.cmdidClangFormatDocument:
211-
FormatDocument();
255+
FormatDocument(GetUserOptions());
212256
break;
213257
}
214258
}
215259

260+
private static bool FileHasExtension(string filePath, string fileExtensions)
261+
{
262+
var extensions = fileExtensions.ToLower().Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
263+
return extensions.Contains(Path.GetExtension(filePath).ToLower());
264+
}
265+
266+
private void OnBeforeSave(object sender, Document document)
267+
{
268+
var options = GetUserOptions();
269+
270+
if (!options.FormatOnSave)
271+
return;
272+
273+
if (!FileHasExtension(document.FullName, options.FormatOnSaveFileExtensions))
274+
return;
275+
276+
if (!Vsix.IsDocumentDirty(document))
277+
return;
278+
279+
var optionsWithNoFallbackStyle = GetUserOptions().Clone();
280+
optionsWithNoFallbackStyle.FallbackStyle = "none";
281+
FormatDocument(document, optionsWithNoFallbackStyle);
282+
}
283+
216284
/// <summary>
217285
/// Runs clang-format on the current selection
218286
/// </summary>
219-
private void FormatSelection()
287+
private void FormatSelection(OptionPageGrid options)
220288
{
221-
IWpfTextView view = GetCurrentView();
289+
IWpfTextView view = Vsix.GetCurrentView();
222290
if (view == null)
223291
// We're not in a text view.
224292
return;
@@ -231,34 +299,43 @@ private void FormatSelection()
231299
// of the file.
232300
if (start >= text.Length && text.Length > 0)
233301
start = text.Length - 1;
234-
string path = GetDocumentParent(view);
235-
string filePath = GetDocumentPath(view);
302+
string path = Vsix.GetDocumentParent(view);
303+
string filePath = Vsix.GetDocumentPath(view);
236304

237-
RunClangFormatAndApplyReplacements(text, start, length, path, filePath, view);
305+
RunClangFormatAndApplyReplacements(text, start, length, path, filePath, options, view);
238306
}
239307

240308
/// <summary>
241309
/// Runs clang-format on the current document
242310
/// </summary>
243-
private void FormatDocument()
311+
private void FormatDocument(OptionPageGrid options)
312+
{
313+
FormatView(Vsix.GetCurrentView(), options);
314+
}
315+
316+
private void FormatDocument(Document document, OptionPageGrid options)
317+
{
318+
FormatView(Vsix.GetDocumentView(document), options);
319+
}
320+
321+
private void FormatView(IWpfTextView view, OptionPageGrid options)
244322
{
245-
IWpfTextView view = GetCurrentView();
246323
if (view == null)
247324
// We're not in a text view.
248325
return;
249326

250-
string filePath = GetDocumentPath(view);
327+
string filePath = Vsix.GetDocumentPath(view);
251328
var path = Path.GetDirectoryName(filePath);
252329
string text = view.TextBuffer.CurrentSnapshot.GetText();
253330

254-
RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, view);
331+
RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, options, view);
255332
}
256333

257-
private void RunClangFormatAndApplyReplacements(string text, int offset, int length, string path, string filePath, IWpfTextView view)
334+
private void RunClangFormatAndApplyReplacements(string text, int offset, int length, string path, string filePath, OptionPageGrid options, IWpfTextView view)
258335
{
259336
try
260337
{
261-
string replacements = RunClangFormat(text, offset, length, path, filePath);
338+
string replacements = RunClangFormat(text, offset, length, path, filePath, options);
262339
ApplyClangFormatReplacements(replacements, view);
263340
}
264341
catch (Exception e)
@@ -283,7 +360,7 @@ private void RunClangFormatAndApplyReplacements(string text, int offset, int len
283360
///
284361
/// Formats the text range starting at offset of the given length.
285362
/// </summary>
286-
private string RunClangFormat(string text, int offset, int length, string path, string filePath)
363+
private static string RunClangFormat(string text, int offset, int length, string path, string filePath, OptionPageGrid options)
287364
{
288365
string vsixPath = Path.GetDirectoryName(
289366
typeof(ClangFormatPackage).Assembly.Location);
@@ -293,16 +370,16 @@ private string RunClangFormat(string text, int offset, int length, string path,
293370
process.StartInfo.FileName = vsixPath + "\\clang-format.exe";
294371
// Poor man's escaping - this will not work when quotes are already escaped
295372
// in the input (but we don't need more).
296-
string style = GetStyle().Replace("\"", "\\\"");
297-
string fallbackStyle = GetFallbackStyle().Replace("\"", "\\\"");
373+
string style = options.Style.Replace("\"", "\\\"");
374+
string fallbackStyle = options.FallbackStyle.Replace("\"", "\\\"");
298375
process.StartInfo.Arguments = " -offset " + offset +
299376
" -length " + length +
300377
" -output-replacements-xml " +
301378
" -style \"" + style + "\"" +
302379
" -fallback-style \"" + fallbackStyle + "\"";
303-
if (GetSortIncludes())
380+
if (options.SortIncludes)
304381
process.StartInfo.Arguments += " -sort-includes ";
305-
string assumeFilename = GetAssumeFilename();
382+
string assumeFilename = options.AssumeFilename;
306383
if (string.IsNullOrEmpty(assumeFilename))
307384
assumeFilename = filePath;
308385
if (!string.IsNullOrEmpty(assumeFilename))
@@ -352,7 +429,7 @@ private string RunClangFormat(string text, int offset, int length, string path,
352429
/// <summary>
353430
/// Applies the clang-format replacements (xml) to the current view
354431
/// </summary>
355-
private void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
432+
private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view)
356433
{
357434
// clang-format returns no replacements if input text is empty
358435
if (replacements.Length == 0)
@@ -369,70 +446,5 @@ private void ApplyClangFormatReplacements(string replacements, IWpfTextView view
369446
}
370447
edit.Apply();
371448
}
372-
373-
/// <summary>
374-
/// Returns the currently active view if it is a IWpfTextView.
375-
/// </summary>
376-
private IWpfTextView GetCurrentView()
377-
{
378-
// The SVsTextManager is a service through which we can get the active view.
379-
var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager));
380-
IVsTextView textView;
381-
textManager.GetActiveView(1, null, out textView);
382-
383-
// Now we have the active view as IVsTextView, but the text interfaces we need
384-
// are in the IWpfTextView.
385-
var userData = (IVsUserData)textView;
386-
if (userData == null)
387-
return null;
388-
Guid guidWpfViewHost = DefGuidList.guidIWpfTextViewHost;
389-
object host;
390-
userData.GetData(ref guidWpfViewHost, out host);
391-
return ((IWpfTextViewHost)host).TextView;
392-
}
393-
394-
private string GetStyle()
395-
{
396-
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
397-
return page.Style;
398-
}
399-
400-
private string GetAssumeFilename()
401-
{
402-
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
403-
return page.AssumeFilename;
404-
}
405-
406-
private string GetFallbackStyle()
407-
{
408-
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
409-
return page.FallbackStyle;
410-
}
411-
412-
private bool GetSortIncludes()
413-
{
414-
var page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
415-
return page.SortIncludes;
416-
}
417-
418-
private string GetDocumentParent(IWpfTextView view)
419-
{
420-
ITextDocument document;
421-
if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
422-
{
423-
return Directory.GetParent(document.FilePath).ToString();
424-
}
425-
return null;
426-
}
427-
428-
private string GetDocumentPath(IWpfTextView view)
429-
{
430-
ITextDocument document;
431-
if (view.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
432-
{
433-
return document.FilePath;
434-
}
435-
return null;
436-
}
437449
}
438450
}

0 commit comments

Comments
 (0)