1
//===-- ClangFormatPackages.cs - VSPackage for clang-format ------*- C# -*-===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 // This class contains a VS extension package that runs clang-format over a
10 // selection in a VS text editor.
12 //===----------------------------------------------------------------------===//
15 using Microsoft
.VisualStudio
.Shell
;
16 using Microsoft
.VisualStudio
.Shell
.Interop
;
17 using Microsoft
.VisualStudio
.Text
;
18 using Microsoft
.VisualStudio
.Text
.Editor
;
20 using System
.Collections
;
21 using System
.ComponentModel
;
22 using System
.ComponentModel
.Design
;
24 using System
.Runtime
.InteropServices
;
25 using System
.Xml
.Linq
;
29 namespace LLVM
.ClangFormat
31 [ClassInterface(ClassInterfaceType
.AutoDual
)]
32 [CLSCompliant(false), ComVisible(true)]
33 public class OptionPageGrid
: DialogPage
35 private string assumeFilename
= "";
36 private string fallbackStyle
= "LLVM";
37 private bool sortIncludes
= false;
38 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";
44 public OptionPageGrid
Clone()
46 // Use MemberwiseClone to copy value types.
47 var clone
= (OptionPageGrid
)MemberwiseClone();
51 public class StyleConverter
: TypeConverter
53 protected ArrayList values
;
54 public StyleConverter()
56 // Initializes the standard values list with defaults.
57 values
= new ArrayList(new string[] { "file", "Chromium", "Google", "LLVM", "Mozilla", "WebKit" }
);
60 public override bool GetStandardValuesSupported(ITypeDescriptorContext context
)
65 public override StandardValuesCollection
GetStandardValues(ITypeDescriptorContext context
)
67 return new StandardValuesCollection(values
);
70 public override bool CanConvertFrom(ITypeDescriptorContext context
, Type sourceType
)
72 if (sourceType
== typeof(string))
75 return base.CanConvertFrom(context
, sourceType
);
78 public override object ConvertFrom(ITypeDescriptorContext context
, System
.Globalization
.CultureInfo culture
, object value)
80 string s
= value as string;
82 return base.ConvertFrom(context
, culture
, value);
88 [Category("Format Options")]
89 [DisplayName("Style")]
90 [Description("Coding style, currently supports:\n" +
91 " - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" +
92 " - 'file' to search for a YAML .clang-format or _clang-format\n" +
93 " configuration file.\n" +
94 " - A YAML configuration snippet.\n\n" +
96 " Searches for a .clang-format or _clang-format configuration file\n" +
97 " in the source file's directory and its parents.\n\n" +
98 "YAML configuration snippet:\n" +
99 " The content of a .clang-format configuration file, as string.\n" +
100 " Example: '{BasedOnStyle: \"LLVM\", IndentWidth: 8}'\n\n" +
101 "See also: http://clang.llvm.org/docs/ClangFormatStyleOptions.html.")]
102 [TypeConverter(typeof(StyleConverter
))]
105 get { return style; }
106 set { style = value; }
109 public sealed class FilenameConverter
: TypeConverter
111 public override bool CanConvertFrom(ITypeDescriptorContext context
, Type sourceType
)
113 if (sourceType
== typeof(string))
116 return base.CanConvertFrom(context
, sourceType
);
119 public override object ConvertFrom(ITypeDescriptorContext context
, System
.Globalization
.CultureInfo culture
, object value)
121 string s
= value as string;
123 return base.ConvertFrom(context
, culture
, value);
125 // Check if string contains quotes. On Windows, file names cannot contain quotes.
126 // We do not accept them however to avoid hard-to-debug problems.
127 // A quote in user input would end the parameter quote and so break the command invocation.
128 if (s
.IndexOf('\"') != -1)
129 throw new NotSupportedException("Filename cannot contain quotes");
135 [Category("Format Options")]
136 [DisplayName("Assume Filename")]
137 [Description("When reading from stdin, clang-format assumes this " +
138 "filename to look for a style config file (with 'file' style) " +
139 "and to determine the language.")]
140 [TypeConverter(typeof(FilenameConverter
))]
141 public string AssumeFilename
143 get { return assumeFilename; }
144 set { assumeFilename = value; }
147 public sealed class FallbackStyleConverter
: StyleConverter
149 public FallbackStyleConverter()
151 // Add "none" to the list of styles.
152 values
.Insert(0, "none");
156 [Category("Format Options")]
157 [DisplayName("Fallback Style")]
158 [Description("The name of the predefined style used as a fallback in case clang-format " +
159 "is invoked with 'file' style, but can not find the configuration file.\n" +
160 "Use 'none' fallback style to skip formatting.")]
161 [TypeConverter(typeof(FallbackStyleConverter
))]
162 public string FallbackStyle
164 get { return fallbackStyle; }
165 set { fallbackStyle = value; }
168 [Category("Format Options")]
169 [DisplayName("Sort includes")]
170 [Description("Sort touched include lines.\n\n" +
171 "See also: http://clang.llvm.org/docs/ClangFormat.html.")]
172 public bool SortIncludes
174 get { return sortIncludes; }
175 set { sortIncludes = value; }
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)."
183 public bool FormatOnSave
185 get { return formatOnSave; }
186 set { formatOnSave = value; }
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
195 get { return formatOnSaveFileExtensions; }
196 set { formatOnSaveFileExtensions = value; }
200 [PackageRegistration(UseManagedResourcesOnly
= true)]
201 [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID
= 400)]
202 [ProvideMenuResource("Menus.ctmenu", 1)]
203 [ProvideAutoLoad(UIContextGuids80
.SolutionExists
)] // Load package on solution load
204 [Guid(GuidList
.guidClangFormatPkgString
)]
205 [ProvideOptionPage(typeof(OptionPageGrid
), "LLVM/Clang", "ClangFormat", 0, 0, true)]
206 public sealed class ClangFormatPackage
: Package
208 #region Package Members
210 RunningDocTableEventsDispatcher _runningDocTableEventsDispatcher
;
212 protected override void Initialize()
216 _runningDocTableEventsDispatcher
= new RunningDocTableEventsDispatcher(this);
217 _runningDocTableEventsDispatcher
.BeforeSave
+= OnBeforeSave
;
219 var commandService
= GetService(typeof(IMenuCommandService
)) as OleMenuCommandService
;
220 if (commandService
!= null)
223 var menuCommandID
= new CommandID(GuidList
.guidClangFormatCmdSet
, (int)PkgCmdIDList
.cmdidClangFormatSelection
);
224 var menuItem
= new MenuCommand(MenuItemCallback
, menuCommandID
);
225 commandService
.AddCommand(menuItem
);
229 var menuCommandID
= new CommandID(GuidList
.guidClangFormatCmdSet
, (int)PkgCmdIDList
.cmdidClangFormatDocument
);
230 var menuItem
= new MenuCommand(MenuItemCallback
, menuCommandID
);
231 commandService
.AddCommand(menuItem
);
237 OptionPageGrid
GetUserOptions()
239 return (OptionPageGrid
)GetDialogPage(typeof(OptionPageGrid
));
242 private void MenuItemCallback(object sender
, EventArgs args
)
244 var mc
= sender
as System
.ComponentModel
.Design
.MenuCommand
;
248 switch (mc
.CommandID
.ID
)
250 case (int)PkgCmdIDList
.cmdidClangFormatSelection
:
251 FormatSelection(GetUserOptions());
254 case (int)PkgCmdIDList
.cmdidClangFormatDocument
:
255 FormatDocument(GetUserOptions());
260 private static bool FileHasExtension(string filePath
, string fileExtensions
)
262 var extensions
= fileExtensions
.ToLower().Split(new char[] { ';' }
, StringSplitOptions
.RemoveEmptyEntries
);
263 return extensions
.Contains(Path
.GetExtension(filePath
).ToLower());
266 private void OnBeforeSave(object sender
, Document document
)
268 var options
= GetUserOptions();
270 if (!options
.FormatOnSave
)
273 if (!FileHasExtension(document
.FullName
, options
.FormatOnSaveFileExtensions
))
276 if (!Vsix
.IsDocumentDirty(document
))
279 var optionsWithNoFallbackStyle
= GetUserOptions().Clone();
280 optionsWithNoFallbackStyle
.FallbackStyle
= "none";
281 FormatDocument(document
, optionsWithNoFallbackStyle
);
285 /// Runs clang-format on the current selection
287 private void FormatSelection(OptionPageGrid options
)
289 IWpfTextView view
= Vsix
.GetCurrentView();
291 // We're not in a text view.
293 string text
= view
.TextBuffer
.CurrentSnapshot
.GetText();
294 int start
= view
.Selection
.Start
.Position
.GetContainingLine().Start
.Position
;
295 int end
= view
.Selection
.End
.Position
.GetContainingLine().End
.Position
;
297 // clang-format doesn't support formatting a range that starts at the end
299 if (start
>= text
.Length
&& text
.Length
> 0)
300 start
= text
.Length
- 1;
301 string path
= Vsix
.GetDocumentParent(view
);
302 string filePath
= Vsix
.GetDocumentPath(view
);
304 RunClangFormatAndApplyReplacements(text
, start
, end
, path
, filePath
, options
, view
);
308 /// Runs clang-format on the current document
310 private void FormatDocument(OptionPageGrid options
)
312 FormatView(Vsix
.GetCurrentView(), options
);
315 private void FormatDocument(Document document
, OptionPageGrid options
)
317 FormatView(Vsix
.GetDocumentView(document
), options
);
320 private void FormatView(IWpfTextView view
, OptionPageGrid options
)
323 // We're not in a text view.
326 string filePath
= Vsix
.GetDocumentPath(view
);
327 var path
= Path
.GetDirectoryName(filePath
);
329 string text
= view
.TextBuffer
.CurrentSnapshot
.GetText();
330 if (!text
.EndsWith(Environment
.NewLine
))
332 view
.TextBuffer
.Insert(view
.TextBuffer
.CurrentSnapshot
.Length
, Environment
.NewLine
);
333 text
+= Environment
.NewLine
;
336 RunClangFormatAndApplyReplacements(text
, 0, text
.Length
, path
, filePath
, options
, view
);
339 private void RunClangFormatAndApplyReplacements(string text
, int start
, int end
, string path
, string filePath
, OptionPageGrid options
, IWpfTextView view
)
343 string replacements
= RunClangFormat(text
, start
, end
, path
, filePath
, options
);
344 ApplyClangFormatReplacements(replacements
, view
);
348 var uiShell
= (IVsUIShell
)GetService(typeof(SVsUIShell
));
351 uiShell
.ShowMessageBox(
353 "Error while running clang-format:",
356 OLEMSGBUTTON
.OLEMSGBUTTON_OK
,
357 OLEMSGDEFBUTTON
.OLEMSGDEFBUTTON_FIRST
,
358 OLEMSGICON
.OLEMSGICON_INFO
,
364 /// Runs the given text through clang-format and returns the replacements as XML.
366 /// Formats the text in range start and end.
368 private static string RunClangFormat(string text
, int start
, int end
, string path
, string filePath
, OptionPageGrid options
)
370 string vsixPath
= Path
.GetDirectoryName(
371 typeof(ClangFormatPackage
).Assembly
.Location
);
373 System
.Diagnostics
.Process process
= new System
.Diagnostics
.Process();
374 process
.StartInfo
.UseShellExecute
= false;
375 process
.StartInfo
.FileName
= vsixPath
+ "\\clang-format.exe";
376 char[] chars
= text
.ToCharArray();
377 int offset
= Encoding
.UTF8
.GetByteCount(chars
, 0, start
);
378 int length
= Encoding
.UTF8
.GetByteCount(chars
, 0, end
) - offset
;
379 // Poor man's escaping - this will not work when quotes are already escaped
380 // in the input (but we don't need more).
381 string style
= options
.Style
.Replace("\"", "\\\"");
382 string fallbackStyle
= options
.FallbackStyle
.Replace("\"", "\\\"");
383 process
.StartInfo
.Arguments
= " -offset " + offset
+
384 " -length " + length
+
385 " -output-replacements-xml " +
386 " -style \"" + style
+ "\"" +
387 " -fallback-style \"" + fallbackStyle
+ "\"";
388 if (options
.SortIncludes
)
389 process
.StartInfo
.Arguments
+= " -sort-includes ";
390 string assumeFilename
= options
.AssumeFilename
;
391 if (string.IsNullOrEmpty(assumeFilename
))
392 assumeFilename
= filePath
;
393 if (!string.IsNullOrEmpty(assumeFilename
))
394 process
.StartInfo
.Arguments
+= " -assume-filename \"" + assumeFilename
+ "\"";
395 process
.StartInfo
.CreateNoWindow
= true;
396 process
.StartInfo
.RedirectStandardInput
= true;
397 process
.StartInfo
.RedirectStandardOutput
= true;
398 process
.StartInfo
.RedirectStandardError
= true;
400 process
.StartInfo
.WorkingDirectory
= path
;
401 // We have to be careful when communicating via standard input / output,
402 // as writes to the buffers will block until they are read from the other side.
404 // 1. Start the process - clang-format.exe will start to read the input from the
413 "Cannot execute " + process
.StartInfo
.FileName
+ ".\n\"" +
414 e
.Message
+ "\".\nPlease make sure it is on the PATH.");
416 // 2. We write everything to the standard output - this cannot block, as clang-format
417 // reads the full standard input before analyzing it without writing anything to the
419 StreamWriter utf8Writer
= new StreamWriter(process
.StandardInput
.BaseStream
, new UTF8Encoding(false));
420 utf8Writer
.Write(text
);
421 // 3. We notify clang-format that the input is done - after this point clang-format
422 // will start analyzing the input and eventually write the output.
424 // 4. We must read clang-format's output before waiting for it to exit; clang-format
425 // will close the channel by exiting.
426 string output
= process
.StandardOutput
.ReadToEnd();
427 // 5. clang-format is done, wait until it is fully shut down.
428 process
.WaitForExit();
429 if (process
.ExitCode
!= 0)
431 // FIXME: If clang-format writes enough to the standard error stream to block,
432 // we will never reach this point; instead, read the standard error asynchronously.
433 throw new Exception(process
.StandardError
.ReadToEnd());
439 /// Applies the clang-format replacements (xml) to the current view
441 private static void ApplyClangFormatReplacements(string replacements
, IWpfTextView view
)
443 // clang-format returns no replacements if input text is empty
444 if (replacements
.Length
== 0)
447 string text
= view
.TextBuffer
.CurrentSnapshot
.GetText();
448 byte[] bytes
= Encoding
.UTF8
.GetBytes(text
);
450 var root
= XElement
.Parse(replacements
);
451 var edit
= view
.TextBuffer
.CreateEdit();
452 foreach (XElement replacement
in root
.Descendants("replacement"))
454 int offset
= int.Parse(replacement
.Attribute("offset").Value
);
455 int length
= int.Parse(replacement
.Attribute("length").Value
);
457 Encoding
.UTF8
.GetCharCount(bytes
, 0, offset
),
458 Encoding
.UTF8
.GetCharCount(bytes
, offset
, length
));
459 edit
.Replace(span
, replacement
.Value
);