1 | //===-- ClangFormatPackages.cs - VSPackage for clang-format ------*- C# -*-===// |
2 | // |
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 |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | // |
9 | // This class contains a VS extension package that runs clang-format over a |
10 | // selection in a VS text editor. |
11 | // |
12 | //===----------------------------------------------------------------------===// |
13 | |
14 | using EnvDTE; |
15 | using Microsoft.VisualStudio.Shell; |
16 | using Microsoft.VisualStudio.Shell.Interop; |
17 | using Microsoft.VisualStudio.Text; |
18 | using Microsoft.VisualStudio.Text.Editor; |
19 | using System; |
20 | using System.Collections; |
21 | using System.ComponentModel; |
22 | using System.ComponentModel.Design; |
23 | using System.IO; |
24 | using System.Runtime.InteropServices; |
25 | using System.Xml.Linq; |
26 | using System.Linq; |
27 | |
28 | namespace LLVM.ClangFormat |
29 | { |
30 | [ClassInterface(ClassInterfaceType.AutoDual)] |
31 | [CLSCompliant(false), ComVisible(true)] |
32 | public class OptionPageGrid : DialogPage |
33 | { |
34 | private string assumeFilename = ""; |
35 | private string fallbackStyle = "LLVM"; |
36 | private bool sortIncludes = false; |
37 | private string style = "file"; |
38 | private bool formatOnSave = false; |
39 | private string formatOnSaveFileExtensions = |
40 | ".c;.cpp;.cxx;.cc;.tli;.tlh;.h;.hh;.hpp;.hxx;.hh;.inl;" + |
41 | ".java;.js;.ts;.m;.mm;.proto;.protodevel;.td"; |
42 | |
43 | public OptionPageGrid Clone() |
44 | { |
45 | // Use MemberwiseClone to copy value types. |
46 | var clone = (OptionPageGrid)MemberwiseClone(); |
47 | return clone; |
48 | } |
49 | |
50 | public class StyleConverter : TypeConverter |
51 | { |
52 | protected ArrayList values; |
53 | public StyleConverter() |
54 | { |
55 | // Initializes the standard values list with defaults. |
56 | values = new ArrayList(new string[] { "file", "Chromium", "Google", "LLVM", "Mozilla", "WebKit" }); |
57 | } |
58 | |
59 | public override bool GetStandardValuesSupported(ITypeDescriptorContext context) |
60 | { |
61 | return true; |
62 | } |
63 | |
64 | public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) |
65 | { |
66 | return new StandardValuesCollection(values); |
67 | } |
68 | |
69 | public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
70 | { |
71 | if (sourceType == typeof(string)) |
72 | return true; |
73 | |
74 | return base.CanConvertFrom(context, sourceType); |
75 | } |
76 | |
77 | public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) |
78 | { |
79 | string s = value as string; |
80 | if (s == null) |
81 | return base.ConvertFrom(context, culture, value); |
82 | |
83 | return value; |
84 | } |
85 | } |
86 | |
87 | [Category("Format Options")] |
88 | [DisplayName("Style")] |
89 | [Description("Coding style, currently supports:\n" + |
90 | " - Predefined styles ('LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit').\n" + |
91 | " - 'file' to search for a YAML .clang-format or _clang-format\n" + |
92 | " configuration file.\n" + |
93 | " - A YAML configuration snippet.\n\n" + |
94 | "'File':\n" + |
95 | " Searches for a .clang-format or _clang-format configuration file\n" + |
96 | " in the source file's directory and its parents.\n\n" + |
97 | "YAML configuration snippet:\n" + |
98 | " The content of a .clang-format configuration file, as string.\n" + |
99 | " Example: '{BasedOnStyle: \"LLVM\", IndentWidth: 8}'\n\n" + |
100 | "See also: http://clang.llvm.org/docs/ClangFormatStyleOptions.html.")] |
101 | [TypeConverter(typeof(StyleConverter))] |
102 | public string Style |
103 | { |
104 | get { return style; } |
105 | set { style = value; } |
106 | } |
107 | |
108 | public sealed class FilenameConverter : TypeConverter |
109 | { |
110 | public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
111 | { |
112 | if (sourceType == typeof(string)) |
113 | return true; |
114 | |
115 | return base.CanConvertFrom(context, sourceType); |
116 | } |
117 | |
118 | public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) |
119 | { |
120 | string s = value as string; |
121 | if (s == null) |
122 | return base.ConvertFrom(context, culture, value); |
123 | |
124 | // Check if string contains quotes. On Windows, file names cannot contain quotes. |
125 | // We do not accept them however to avoid hard-to-debug problems. |
126 | // A quote in user input would end the parameter quote and so break the command invocation. |
127 | if (s.IndexOf('\"') != -1) |
128 | throw new NotSupportedException("Filename cannot contain quotes"); |
129 | |
130 | return value; |
131 | } |
132 | } |
133 | |
134 | [Category("Format Options")] |
135 | [DisplayName("Assume Filename")] |
136 | [Description("When reading from stdin, clang-format assumes this " + |
137 | "filename to look for a style config file (with 'file' style) " + |
138 | "and to determine the language.")] |
139 | [TypeConverter(typeof(FilenameConverter))] |
140 | public string AssumeFilename |
141 | { |
142 | get { return assumeFilename; } |
143 | set { assumeFilename = value; } |
144 | } |
145 | |
146 | public sealed class FallbackStyleConverter : StyleConverter |
147 | { |
148 | public FallbackStyleConverter() |
149 | { |
150 | // Add "none" to the list of styles. |
151 | values.Insert(0, "none"); |
152 | } |
153 | } |
154 | |
155 | [Category("Format Options")] |
156 | [DisplayName("Fallback Style")] |
157 | [Description("The name of the predefined style used as a fallback in case clang-format " + |
158 | "is invoked with 'file' style, but can not find the configuration file.\n" + |
159 | "Use 'none' fallback style to skip formatting.")] |
160 | [TypeConverter(typeof(FallbackStyleConverter))] |
161 | public string FallbackStyle |
162 | { |
163 | get { return fallbackStyle; } |
164 | set { fallbackStyle = value; } |
165 | } |
166 | |
167 | [Category("Format Options")] |
168 | [DisplayName("Sort includes")] |
169 | [Description("Sort touched include lines.\n\n" + |
170 | "See also: http://clang.llvm.org/docs/ClangFormat.html.")] |
171 | public bool SortIncludes |
172 | { |
173 | get { return sortIncludes; } |
174 | set { sortIncludes = value; } |
175 | } |
176 | |
177 | [Category("Format On Save")] |
178 | [DisplayName("Enable")] |
179 | [Description("Enable running clang-format when modified files are saved. " + |
180 | "Will only format if Style is found (ignores Fallback Style)." |
181 | )] |
182 | public bool FormatOnSave |
183 | { |
184 | get { return formatOnSave; } |
185 | set { formatOnSave = value; } |
186 | } |
187 | |
188 | [Category("Format On Save")] |
189 | [DisplayName("File extensions")] |
190 | [Description("When formatting on save, clang-format will be applied only to " + |
191 | "files with these extensions.")] |
192 | public string FormatOnSaveFileExtensions |
193 | { |
194 | get { return formatOnSaveFileExtensions; } |
195 | set { formatOnSaveFileExtensions = value; } |
196 | } |
197 | } |
198 | |
199 | [PackageRegistration(UseManagedResourcesOnly = true)] |
200 | [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] |
201 | [ProvideMenuResource("Menus.ctmenu", 1)] |
202 | [ProvideAutoLoad(UIContextGuids80.SolutionExists)] // Load package on solution load |
203 | [Guid(GuidList.guidClangFormatPkgString)] |
204 | [ProvideOptionPage(typeof(OptionPageGrid), "LLVM/Clang", "ClangFormat", 0, 0, true)] |
205 | public sealed class ClangFormatPackage : Package |
206 | { |
207 | #region Package Members |
208 | |
209 | RunningDocTableEventsDispatcher _runningDocTableEventsDispatcher; |
210 | |
211 | protected override void Initialize() |
212 | { |
213 | base.Initialize(); |
214 | |
215 | _runningDocTableEventsDispatcher = new RunningDocTableEventsDispatcher(this); |
216 | _runningDocTableEventsDispatcher.BeforeSave += OnBeforeSave; |
217 | |
218 | var commandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService; |
219 | if (commandService != null) |
220 | { |
221 | { |
222 | var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatSelection); |
223 | var menuItem = new MenuCommand(MenuItemCallback, menuCommandID); |
224 | commandService.AddCommand(menuItem); |
225 | } |
226 | |
227 | { |
228 | var menuCommandID = new CommandID(GuidList.guidClangFormatCmdSet, (int)PkgCmdIDList.cmdidClangFormatDocument); |
229 | var menuItem = new MenuCommand(MenuItemCallback, menuCommandID); |
230 | commandService.AddCommand(menuItem); |
231 | } |
232 | } |
233 | } |
234 | #endregion |
235 | |
236 | OptionPageGrid GetUserOptions() |
237 | { |
238 | return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); |
239 | } |
240 | |
241 | private void MenuItemCallback(object sender, EventArgs args) |
242 | { |
243 | var mc = sender as System.ComponentModel.Design.MenuCommand; |
244 | if (mc == null) |
245 | return; |
246 | |
247 | switch (mc.CommandID.ID) |
248 | { |
249 | case (int)PkgCmdIDList.cmdidClangFormatSelection: |
250 | FormatSelection(GetUserOptions()); |
251 | break; |
252 | |
253 | case (int)PkgCmdIDList.cmdidClangFormatDocument: |
254 | FormatDocument(GetUserOptions()); |
255 | break; |
256 | } |
257 | } |
258 | |
259 | private static bool FileHasExtension(string filePath, string fileExtensions) |
260 | { |
261 | var extensions = fileExtensions.ToLower().Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); |
262 | return extensions.Contains(Path.GetExtension(filePath).ToLower()); |
263 | } |
264 | |
265 | private void OnBeforeSave(object sender, Document document) |
266 | { |
267 | var options = GetUserOptions(); |
268 | |
269 | if (!options.FormatOnSave) |
270 | return; |
271 | |
272 | if (!FileHasExtension(document.FullName, options.FormatOnSaveFileExtensions)) |
273 | return; |
274 | |
275 | if (!Vsix.IsDocumentDirty(document)) |
276 | return; |
277 | |
278 | var optionsWithNoFallbackStyle = GetUserOptions().Clone(); |
279 | optionsWithNoFallbackStyle.FallbackStyle = "none"; |
280 | FormatDocument(document, optionsWithNoFallbackStyle); |
281 | } |
282 | |
283 | /// <summary> |
284 | /// Runs clang-format on the current selection |
285 | /// </summary> |
286 | private void FormatSelection(OptionPageGrid options) |
287 | { |
288 | IWpfTextView view = Vsix.GetCurrentView(); |
289 | if (view == null) |
290 | // We're not in a text view. |
291 | return; |
292 | string text = view.TextBuffer.CurrentSnapshot.GetText(); |
293 | int start = view.Selection.Start.Position.GetContainingLine().Start.Position; |
294 | int end = view.Selection.End.Position.GetContainingLine().End.Position; |
295 | int length = end - start; |
296 | |
297 | // clang-format doesn't support formatting a range that starts at the end |
298 | // of the file. |
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); |
303 | |
304 | RunClangFormatAndApplyReplacements(text, start, length, path, filePath, options, view); |
305 | } |
306 | |
307 | /// <summary> |
308 | /// Runs clang-format on the current document |
309 | /// </summary> |
310 | private void FormatDocument(OptionPageGrid options) |
311 | { |
312 | FormatView(Vsix.GetCurrentView(), options); |
313 | } |
314 | |
315 | private void FormatDocument(Document document, OptionPageGrid options) |
316 | { |
317 | FormatView(Vsix.GetDocumentView(document), options); |
318 | } |
319 | |
320 | private void FormatView(IWpfTextView view, OptionPageGrid options) |
321 | { |
322 | if (view == null) |
323 | // We're not in a text view. |
324 | return; |
325 | |
326 | string filePath = Vsix.GetDocumentPath(view); |
327 | var path = Path.GetDirectoryName(filePath); |
328 | |
329 | string text = view.TextBuffer.CurrentSnapshot.GetText(); |
330 | if (!text.EndsWith(Environment.NewLine)) |
331 | { |
332 | view.TextBuffer.Insert(view.TextBuffer.CurrentSnapshot.Length, Environment.NewLine); |
333 | text += Environment.NewLine; |
334 | } |
335 | |
336 | RunClangFormatAndApplyReplacements(text, 0, text.Length, path, filePath, options, view); |
337 | } |
338 | |
339 | private void RunClangFormatAndApplyReplacements(string text, int offset, int length, string path, string filePath, OptionPageGrid options, IWpfTextView view) |
340 | { |
341 | try |
342 | { |
343 | string replacements = RunClangFormat(text, offset, length, path, filePath, options); |
344 | ApplyClangFormatReplacements(replacements, view); |
345 | } |
346 | catch (Exception e) |
347 | { |
348 | var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); |
349 | var id = Guid.Empty; |
350 | int result; |
351 | uiShell.ShowMessageBox( |
352 | 0, ref id, |
353 | "Error while running clang-format:", |
354 | e.Message, |
355 | string.Empty, 0, |
356 | OLEMSGBUTTON.OLEMSGBUTTON_OK, |
357 | OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, |
358 | OLEMSGICON.OLEMSGICON_INFO, |
359 | 0, out result); |
360 | } |
361 | } |
362 | |
363 | /// <summary> |
364 | /// Runs the given text through clang-format and returns the replacements as XML. |
365 | /// |
366 | /// Formats the text range starting at offset of the given length. |
367 | /// </summary> |
368 | private static string RunClangFormat(string text, int offset, int length, string path, string filePath, OptionPageGrid options) |
369 | { |
370 | string vsixPath = Path.GetDirectoryName( |
371 | typeof(ClangFormatPackage).Assembly.Location); |
372 | |
373 | System.Diagnostics.Process process = new System.Diagnostics.Process(); |
374 | process.StartInfo.UseShellExecute = false; |
375 | process.StartInfo.FileName = vsixPath + "\\clang-format.exe"; |
376 | // Poor man's escaping - this will not work when quotes are already escaped |
377 | // in the input (but we don't need more). |
378 | string style = options.Style.Replace("\"", "\\\""); |
379 | string fallbackStyle = options.FallbackStyle.Replace("\"", "\\\""); |
380 | process.StartInfo.Arguments = " -offset " + offset + |
381 | " -length " + length + |
382 | " -output-replacements-xml " + |
383 | " -style \"" + style + "\"" + |
384 | " -fallback-style \"" + fallbackStyle + "\""; |
385 | if (options.SortIncludes) |
386 | process.StartInfo.Arguments += " -sort-includes "; |
387 | string assumeFilename = options.AssumeFilename; |
388 | if (string.IsNullOrEmpty(assumeFilename)) |
389 | assumeFilename = filePath; |
390 | if (!string.IsNullOrEmpty(assumeFilename)) |
391 | process.StartInfo.Arguments += " -assume-filename \"" + assumeFilename + "\""; |
392 | process.StartInfo.CreateNoWindow = true; |
393 | process.StartInfo.RedirectStandardInput = true; |
394 | process.StartInfo.RedirectStandardOutput = true; |
395 | process.StartInfo.RedirectStandardError = true; |
396 | if (path != null) |
397 | process.StartInfo.WorkingDirectory = path; |
398 | // We have to be careful when communicating via standard input / output, |
399 | // as writes to the buffers will block until they are read from the other side. |
400 | // Thus, we: |
401 | // 1. Start the process - clang-format.exe will start to read the input from the |
402 | // standard input. |
403 | try |
404 | { |
405 | process.Start(); |
406 | } |
407 | catch (Exception e) |
408 | { |
409 | throw new Exception( |
410 | "Cannot execute " + process.StartInfo.FileName + ".\n\"" + |
411 | e.Message + "\".\nPlease make sure it is on the PATH."); |
412 | } |
413 | // 2. We write everything to the standard output - this cannot block, as clang-format |
414 | // reads the full standard input before analyzing it without writing anything to the |
415 | // standard output. |
416 | process.StandardInput.Write(text); |
417 | // 3. We notify clang-format that the input is done - after this point clang-format |
418 | // will start analyzing the input and eventually write the output. |
419 | process.StandardInput.Close(); |
420 | // 4. We must read clang-format's output before waiting for it to exit; clang-format |
421 | // will close the channel by exiting. |
422 | string output = process.StandardOutput.ReadToEnd(); |
423 | // 5. clang-format is done, wait until it is fully shut down. |
424 | process.WaitForExit(); |
425 | if (process.ExitCode != 0) |
426 | { |
427 | // FIXME: If clang-format writes enough to the standard error stream to block, |
428 | // we will never reach this point; instead, read the standard error asynchronously. |
429 | throw new Exception(process.StandardError.ReadToEnd()); |
430 | } |
431 | return output; |
432 | } |
433 | |
434 | /// <summary> |
435 | /// Applies the clang-format replacements (xml) to the current view |
436 | /// </summary> |
437 | private static void ApplyClangFormatReplacements(string replacements, IWpfTextView view) |
438 | { |
439 | // clang-format returns no replacements if input text is empty |
440 | if (replacements.Length == 0) |
441 | return; |
442 | |
443 | var root = XElement.Parse(replacements); |
444 | var edit = view.TextBuffer.CreateEdit(); |
445 | foreach (XElement replacement in root.Descendants("replacement")) |
446 | { |
447 | var span = new Span( |
448 | int.Parse(replacement.Attribute("offset").Value), |
449 | int.Parse(replacement.Attribute("length").Value)); |
450 | edit.Replace(span, replacement.Value); |
451 | } |
452 | edit.Apply(); |
453 | } |
454 | } |
455 | } |
456 | |