From b4e6d0dba1973356bf668cbfd4470aa03b93c5a6 Mon Sep 17 00:00:00 2001 From: Dakota Hawkins Date: Tue, 26 Sep 2017 10:59:39 -0400 Subject: [PATCH] [WIP, ADDTESTS] Add new options for formatting relative include paths Setting PathFormat=PathMode.Absolute_FromParentDirWithFile in combination with setting FromParentDirWithFile to a filename that exists "toward" the root of your source directory tree allows us to make include paths relative to the nearest parent folder containing the specified file. This is something we do with our projects in a global property sheet. MSBuild has the awesome built-in property function "GetDirectoryNameOfFileAbove", which lets you use a known file (e.g. we have an empty "build.root" in the root of our source tree) to do the same thing as this feature. Using this functionality lets you specify things like additional include/link dirs in a common way that works everywhere so you don't have to spam a lot of relative paths in your project settings or include directives. In other words, with that set, any file can include any header by specifying its path relative to the root of our source. To "correctly" handle system includes and includes that are in the same directory as the file you're working on (that we want to leave path-less), IgnoreFileRelative was changed from a boolean setting to an enum, where Never/Always should be the same as false/true and InSameDirectory/InSameOrSubDirectory provide some more control over which directives to skip. I'm not entirely happy with the IgnoreFileRelative setting, I don't think it's very clear what it's supposed to do (in fact, I don't think I knew what it did before working on this), so perhaps this could be improved somehow in the future. --- IncludeToolbox.vsix | 4 +- IncludeToolbox/Formatter/IncludeFormatter.cs | 80 +++++++++++++++++-- .../Options/FormatterOptionsPage.cs | 24 +++++- .../Package/source.extension.vsixmanifest | 2 +- IncludeToolbox/Utils.cs | 60 +++++++++++--- 5 files changed, 145 insertions(+), 25 deletions(-) diff --git a/IncludeToolbox.vsix b/IncludeToolbox.vsix index 1a1e168..70c5917 100644 --- a/IncludeToolbox.vsix +++ b/IncludeToolbox.vsix @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50ac3952952fee7141aabb2a03e9cfd968a164a403c8d86529748c7df89d545f -size 128269 +oid sha256:0c432712fbe792d75f5dff593f0cdad3f2d72cbcd081a6e5a4b6c13c51a07ef8 +size 128533 diff --git a/IncludeToolbox/Formatter/IncludeFormatter.cs b/IncludeToolbox/Formatter/IncludeFormatter.cs index 0b5f61f..05d1042 100644 --- a/IncludeToolbox/Formatter/IncludeFormatter.cs +++ b/IncludeToolbox/Formatter/IncludeFormatter.cs @@ -8,12 +8,21 @@ namespace IncludeToolbox.Formatter { public static class IncludeFormatter { - public static string FormatPath(string absoluteIncludeFilename, FormatterOptionsPage.PathMode pathformat, IEnumerable includeDirectories) + public static string FormatPath(string absoluteIncludeFilename, + FormatterOptionsPage.PathMode pathformat, + IEnumerable 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 /// /// Formats the paths of a given list of include line info. /// - private static void FormatPaths(IEnumerable lines, FormatterOptionsPage.PathMode pathformat, IEnumerable includeDirectories) + private static void FormatPaths(IEnumerable lines, + FormatterOptionsPage.PathMode pathformat, + FormatterOptionsPage.IgnoreFileRelativeMode ignoreFileRelativeMode, + IEnumerable 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; + } + line.IncludeContent = FormatPath(absoluteIncludePath, + currentPathFormat, + includeDirectories, + currentIncludeRootDirectory) ?? line.IncludeContent; + } } } @@ -225,8 +262,32 @@ private static bool SortIncludeBatch(FormatterOptionsPage settings, string[] pre /// Formated text. public static string FormatIncludes(string text, string documentPath, IEnumerable includeDirectories, FormatterOptionsPage settings) { - string documentDir = Path.GetDirectoryName(documentPath); + string documentDir = Utils.GetExactPathName(Path.GetDirectoryName(documentPath)); string documentName = Path.GetFileNameWithoutExtension(documentPath); + string includeRootDirectory = null; + 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 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); diff --git a/IncludeToolbox/Options/FormatterOptionsPage.cs b/IncludeToolbox/Options/FormatterOptionsPage.cs index eee752b..b156182 100644 --- a/IncludeToolbox/Options/FormatterOptionsPage.cs +++ b/IncludeToolbox/Options/FormatterOptionsPage.cs @@ -19,16 +19,29 @@ public enum PathMode Shortest, Shortest_AvoidUpSteps, Absolute, + Absolute_FromParentDirWithFile } [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.")] + public string FromParentDirWithFile { get; set; } = "build.root"; + + public enum IgnoreFileRelativeMode + { + 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)); if (settingsStore.PropertyExists(collectionName, nameof(DelimiterFormatting))) DelimiterFormatting = (DelimiterMode) settingsStore.GetInt32(collectionName, nameof(DelimiterFormatting)); diff --git a/IncludeToolbox/Package/source.extension.vsixmanifest b/IncludeToolbox/Package/source.extension.vsixmanifest index 3dade45..eda7790 100644 --- a/IncludeToolbox/Package/source.extension.vsixmanifest +++ b/IncludeToolbox/Package/source.extension.vsixmanifest @@ -1,7 +1,7 @@  - + IncludeToolbox Various tools for managing C/C++ #includes: Formatting, sorting, exploring, pruning. license.txt diff --git a/IncludeToolbox/Utils.cs b/IncludeToolbox/Utils.cs index ea49990..29cab16 100644 --- a/IncludeToolbox/Utils.cs +++ b/IncludeToolbox/Utils.cs @@ -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; 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 + 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 ""; + } + + /// + /// Gets the path name as it exists it the file system, normalizing it. + /// + /// Path name + /// Normalized path name. Directories are returned with trailing separator. + 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; } ///