Skip to content

Commit 2390405

Browse files
committed
[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.
1 parent 2e88080 commit 2390405

File tree

5 files changed

+145
-25
lines changed

5 files changed

+145
-25
lines changed

IncludeToolbox.vsix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:96da2600a661f022024c446771873504e6aec1d77a25c0f0268adbe1e7dcb784
3-
size 127921
2+
oid sha256:7b7976b085518b85f089f237f57d404023c3fc72b884e73b9501a68e4f9484f0
3+
size 128672

IncludeToolbox/Formatter/IncludeFormatter.cs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ namespace IncludeToolbox.Formatter
88
{
99
public static class IncludeFormatter
1010
{
11-
public static string FormatPath(string absoluteIncludeFilename, FormatterOptionsPage.PathMode pathformat, IEnumerable<string> includeDirectories)
11+
public static string FormatPath(string absoluteIncludeFilename,
12+
FormatterOptionsPage.PathMode pathformat,
13+
IEnumerable<string> includeDirectories,
14+
string includeRootDirectory = null)
1215
{
1316
if (pathformat == FormatterOptionsPage.PathMode.Absolute)
1417
{
1518
return absoluteIncludeFilename;
1619
}
20+
else if (pathformat == FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile &&
21+
null != absoluteIncludeFilename &&
22+
null != includeRootDirectory)
23+
{
24+
return Utils.MakeRelative(includeRootDirectory, absoluteIncludeFilename);
25+
}
1726
else
1827
{
1928
// todo: Treat std library files special?
@@ -47,16 +56,44 @@ public static string FormatPath(string absoluteIncludeFilename, FormatterOptions
4756
/// <summary>
4857
/// Formats the paths of a given list of include line info.
4958
/// </summary>
50-
private static void FormatPaths(IEnumerable<IncludeLineInfo> lines, FormatterOptionsPage.PathMode pathformat, IEnumerable<string> includeDirectories)
59+
private static void FormatPaths(IEnumerable<IncludeLineInfo> lines,
60+
FormatterOptionsPage.PathMode pathformat,
61+
FormatterOptionsPage.IgnoreFileRelativeMode ignoreFileRelativeMode,
62+
IEnumerable<string> includeDirectories,
63+
string documentDir,
64+
string includeRootDirectory = null)
5165
{
5266
if (pathformat == FormatterOptionsPage.PathMode.Unchanged)
5367
return;
5468

5569
foreach (var line in lines)
5670
{
57-
string absoluteIncludeDir = line.TryResolveInclude(includeDirectories, out bool resolvedPath);
71+
string absoluteIncludePath = line.TryResolveInclude(includeDirectories, out bool resolvedPath);
5872
if (resolvedPath)
59-
line.IncludeContent = FormatPath(absoluteIncludeDir, pathformat, includeDirectories) ?? line.IncludeContent;
73+
{
74+
if (pathformat == FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile &&
75+
includeRootDirectory != null &&
76+
!absoluteIncludePath.StartsWith(includeRootDirectory))
77+
{
78+
continue;
79+
}
80+
81+
var currentPathFormat = pathformat;
82+
var currentIncludeRootDirectory = includeRootDirectory;
83+
string includeFilename = Path.GetFileName(absoluteIncludePath);
84+
if ((ignoreFileRelativeMode == FormatterOptionsPage.IgnoreFileRelativeMode.InSameDirectory &&
85+
Path.Combine(documentDir, includeFilename) == absoluteIncludePath) ||
86+
(ignoreFileRelativeMode == FormatterOptionsPage.IgnoreFileRelativeMode.InSameOrSubDirectory &&
87+
absoluteIncludePath.StartsWith(documentDir)))
88+
{
89+
currentPathFormat = FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile;
90+
currentIncludeRootDirectory = documentDir;
91+
}
92+
line.IncludeContent = FormatPath(absoluteIncludePath,
93+
currentPathFormat,
94+
includeDirectories,
95+
currentIncludeRootDirectory) ?? line.IncludeContent;
96+
}
6097
}
6198
}
6299

@@ -213,8 +250,32 @@ private static bool SortIncludeBatch(FormatterOptionsPage settings, string[] pre
213250
/// <returns>Formated text.</returns>
214251
public static string FormatIncludes(string text, string documentPath, IEnumerable<string> includeDirectories, FormatterOptionsPage settings)
215252
{
216-
string documentDir = Path.GetDirectoryName(documentPath);
253+
string documentDir = Utils.GetExactPathName(Path.GetDirectoryName(documentPath));
217254
string documentName = Path.GetFileNameWithoutExtension(documentPath);
255+
string includeRootDirectory = null;
256+
if (settings.PathFormat == FormatterOptionsPage.PathMode.Absolute_FromParentDirWithFile &&
257+
!String.IsNullOrWhiteSpace(settings.FromParentDirWithFile))
258+
{
259+
var dir = new DirectoryInfo(documentDir);
260+
while (dir != null)
261+
{
262+
if (File.Exists(Path.Combine(dir.FullName, settings.FromParentDirWithFile)))
263+
{
264+
includeRootDirectory = Utils.GetExactPathName(dir.FullName);
265+
break;
266+
}
267+
268+
try
269+
{
270+
dir = dir.Parent;
271+
}
272+
catch (System.Security.SecurityException)
273+
{
274+
// Permission denied
275+
break;
276+
}
277+
}
278+
}
218279

219280
includeDirectories = new string[] { Microsoft.VisualStudio.PlatformUI.PathUtil.Normalize(documentDir) + Path.DirectorySeparatorChar }.Concat(includeDirectories);
220281

@@ -224,11 +285,16 @@ public static string FormatIncludes(string text, string documentPath, IEnumerabl
224285

225286
// Format.
226287
IEnumerable<string> formatingDirs = includeDirectories;
227-
if (settings.IgnoreFileRelative)
288+
if (settings.IgnoreFileRelative == FormatterOptionsPage.IgnoreFileRelativeMode.Always)
228289
{
229290
formatingDirs = formatingDirs.Skip(1);
230291
}
231-
FormatPaths(lines, settings.PathFormat, formatingDirs);
292+
FormatPaths(lines,
293+
settings.PathFormat,
294+
settings.IgnoreFileRelative,
295+
formatingDirs,
296+
documentDir,
297+
includeRootDirectory);
232298
FormatDelimiters(lines, settings.DelimiterFormatting);
233299
FormatSlashes(lines, settings.SlashFormatting);
234300

IncludeToolbox/Options/FormatterOptionsPage.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,29 @@ public enum PathMode
1919
Shortest,
2020
Shortest_AvoidUpSteps,
2121
Absolute,
22+
Absolute_FromParentDirWithFile
2223
}
2324
[Category("Path")]
2425
[DisplayName("Mode")]
2526
[Description("Changes the path mode to the given pattern.")]
2627
public PathMode PathFormat { get; set; } = PathMode.Shortest_AvoidUpSteps;
2728

29+
[Category("Path")]
30+
[DisplayName("Filename in parent directory for absolute include paths")]
31+
[Description("The Absolute_FromParentDirWithFile mode will look for this file in parent directories and make include paths absolute from its location.")]
32+
public string FromParentDirWithFile { get; set; } = "build.root";
33+
34+
public enum IgnoreFileRelativeMode
35+
{
36+
Never,
37+
Always,
38+
InSameDirectory,
39+
InSameOrSubDirectory
40+
}
2841
[Category("Path")]
2942
[DisplayName("Ignore File Relative")]
30-
[Description("If true, include directives will not take the path of the file into account.")]
31-
public bool IgnoreFileRelative { get; set; } = false;
43+
[Description("Whether to ignore include paths relative to the current file.")]
44+
public IgnoreFileRelativeMode IgnoreFileRelative { get; set; } = IgnoreFileRelativeMode.Never;
3245

3346
//[Category("Path")]
3447
//[DisplayName("Ignore Standard Library")]
@@ -110,7 +123,8 @@ public override void SaveSettingsToStorage()
110123
settingsStore.CreateCollection(collectionName);
111124

112125
settingsStore.SetInt32(collectionName, nameof(PathFormat), (int)PathFormat);
113-
settingsStore.SetBoolean(collectionName, nameof(IgnoreFileRelative), IgnoreFileRelative);
126+
settingsStore.SetString(collectionName, nameof(FromParentDirWithFile), FromParentDirWithFile);
127+
settingsStore.SetInt32(collectionName, nameof(IgnoreFileRelative), (int)IgnoreFileRelative);
114128

115129
settingsStore.SetInt32(collectionName, nameof(DelimiterFormatting), (int)DelimiterFormatting);
116130
settingsStore.SetInt32(collectionName, nameof(SlashFormatting), (int)SlashFormatting);
@@ -129,8 +143,10 @@ public override void LoadSettingsFromStorage()
129143

130144
if (settingsStore.PropertyExists(collectionName, nameof(PathFormat)))
131145
PathFormat = (PathMode)settingsStore.GetInt32(collectionName, nameof(PathFormat));
146+
if (settingsStore.PropertyExists(collectionName, nameof(FromParentDirWithFile)))
147+
FromParentDirWithFile = settingsStore.GetString(collectionName, nameof(FromParentDirWithFile));
132148
if (settingsStore.PropertyExists(collectionName, nameof(IgnoreFileRelative)))
133-
IgnoreFileRelative = settingsStore.GetBoolean(collectionName, nameof(IgnoreFileRelative));
149+
IgnoreFileRelative = (IgnoreFileRelativeMode)settingsStore.GetInt32(collectionName, nameof(IgnoreFileRelative));
134150

135151
if (settingsStore.PropertyExists(collectionName, nameof(DelimiterFormatting)))
136152
DelimiterFormatting = (DelimiterMode) settingsStore.GetInt32(collectionName, nameof(DelimiterFormatting));

IncludeToolbox/Package/source.extension.vsixmanifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
33
<Metadata>
4-
<Identity Id="IncludeToolbox.Andreas Reich.075c2e2b-7b71-45ba-b2e6-c1dadc81cfac" Version="2.2.0" Language="en-US" Publisher="Andreas Reich" />
4+
<Identity Id="IncludeToolbox.Andreas Reich.075c2e2b-7b71-45ba-b2e6-c1dadc81cfac" Version="2.2.0.2" Language="en-US" Publisher="Andreas Reich" />
55
<DisplayName>IncludeToolbox</DisplayName>
66
<Description xml:space="preserve">Various tools for managing C/C++ #includes: Formatting, sorting, exploring, pruning.</Description>
77
<License>license.txt</License>

IncludeToolbox/Utils.cs

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using Microsoft.VisualStudio.Shell;
77
using Microsoft.VisualStudio.Text.Editor;
88
using Microsoft.VisualStudio.TextManager.Interop;
9-
using Microsoft.VisualStudio.VCProjectEngine;
9+
//using Microsoft.VisualStudio.VCProjectEngine;
1010

1111
namespace IncludeToolbox
1212
{
@@ -35,23 +35,61 @@ public static string MakeRelative(string absoluteRoot, string absoluteTarget)
3535
return relativePath;
3636
}
3737

38-
public static string GetExactPathName(string pathName)
38+
// Shamelessly stolen from https://stackoverflow.com/a/5076517/153079
39+
private static string GetFileSystemCasing(string path)
3940
{
40-
if (!File.Exists(pathName) && !Directory.Exists(pathName))
41-
return pathName;
41+
if (Path.IsPathRooted(path))
42+
{
43+
path = path.TrimEnd(Path.DirectorySeparatorChar); // if you type c:\foo\ instead of c:\foo
44+
try
45+
{
46+
string name = Path.GetFileName(path);
47+
if (name == "")
48+
return path.ToUpper() + Path.DirectorySeparatorChar; // root reached
4249

43-
var di = new DirectoryInfo(pathName);
50+
string parent = Path.GetDirectoryName(path);
4451

45-
if (di.Parent != null)
46-
{
47-
return Path.Combine(
48-
GetExactPathName(di.Parent.FullName),
49-
di.Parent.GetFileSystemInfos(di.Name)[0].Name);
52+
parent = GetFileSystemCasing(parent);
53+
54+
DirectoryInfo diParent = new DirectoryInfo(parent);
55+
FileSystemInfo[] fsiChildren = diParent.GetFileSystemInfos(name);
56+
FileSystemInfo fsiChild = fsiChildren.First();
57+
return fsiChild.FullName;
58+
}
59+
catch (Exception ex)
60+
{
61+
Output.Instance.ErrorMsg("Invalid path: '{0}'\nError:\n{1}", path, ex.Message);
62+
}
5063
}
5164
else
5265
{
53-
return di.Name.ToUpper();
66+
Output.Instance.ErrorMsg("Absolute path needed, not relative: '{0}'", path);
67+
}
68+
69+
return "";
70+
}
71+
72+
/// <summary>
73+
/// Gets the path name as it exists it the file system, normalizing it.
74+
/// </summary>
75+
/// <param name="pathName">Path name</param>
76+
/// <returns>Normalized path name. Directories are returned with trailing separator.</returns>
77+
public static string GetExactPathName(string pathName)
78+
{
79+
if (!File.Exists(pathName) && !Directory.Exists(pathName))
80+
{
81+
return pathName;
5482
}
83+
84+
string exactPathName = GetFileSystemCasing(Path.GetFullPath(pathName).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));
85+
if (exactPathName == "")
86+
return pathName;
87+
88+
// Add trailing slash for directories
89+
exactPathName = exactPathName.TrimEnd(Path.DirectorySeparatorChar);
90+
if (File.GetAttributes(exactPathName).HasFlag(FileAttributes.Directory))
91+
return exactPathName + Path.DirectorySeparatorChar;
92+
return exactPathName;
5593
}
5694

5795
/// <summary>

0 commit comments

Comments
 (0)