From c18eaad36455281d35b26180a735e998b8d648be Mon Sep 17 00:00:00 2001 From: Dark Daskin Date: Fri, 14 Jun 2019 17:20:13 +0300 Subject: [PATCH] Showing relevant path segments like VSCode. --- VSTabPath.Tests/DisplayPathResolverTests.cs | 162 ----------- .../DisplayPathResolverTests_NoSolution.cs | 267 ++++++++++++++++++ .../DisplayPathResolverTests_Solution.cs | 221 +++++++++++++++ VSTabPath.Tests/VSTabPath.Tests.csproj | 4 +- VSTabPath.Tests/app.config | 15 + VSTabPath/Models/DisplayPathResolver.cs | 154 +++++++++- VSTabPath/Models/TabModel.cs | 2 + VSTabPath/TabTitleManager.cs | 15 + VSTabPath/VSTabPath.csproj | 3 + VSTabPath/packages.config | 1 + 10 files changed, 669 insertions(+), 175 deletions(-) delete mode 100644 VSTabPath.Tests/DisplayPathResolverTests.cs create mode 100644 VSTabPath.Tests/DisplayPathResolverTests_NoSolution.cs create mode 100644 VSTabPath.Tests/DisplayPathResolverTests_Solution.cs create mode 100644 VSTabPath.Tests/app.config diff --git a/VSTabPath.Tests/DisplayPathResolverTests.cs b/VSTabPath.Tests/DisplayPathResolverTests.cs deleted file mode 100644 index 124bf74..0000000 --- a/VSTabPath.Tests/DisplayPathResolverTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using VSTabPath.Models; - -namespace VSTabPath.Tests -{ - [TestClass] - public class DisplayPathResolverTests - { - [TestMethod] - public void WhenNoDuplicateFileNames_DoNotShowPaths() - { - var models = new[] - { - new TabModel(@"c:\Directory1\1.txt"), - new TabModel(@"c:\Directory1\2.txt"), - new TabModel(@"c:\Directory2\3.txt"), - }; - - var resolver = new DisplayPathResolver - { - models[0], - models[1], - models[2], - }; - - Assert.AreEqual(null, models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual(null, models[2].DisplayPath); - } - - [TestMethod] - public void WhenDuplicateFileNames_ShowPaths() - { - var models = new[] - { - new TabModel(@"c:\Directory1\1.txt"), - new TabModel(@"c:\Directory1\2.txt"), - new TabModel(@"c:\Directory2\1.txt"), - }; - - var resolver = new DisplayPathResolver - { - models[0], - models[1], - models[2], - }; - - Assert.AreEqual("Directory1", models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual("Directory2", models[2].DisplayPath); - } - - [TestMethod] - public void WhenNewTabIsAdded_UpdatePaths() - { - var models = new[] - { - new TabModel(@"c:\Directory1\1.txt"), - new TabModel(@"c:\Directory1\2.txt"), - new TabModel(@"c:\Directory2\1.txt"), - }; - - var resolver = new DisplayPathResolver - { - models[0], - models[1], - }; - - Assert.AreEqual(null, models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - - resolver.Add(models[2]); - - Assert.AreEqual("Directory1", models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual("Directory2", models[2].DisplayPath); - } - - [TestMethod] - public void WhenNewTabIsRemoved_UpdatePaths() - { - var models = new[] - { - new TabModel(@"c:\Directory1\1.txt"), - new TabModel(@"c:\Directory1\2.txt"), - new TabModel(@"c:\Directory2\1.txt"), - }; - - var resolver = new DisplayPathResolver - { - models[0], - models[1], - models[2], - }; - - Assert.AreEqual("Directory1", models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual("Directory2", models[2].DisplayPath); - - resolver.Remove(models[2]); - - Assert.AreEqual(null, models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - } - - [TestMethod] - public void WhenNewTabIsRenamed_UpdatePaths() - { - var models = new[] - { - new TabModel(@"c:\Directory1\1.txt"), - new TabModel(@"c:\Directory1\2.txt"), - new TabModel(@"c:\Directory2\3.txt"), - }; - - var resolver = new DisplayPathResolver - { - models[0], - models[1], - models[2], - }; - - Assert.AreEqual(null, models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual(null, models[2].DisplayPath); - - models[2].FullPath = @"c:\Directory2\1.txt"; - - Assert.AreEqual("Directory1", models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual("Directory2", models[2].DisplayPath); - } - - [TestMethod] - public void WhenNewTabIsMoved_UpdatePaths() - { - var models = new[] - { - new TabModel(@"c:\Directory1\1.txt"), - new TabModel(@"c:\Directory1\2.txt"), - new TabModel(@"c:\Directory2\1.txt"), - }; - - var resolver = new DisplayPathResolver - { - models[0], - models[1], - models[2], - }; - - Assert.AreEqual("Directory1", models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual("Directory2", models[2].DisplayPath); - - models[2].FullPath = @"c:\Directory3\1.txt"; - - Assert.AreEqual("Directory1", models[0].DisplayPath); - Assert.AreEqual(null, models[1].DisplayPath); - Assert.AreEqual("Directory3", models[2].DisplayPath); - } - } -} diff --git a/VSTabPath.Tests/DisplayPathResolverTests_NoSolution.cs b/VSTabPath.Tests/DisplayPathResolverTests_NoSolution.cs new file mode 100644 index 0000000..1e4e14f --- /dev/null +++ b/VSTabPath.Tests/DisplayPathResolverTests_NoSolution.cs @@ -0,0 +1,267 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VSTabPath.Models; + +namespace VSTabPath.Tests +{ + [TestClass] + // ReSharper disable once InconsistentNaming + public class DisplayPathResolverTests_NoSolution + { + [TestMethod] + public void WhenNoDuplicateFileNames_DoNotShowPaths() + { + var models = new[] + { + new TabModel(@"c:\Directory1\1.txt"), + new TabModel(@"c:\Directory1\2.txt"), + new TabModel(@"c:\Directory2\3.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + models[2], + }; + + Assert.AreEqual(null, models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(null, models[2].DisplayPath); + } + + [TestMethod] + public void WhenDuplicateFileNames_ShowPaths() + { + var models = new[] + { + new TabModel(@"c:\Directory1\1.txt"), + new TabModel(@"c:\Directory1\2.txt"), + new TabModel(@"c:\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + models[2], + }; + + Assert.AreEqual(@"c:\Directory1", models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(@"c:\Directory2", models[2].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesInDeepSiblingDirectories_ShowPathsWithEllipsis() + { + var models = new[] + { + new TabModel(@"c:\root\Directory1\1.txt"), + new TabModel(@"c:\root\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + }; + + Assert.AreEqual(@"c:\…\Directory1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\Directory2", models[1].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesInVeryDeepSiblingDirectories_ShowPathsWithSingleEllipsis() + { + var models = new[] + { + new TabModel(@"c:\root\very\deep\Directory1\1.txt"), + new TabModel(@"c:\root\very\deep\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + }; + + Assert.AreEqual(@"c:\…\Directory1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\Directory2", models[1].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesInNonSiblingDirectories_ShowPathsEndWithSingleDirectory() + { + var models = new[] + { + new TabModel(@"c:\root\Directory1\1.txt"), + new TabModel(@"c:\root\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + }; + + Assert.AreEqual(@"c:\…\Directory1", models[0].DisplayPath); + Assert.AreEqual(@"c:\root", models[1].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesInSameNamedDirectories_ShowPathsEndWithMultipleDirectory() + { + var models = new[] + { + new TabModel(@"c:\root\Directory1\SubDirectory1\1.txt"), + new TabModel(@"c:\root\Directory2\SubDirectory1\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + }; + + // TODO: VSCode has better display: c:\…\Directory1\… + Assert.AreEqual(@"c:\…\Directory1\SubDirectory1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\Directory2\SubDirectory1", models[1].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesDifferByCase_ShowPaths() + { + var models = new[] + { + new TabModel(@"c:\root\Directory1\1.txt"), + new TabModel(@"c:\Root\Directory2\1.txt"), + new TabModel(@"c:\root\Directory1\2.txt"), + new TabModel(@"c:\root\Directory2\2.TXT"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + models[2], + models[3], + }; + + Assert.AreEqual(@"c:\…\Directory1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\Directory2", models[1].DisplayPath); + Assert.AreEqual(@"c:\…\Directory1", models[2].DisplayPath); + Assert.AreEqual(@"c:\…\Directory2", models[3].DisplayPath); + } + + // TODO: VSCode with different drives: c:\… d:\… + + [TestMethod] + public void WhenNewTabIsAdded_UpdatePaths() + { + var models = new[] + { + new TabModel(@"c:\Directory1\1.txt"), + new TabModel(@"c:\Directory1\2.txt"), + new TabModel(@"c:\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + }; + + Assert.AreEqual(null, models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + + resolver.Add(models[2]); + + Assert.AreEqual(@"c:\Directory1", models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(@"c:\Directory2", models[2].DisplayPath); + } + + [TestMethod] + public void WhenNewTabIsRemoved_UpdatePaths() + { + var models = new[] + { + new TabModel(@"c:\Directory1\1.txt"), + new TabModel(@"c:\Directory1\2.txt"), + new TabModel(@"c:\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + models[2], + }; + + Assert.AreEqual(@"c:\Directory1", models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(@"c:\Directory2", models[2].DisplayPath); + + resolver.Remove(models[2]); + + Assert.AreEqual(null, models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + } + + [TestMethod] + public void WhenNewTabIsRenamed_UpdatePaths() + { + var models = new[] + { + new TabModel(@"c:\Directory1\1.txt"), + new TabModel(@"c:\Directory1\2.txt"), + new TabModel(@"c:\Directory2\3.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + models[2], + }; + + Assert.AreEqual(null, models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(null, models[2].DisplayPath); + + models[2].FullPath = @"c:\Directory2\1.txt"; + + Assert.AreEqual(@"c:\Directory1", models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(@"c:\Directory2", models[2].DisplayPath); + } + + [TestMethod] + public void WhenNewTabIsMoved_UpdatePaths() + { + var models = new[] + { + new TabModel(@"c:\Directory1\1.txt"), + new TabModel(@"c:\Directory1\2.txt"), + new TabModel(@"c:\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + models[2], + }; + + Assert.AreEqual(@"c:\Directory1", models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(@"c:\Directory2", models[2].DisplayPath); + + models[2].FullPath = @"c:\Directory3\1.txt"; + + Assert.AreEqual(@"c:\Directory1", models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(@"c:\Directory3", models[2].DisplayPath); + } + } +} diff --git a/VSTabPath.Tests/DisplayPathResolverTests_Solution.cs b/VSTabPath.Tests/DisplayPathResolverTests_Solution.cs new file mode 100644 index 0000000..dfdda52 --- /dev/null +++ b/VSTabPath.Tests/DisplayPathResolverTests_Solution.cs @@ -0,0 +1,221 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VSTabPath.Models; + +namespace VSTabPath.Tests +{ + [TestClass] + public class DisplayPathResolverTests_Solution + { + [TestMethod] + public void WhenNoDuplicateFileNames_DoNotShowPaths() + { + var models = new[] + { + new TabModel(@"c:\Solution\Directory1\1.txt"), + new TabModel(@"c:\Solution\Directory1\2.txt"), + new TabModel(@"c:\Solution\Directory2\3.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + models[2], + }; + + Assert.AreEqual(null, models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(null, models[2].DisplayPath); + } + + [TestMethod] + public void WhenDuplicateFileNames_ShowPaths() + { + var models = new[] + { + new TabModel(@"c:\Solution\Directory1\1.txt"), + new TabModel(@"c:\Solution\Directory1\2.txt"), + new TabModel(@"c:\Solution\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + models[2], + }; + + Assert.AreEqual(@"Directory1", models[0].DisplayPath); + Assert.AreEqual(null, models[1].DisplayPath); + Assert.AreEqual(@"Directory2", models[2].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesInDeepSiblingDirectories_ShowPathsWithEllipsis() + { + var models = new[] + { + new TabModel(@"c:\Solution\Project\Directory1\1.txt"), + new TabModel(@"c:\Solution\Project\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + }; + + Assert.AreEqual(@"…\Directory1", models[0].DisplayPath); + Assert.AreEqual(@"…\Directory2", models[1].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesInVeryDeepSiblingDirectories_ShowPathsWithSingleEllipsis() + { + var models = new[] + { + new TabModel(@"c:\Solution\Project\very\deep\Directory1\1.txt"), + new TabModel(@"c:\Solution\Project\very\deep\Directory2\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + }; + + Assert.AreEqual(@"…\Directory1", models[0].DisplayPath); + Assert.AreEqual(@"…\Directory2", models[1].DisplayPath); + } + + [TestMethod] + public void WhenDuplicatesInSameNamedDirectories_ShowPathsEndWithMultipleDirectory() + { + var models = new[] + { + new TabModel(@"c:\Solution\Project1\SubDirectory1\1.txt"), + new TabModel(@"c:\Solution\Project2\SubDirectory1\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + }; + + Assert.AreEqual(@"Project1\SubDirectory1", models[0].DisplayPath); + Assert.AreEqual(@"Project2\SubDirectory1", models[1].DisplayPath); + } + + [TestMethod] + public void WhenOneDuplicateOutsideSolutionRoot_ShowPathWithDriveLetter() + { + var models = new[] + { + new TabModel(@"c:\Solution\Project1\SubDirectory1\1.txt"), + new TabModel(@"c:\Other\SubDirectory1\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + }; + + Assert.AreEqual(@"…\SubDirectory1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\SubDirectory1", models[1].DisplayPath); + } + + [TestMethod] + public void WhenOneDuplicateAtSolutionRoot_ShowDotPath() + { + var models = new[] + { + new TabModel(@"c:\Solution\Project1\1.txt"), + new TabModel(@"c:\Solution\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + }; + + Assert.AreEqual(@"Project1", models[0].DisplayPath); + Assert.AreEqual(@".\", models[1].DisplayPath); + } + + [TestMethod] + public void WhenSolutionIsLoaded_UpdatePaths() + { + var models = new[] + { + new TabModel(@"c:\Solution\Project1\1.txt"), + new TabModel(@"c:\Solution\Project2\1.txt"), + }; + + var resolver = new DisplayPathResolver + { + models[0], + models[1], + }; + + Assert.AreEqual(@"c:\…\Project1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\Project2", models[1].DisplayPath); + + resolver.SolutionRootPath = @"c:\Solution"; + + Assert.AreEqual(@"Project1", models[0].DisplayPath); + Assert.AreEqual(@"Project2", models[1].DisplayPath); + } + + [TestMethod] + public void WhenSolutionIsUnloaded_UpdatePaths() + { + var models = new[] + { + new TabModel(@"c:\Solution\Project1\1.txt"), + new TabModel(@"c:\Solution\Project2\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution") + { + models[0], + models[1], + }; + + Assert.AreEqual(@"Project1", models[0].DisplayPath); + Assert.AreEqual(@"Project2", models[1].DisplayPath); + + resolver.SolutionRootPath = null; + + Assert.AreEqual(@"c:\…\Project1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\Project2", models[1].DisplayPath); + } + + [TestMethod] + public void WhenSolutionIsChanged_UpdatePaths() + { + var models = new[] + { + new TabModel(@"c:\Solution1\Project1\1.txt"), + new TabModel(@"c:\Solution2\Project2\1.txt"), + }; + + var resolver = new DisplayPathResolver(@"c:\Solution1") + { + models[0], + models[1], + }; + + Assert.AreEqual(@"Project1", models[0].DisplayPath); + Assert.AreEqual(@"c:\…\Project2", models[1].DisplayPath); + + resolver.SolutionRootPath = @"c:\Solution2"; + + Assert.AreEqual(@"c:\…\Project1", models[0].DisplayPath); + Assert.AreEqual(@"Project2", models[1].DisplayPath); + } + } +} diff --git a/VSTabPath.Tests/VSTabPath.Tests.csproj b/VSTabPath.Tests/VSTabPath.Tests.csproj index 362f1fd..6f65f66 100644 --- a/VSTabPath.Tests/VSTabPath.Tests.csproj +++ b/VSTabPath.Tests/VSTabPath.Tests.csproj @@ -50,10 +50,12 @@ - + + + diff --git a/VSTabPath.Tests/app.config b/VSTabPath.Tests/app.config new file mode 100644 index 0000000..e3c4ef6 --- /dev/null +++ b/VSTabPath.Tests/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VSTabPath/Models/DisplayPathResolver.cs b/VSTabPath/Models/DisplayPathResolver.cs index 1b8195d..96607e5 100644 --- a/VSTabPath/Models/DisplayPathResolver.cs +++ b/VSTabPath/Models/DisplayPathResolver.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.IO; @@ -8,7 +9,33 @@ namespace VSTabPath.Models { public class DisplayPathResolver : IEnumerable { + private const string Ellipsis = "…"; + + private static readonly char[] DirectorySeparators = + {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}; + private readonly List _models = new List(); + private string _solutionRootPath; + + public string SolutionRootPath + { + get => _solutionRootPath; + set + { + _solutionRootPath = value; + + UpdateModels(); + } + } + + public DisplayPathResolver() + { + } + + public DisplayPathResolver(string solutionRootPath) + { + _solutionRootPath = solutionRootPath; + } public void Add(TabModel model) { @@ -46,25 +73,22 @@ private void OnModelPropertyChanged(object sender, PropertyChangedEventArgs args private void UpdateModels(string fileName) { - var modelsToUpdate = _models.Where(m => m.FileName == fileName).ToList(); + var modelsToUpdate = _models.Where(m => PathEquals(m.FileName, fileName)).ToList(); if (modelsToUpdate.Count == 1) modelsToUpdate[0].DisplayPath = null; else - { - foreach (var model in modelsToUpdate) - model.DisplayPath = GetParentDirectoryName(model); - } + UpdateModelsWithDuplicateFilename(modelsToUpdate); } private void UpdateModels() { - var modelsByFileName = _models.ToLookup(m => m.FileName); + var modelsByFileName = _models.ToLookup(m => m.FileName, StringComparer.OrdinalIgnoreCase); var modelsWithDuplicateFileName = modelsByFileName .Where(g => g.Count() > 1) - .SelectMany(g => g); - foreach (var model in modelsWithDuplicateFileName) - model.DisplayPath = GetParentDirectoryName(model); + .SelectMany(g => g) + .ToList(); + UpdateModelsWithDuplicateFilename(modelsWithDuplicateFileName); var modelsWithUniqueFileName = modelsByFileName .Where(g => g.Count() == 1) @@ -73,9 +97,115 @@ private void UpdateModels() model.DisplayPath = null; } - private static string GetParentDirectoryName(TabModel model) + private void UpdateModelsWithDuplicateFilename(IReadOnlyCollection models) + { + var modelsAtSolutionRoot = models + .Where(m => PathEquals(m.DirectoryName, SolutionRootPath)); + foreach (var model in modelsAtSolutionRoot) + model.DisplayPath = @".\"; + + var modelsOutsideSolutionRoot = models + .Where(m => !IsBaseOf(SolutionRootPath, m.DirectoryName) && !PathEquals(m.DirectoryName, SolutionRootPath)); + ResolvePartialDisplayPaths(modelsOutsideSolutionRoot, false); + + var modelsUnderSolutionRoot = models + .Where(m => IsBaseOf(SolutionRootPath, m.DirectoryName)); + ResolvePartialDisplayPaths(modelsUnderSolutionRoot, true); + } + + private static bool PathEquals(string x, string y) + { + return string.Equals(x, y, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsBaseOf(string root, string path) { - return Path.GetFileName(Path.GetDirectoryName(model.FullPath)); + if (root == null) + return false; + return new Uri(Path.Combine(root, ".")).IsBaseOf(new Uri(path)); + } + + private static string GetRelativePath(string root, string path) + { + return new Uri(Path.Combine(root, ".")).MakeRelativeUri(new Uri(path)).ToString() + .Replace('/', '\\'); + } + + private void ResolvePartialDisplayPaths(IEnumerable models, bool isUnderSolutionRoot) + { + var modelsByFileName = models.GroupBy(m => m.FileName, + (key, g) => g.ToDictionary(m => m, + m => GetPathParts(m.DirectoryName, isUnderSolutionRoot).Select(p => (p, false)).ToList()), + StringComparer.OrdinalIgnoreCase); + foreach (var group in modelsByFileName) + ResolvePartialDisplayPaths(group, isUnderSolutionRoot); + } + + private string[] GetPathParts(string path, bool isUnderSolutionRoot) + { + if (isUnderSolutionRoot) + path = GetRelativePath(SolutionRootPath, path); + + return path.Split(DirectorySeparators, StringSplitOptions.RemoveEmptyEntries); + } + + private static void ResolvePartialDisplayPaths(Dictionary> models, bool isUnderSolutionRoot) + { + // When path is outside solution root, always include the root segment (i.e. drive letter). + // A separator has to be appended to keep it after Path.Combine. + if (!isUnderSolutionRoot) + foreach (var segments in models.Values) + segments[0] = (segments[0].value + Path.DirectorySeparatorChar, true); + + ResolvePartialDisplayPaths(models); + } + + private static void ResolvePartialDisplayPaths(Dictionary> models) + { + // Include one more segment from the end. + foreach (var segments in models.Values) + { + for (var i = segments.Count - 1; i >= 0; i--) + { + if (segments[i].isIncluded) + continue; + + segments[i] = (segments[i].value, true); + break; + } + } + + // Replace gaps with ellipsis and build the display path. + foreach (var model in models) + { + var finalSegments = new List(); + var hasEllipsis = false; + foreach (var (value, isIncluded) in model.Value) + { + if (isIncluded) + { + hasEllipsis = false; + finalSegments.Add(value); + continue; + } + + if (!hasEllipsis) + { + hasEllipsis = true; + finalSegments.Add(Ellipsis); + } + } + + model.Key.DisplayPath = Path.Combine(finalSegments.ToArray()); + } + + // Find ambiguous paths and resolve them. + var modelsWithDuplicateDisplayPath = models + .GroupBy(m => m.Key.DisplayPath, (key, g) => g.ToDictionary(m => m.Key, m => m.Value), + StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1); + foreach (var group in modelsWithDuplicateDisplayPath) + ResolvePartialDisplayPaths(group); } } } \ No newline at end of file diff --git a/VSTabPath/Models/TabModel.cs b/VSTabPath/Models/TabModel.cs index af43ab2..1de97b8 100644 --- a/VSTabPath/Models/TabModel.cs +++ b/VSTabPath/Models/TabModel.cs @@ -32,6 +32,8 @@ public string DisplayPath } } + public string DirectoryName => Path.GetDirectoryName(FullPath); + public string FileName => Path.GetFileName(FullPath); public TabModel() diff --git a/VSTabPath/TabTitleManager.cs b/VSTabPath/TabTitleManager.cs index b29a1fb..2256a5c 100644 --- a/VSTabPath/TabTitleManager.cs +++ b/VSTabPath/TabTitleManager.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Windows; using System.Windows.Data; +using EnvDTE; using Microsoft.VisualStudio.Platform.WindowManagement; using Microsoft.VisualStudio.PlatformUI.Shell; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; using VSTabPath.Models; using VSTabPath.ViewModels; @@ -53,6 +57,17 @@ public static TabViewModel GetTabViewModel(DependencyObject element) private readonly Dictionary _viewModels = new Dictionary(); private readonly DisplayPathResolver _displayPathResolver = new DisplayPathResolver(); + private readonly DTE _dte = (DTE) Package.GetGlobalService(typeof(SDTE)); + + public TabTitleManager() + { + _displayPathResolver.SolutionRootPath = Path.GetDirectoryName(_dte.Solution?.FullName); + + _dte.Events.SolutionEvents.Opened += () => + _displayPathResolver.SolutionRootPath = Path.GetDirectoryName(_dte.Solution.FullName); + _dte.Events.SolutionEvents.AfterClosing += () => + _displayPathResolver.SolutionRootPath = null; + } public void RegisterDocumentView(DocumentView view) { diff --git a/VSTabPath/VSTabPath.csproj b/VSTabPath/VSTabPath.csproj index ea8cde3..c4d29d3 100644 --- a/VSTabPath/VSTabPath.csproj +++ b/VSTabPath/VSTabPath.csproj @@ -210,6 +210,9 @@ + + ..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + diff --git a/VSTabPath/packages.config b/VSTabPath/packages.config index d3f23da..a4148f6 100644 --- a/VSTabPath/packages.config +++ b/VSTabPath/packages.config @@ -26,4 +26,5 @@ + \ No newline at end of file