-
Notifications
You must be signed in to change notification settings - Fork 22
Add new options for formatting relative include paths #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,12 +8,21 @@ namespace IncludeToolbox.Formatter | |
| { | ||
| public static class IncludeFormatter | ||
| { | ||
| public static string FormatPath(string absoluteIncludeFilename, FormatterOptionsPage.PathMode pathformat, IEnumerable<string> includeDirectories) | ||
| public static string FormatPath(string absoluteIncludeFilename, | ||
| FormatterOptionsPage.PathMode pathformat, | ||
| IEnumerable<string> includeDirectories, | ||
| string includeRootDirectory = null) | ||
| { | ||
| if (pathformat == FormatterOptionsPage.PathMode.Absolute) | ||
| { | ||
| return absoluteIncludeFilename; | ||
| } | ||
| else if (pathformat == FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile && | ||
| null != absoluteIncludeFilename && | ||
| null != includeRootDirectory) | ||
| { | ||
| return Utils.MakeRelative(includeRootDirectory, absoluteIncludeFilename); | ||
| } | ||
| else | ||
| { | ||
| // todo: Treat std library files special? | ||
|
|
@@ -47,16 +56,44 @@ public static string FormatPath(string absoluteIncludeFilename, FormatterOptions | |
| /// <summary> | ||
| /// Formats the paths of a given list of include line info. | ||
| /// </summary> | ||
| private static void FormatPaths(IEnumerable<IncludeLineInfo> lines, FormatterOptionsPage.PathMode pathformat, IEnumerable<string> includeDirectories) | ||
| private static void FormatPaths(IEnumerable<IncludeLineInfo> lines, | ||
| FormatterOptionsPage.PathMode pathformat, | ||
| FormatterOptionsPage.IgnoreFileRelativeMode ignoreFileRelativeMode, | ||
| IEnumerable<string> includeDirectories, | ||
| string documentDir, | ||
| string includeRootDirectory = null) | ||
| { | ||
| if (pathformat == FormatterOptionsPage.PathMode.Unchanged) | ||
| return; | ||
|
|
||
| foreach (var line in lines) | ||
| { | ||
| string absoluteIncludeDir = line.TryResolveInclude(includeDirectories, out bool resolvedPath); | ||
| string absoluteIncludePath = line.TryResolveInclude(includeDirectories, out bool resolvedPath); | ||
| if (resolvedPath) | ||
| line.IncludeContent = FormatPath(absoluteIncludeDir, pathformat, includeDirectories) ?? line.IncludeContent; | ||
| { | ||
| if (pathformat == FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile && | ||
| includeRootDirectory != null && | ||
| !absoluteIncludePath.StartsWith(includeRootDirectory)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var currentPathFormat = pathformat; | ||
| var currentIncludeRootDirectory = includeRootDirectory; | ||
| string includeFilename = Path.GetFileName(absoluteIncludePath); | ||
| if ((ignoreFileRelativeMode == FormatterOptionsPage.IgnoreFileRelativeMode.InSameDirectory && | ||
| Path.Combine(documentDir, includeFilename) == absoluteIncludePath) || | ||
| (ignoreFileRelativeMode == FormatterOptionsPage.IgnoreFileRelativeMode.InSameOrSubDirectory && | ||
| absoluteIncludePath.StartsWith(documentDir))) | ||
| { | ||
| currentPathFormat = FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile; | ||
| currentIncludeRootDirectory = documentDir; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not very happy with this. The combination of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll give that a try. I worry a bit that some options will be at cross-purposes with one another in some configurations (like "why would you do that?" configurations). If it makes things less awkward, though, I'm all for it! |
||
| } | ||
| line.IncludeContent = FormatPath(absoluteIncludePath, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Old version was guarded against FormatPath returning null. There might have been a good reason for that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It still is, just a few lines down because of how I formatted the parameters. (I dislike commenting on code in GitHub because I find inline comments hard to place well and hard to read -- I'd rather them show up in some kind of side bar so they don't split up the actual code!) |
||
| currentPathFormat, | ||
| includeDirectories, | ||
| currentIncludeRootDirectory) ?? line.IncludeContent; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -225,8 +262,32 @@ private static bool SortIncludeBatch(FormatterOptionsPage settings, string[] pre | |
| /// <returns>Formated text.</returns> | ||
| public static string FormatIncludes(string text, string documentPath, IEnumerable<string> includeDirectories, FormatterOptionsPage settings) | ||
| { | ||
| string documentDir = Path.GetDirectoryName(documentPath); | ||
| string documentDir = Utils.GetExactPathName(Path.GetDirectoryName(documentPath)); | ||
| string documentName = Path.GetFileNameWithoutExtension(documentPath); | ||
| string includeRootDirectory = null; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The following loops up through parent directories looking for the file specified in the settings. |
||
| if (settings.PathFormat == FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile && | ||
| !String.IsNullOrWhiteSpace(settings.FromParentDirWithFile)) | ||
| { | ||
| var dir = new DirectoryInfo(documentDir); | ||
| while (dir != null) | ||
| { | ||
| if (File.Exists(Path.Combine(dir.FullName, settings.FromParentDirWithFile))) | ||
| { | ||
| includeRootDirectory = Utils.GetExactPathName(dir.FullName); | ||
| break; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| dir = dir.Parent; | ||
| } | ||
| catch (System.Security.SecurityException) | ||
| { | ||
| // Permission denied | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| includeDirectories = new string[] { Microsoft.VisualStudio.PlatformUI.PathUtil.Normalize(documentDir) + Path.DirectorySeparatorChar }.Concat(includeDirectories); | ||
|
|
||
|
|
@@ -236,11 +297,16 @@ public static string FormatIncludes(string text, string documentPath, IEnumerabl | |
|
|
||
| // Format. | ||
| IEnumerable<string> formatingDirs = includeDirectories; | ||
| if (settings.IgnoreFileRelative) | ||
| if (settings.IgnoreFileRelative == FormatterOptionsPage.IgnoreFileRelativeMode.Always) | ||
| { | ||
| formatingDirs = formatingDirs.Skip(1); | ||
| } | ||
| FormatPaths(lines, settings.PathFormat, formatingDirs); | ||
| FormatPaths(lines, | ||
| settings.PathFormat, | ||
| settings.IgnoreFileRelative, | ||
| formatingDirs, | ||
| documentDir, | ||
| includeRootDirectory); | ||
| FormatDelimiters(lines, settings.DelimiterFormatting); | ||
| FormatSlashes(lines, settings.SlashFormatting); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,16 +19,29 @@ public enum PathMode | |
| Shortest, | ||
| Shortest_AvoidUpSteps, | ||
| Absolute, | ||
| Absolute_FromParentDirWithFile | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This not actually absolute in any sense. It is relative from where the special file is placed, so I'd call it Please correct me if I got this wrong :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, you're right. I think your name will make more sense. In our workflow I just tend to think about them as "absolute with respect to the code base" vs. "relative to the current file". |
||
| } | ||
| [Category("Path")] | ||
| [DisplayName("Mode")] | ||
| [Description("Changes the path mode to the given pattern.")] | ||
| public PathMode PathFormat { get; set; } = PathMode.Shortest_AvoidUpSteps; | ||
|
|
||
| [Category("Path")] | ||
| [DisplayName("Filename in parent directory for absolute include paths")] | ||
| [Description("The Absolute_FromParentDirWithFile mode will look for this file in parent directories and make include paths absolute from its location.")] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This functionality is somewhat difficult to describe... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I never heard of this whole thing and it took me a while indeed. I think your description here is quite good after all though. Suggestion for a slight improvement: |
||
| public string FromParentDirWithFile { get; set; } = "build.root"; | ||
|
|
||
| public enum IgnoreFileRelativeMode | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Originally this was intended to suppress the "natural include path" even if that one would be better according to the policy in PathMode. enum AllowRelativePathMode There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be a better idea to do something different. What do you think about this: Remove Always/Never
Basically I'm envisioning these two being like this: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you're definitely right on the resolve strategy! Didn't know about "2" for '"' delimiters. Taking this into account would be hard ... I also agree on most of the rest as well but, as you're pointing out neither If I got everything right that would mean that your new desired mode is configured as About the delimiter thing: Having auto mode sounds great! Just don't rely on any semantic for existing/targeted delimiters, I worked in projects where all delimiters were angle brackets by convention. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With respect to I've already seen a couple of cases in our codebase where files that are in the current directory had longer relative paths that my current implementation fixed appropriately. As an example, consider:
Group regexes: Original: // MainProj.cpp
// AdditionalIncludeDirs="../ProjA;../ProjB;$(BuildRootDirMacro);..."
#include "stdafx.h"
#include "A_2.h"
#include "B_1.h"
#include "B_2.h"
#include "MainProj.h"
#include "../Src/ProjA/A_1.h"
#include "../MainProj/MainUtility.h"
#include <vector>
#include <windows.h>
#include <string>Formatted: // MainProj.cpp
// AdditionalIncludeDirs="../ProjA;../ProjB;$(BuildRootDirMacro);..."
#include "stdafx.h"
#include "MainProj.h"
#include <windows.h>
#include <vector>
#include <string>
#include "Src/ProjA/A_1.h"
#include "Src/ProjA/A_2.h"
#include "Src/ProjB/B_1.h"
#include "Src/ProjB/B_2.h"
#include "MainUtility.h"If you're curious, that grouping/order comes from the Google C++ Style Guide, which is what our style guide is based on. I have a few reasons for wanting those long, rooted paths:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In terms of "paths going outside of the source dir", I'll admit that's not a case that affects me personally so I wasn't terribly concerned about it, but I think failing to fit other strategies and falling back on one of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really think the wiki page would be good to sort this out, since my intention was to take a step back from the implementation and address what kind of general behavior we'd like to support. Also, with so many options that affect one another, the possible behaviors really scale. It might be OK if there are some combinations of them that don't make sense and produce bad/silly results as long as those combinations aren't easy to accidentally have and the "good" combinations allow lots of kinds of results that people might actually want or need. |
||
| { | ||
| Never, | ||
| Always, | ||
| InSameDirectory, | ||
| InSameOrSubDirectory | ||
| } | ||
| [Category("Path")] | ||
| [DisplayName("Ignore File Relative")] | ||
| [Description("If true, include directives will not take the path of the file into account.")] | ||
| public bool IgnoreFileRelative { get; set; } = false; | ||
| [Description("Whether to ignore include paths relative to the current file.")] | ||
| public IgnoreFileRelativeMode IgnoreFileRelative { get; set; } = IgnoreFileRelativeMode.Never; | ||
|
|
||
| //[Category("Path")] | ||
| //[DisplayName("Ignore Standard Library")] | ||
|
|
@@ -115,7 +128,8 @@ public override void SaveSettingsToStorage() | |
| settingsStore.CreateCollection(collectionName); | ||
|
|
||
| settingsStore.SetInt32(collectionName, nameof(PathFormat), (int)PathFormat); | ||
| settingsStore.SetBoolean(collectionName, nameof(IgnoreFileRelative), IgnoreFileRelative); | ||
| settingsStore.SetString(collectionName, nameof(FromParentDirWithFile), FromParentDirWithFile); | ||
| settingsStore.SetInt32(collectionName, nameof(IgnoreFileRelative), (int)IgnoreFileRelative); | ||
|
|
||
| settingsStore.SetInt32(collectionName, nameof(DelimiterFormatting), (int)DelimiterFormatting); | ||
| settingsStore.SetInt32(collectionName, nameof(SlashFormatting), (int)SlashFormatting); | ||
|
|
@@ -135,8 +149,10 @@ public override void LoadSettingsFromStorage() | |
|
|
||
| if (settingsStore.PropertyExists(collectionName, nameof(PathFormat))) | ||
| PathFormat = (PathMode)settingsStore.GetInt32(collectionName, nameof(PathFormat)); | ||
| if (settingsStore.PropertyExists(collectionName, nameof(FromParentDirWithFile))) | ||
| FromParentDirWithFile = settingsStore.GetString(collectionName, nameof(FromParentDirWithFile)); | ||
| if (settingsStore.PropertyExists(collectionName, nameof(IgnoreFileRelative))) | ||
| IgnoreFileRelative = settingsStore.GetBoolean(collectionName, nameof(IgnoreFileRelative)); | ||
| IgnoreFileRelative = (IgnoreFileRelativeMode)settingsStore.GetInt32(collectionName, nameof(IgnoreFileRelative)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't checked - what happens if the bool was already saved and were loading an integer now with the same identifier? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That just happened to me accidentally. The field is blank :( I guess I could add conversion code, but hacking that in seems like a slippery slope in the event there are more changes like that in the future. What do you think the right thing to do is? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it only doesn't work going backwards in version. Going forwards I think it works because the first two enum values correspond to the original boolean values. Of course, that will change if we flip the meaning of the setting. Also changing the name of the setting, however, should just give you default values. So, not ideal for users that had non-defaults before but maybe not too terrible either. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah let's not over-think this (too). Let's go with a different name, then we're on the safe side. |
||
|
|
||
| if (settingsStore.PropertyExists(collectionName, nameof(DelimiterFormatting))) | ||
| DelimiterFormatting = (DelimiterMode) settingsStore.GetInt32(collectionName, nameof(DelimiterFormatting)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |
| using Microsoft.VisualStudio.Shell; | ||
| using Microsoft.VisualStudio.Text.Editor; | ||
| using Microsoft.VisualStudio.TextManager.Interop; | ||
| using Microsoft.VisualStudio.VCProjectEngine; | ||
| //using Microsoft.VisualStudio.VCProjectEngine; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this just left over from an old version? VS2017 complains about it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm odd. With the newest VS2017 version I'm hitting this as well. Sounds like I should make sure stuff works still in VS2015 before I upload to the marketplace. |
||
|
|
||
| namespace IncludeToolbox | ||
| { | ||
|
|
@@ -35,23 +35,61 @@ public static string MakeRelative(string absoluteRoot, string absoluteTarget) | |
| return relativePath; | ||
| } | ||
|
|
||
| public static string GetExactPathName(string pathName) | ||
| // Shamelessly stolen from https://stackoverflow.com/a/5076517/153079 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is similar to the previous functionality, but should be more reliable since each part of the path is normalized with GetFileSystemInfos. It's important to get this right, because eventually we rely on string comparisons to determine whether a file is in/under a directory path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting. I'm not sure I could really follow the discussion on that SO page. Can you give me an example where GetExactPathName gives a different result than GetFileSystemCasing for absolute paths? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly, I didn't hit a problem-case, but I became very concerned about it because I was adding code that uses naked string operations to compare two different paths. However, there aren't really two functions now, Ignoring the possibility that the old There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading back over the SO comments, I think the old version probably did work. The only thing I'm not 100% sure about is using DI |
||
| private static string GetFileSystemCasing(string path) | ||
| { | ||
| if (!File.Exists(pathName) && !Directory.Exists(pathName)) | ||
| return pathName; | ||
| if (Path.IsPathRooted(path)) | ||
| { | ||
| path = path.TrimEnd(Path.DirectorySeparatorChar); // if you type c:\foo\ instead of c:\foo | ||
| try | ||
| { | ||
| string name = Path.GetFileName(path); | ||
| if (name == "") | ||
| return path.ToUpper() + Path.DirectorySeparatorChar; // root reached | ||
|
|
||
| var di = new DirectoryInfo(pathName); | ||
| string parent = Path.GetDirectoryName(path); | ||
|
|
||
| if (di.Parent != null) | ||
| { | ||
| return Path.Combine( | ||
| GetExactPathName(di.Parent.FullName), | ||
| di.Parent.GetFileSystemInfos(di.Name)[0].Name); | ||
| parent = GetFileSystemCasing(parent); | ||
|
|
||
| DirectoryInfo diParent = new DirectoryInfo(parent); | ||
| FileSystemInfo[] fsiChildren = diParent.GetFileSystemInfos(name); | ||
| FileSystemInfo fsiChild = fsiChildren.First(); | ||
| return fsiChild.FullName; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Output.Instance.ErrorMsg("Invalid path: '{0}'\nError:\n{1}", path, ex.Message); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| return di.Name.ToUpper(); | ||
| Output.Instance.ErrorMsg("Absolute path needed, not relative: '{0}'", path); | ||
| } | ||
|
|
||
| return ""; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the path name as it exists it the file system, normalizing it. | ||
| /// </summary> | ||
| /// <param name="pathName">Path name</param> | ||
| /// <returns>Normalized path name. Directories are returned with trailing separator.</returns> | ||
| public static string GetExactPathName(string pathName) | ||
| { | ||
| if (!File.Exists(pathName) && !Directory.Exists(pathName)) | ||
| { | ||
| return pathName; | ||
| } | ||
|
|
||
| string exactPathName = GetFileSystemCasing(Path.GetFullPath(pathName).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)); | ||
| if (exactPathName == "") | ||
| return pathName; | ||
|
|
||
| // Add trailing slash for directories | ||
| exactPathName = exactPathName.TrimEnd(Path.DirectorySeparatorChar); | ||
| if (File.GetAttributes(exactPathName).HasFlag(FileAttributes.Directory)) | ||
| return exactPathName + Path.DirectorySeparatorChar; | ||
| return exactPathName; | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoids files outside of the root directory tree (e.g. system includes)