diff --git a/VSTabPath.Tests/DisplayPathResolverTests.cs b/VSTabPath.Tests/DisplayPathResolverTests.cs new file mode 100644 index 0000000..124bf74 --- /dev/null +++ b/VSTabPath.Tests/DisplayPathResolverTests.cs @@ -0,0 +1,162 @@ +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/Properties/AssemblyInfo.cs b/VSTabPath.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..54c18b8 --- /dev/null +++ b/VSTabPath.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("VSTabPath.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("VSTabPath.Tests")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("13611075-94b6-4ea9-b777-d56a83db16d6")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/VSTabPath.Tests/VSTabPath.Tests.csproj b/VSTabPath.Tests/VSTabPath.Tests.csproj new file mode 100644 index 0000000..362f1fd --- /dev/null +++ b/VSTabPath.Tests/VSTabPath.Tests.csproj @@ -0,0 +1,75 @@ + + + + + + Debug + AnyCPU + {13611075-94B6-4EA9-B777-D56A83DB16D6} + Library + Properties + VSTabPath.Tests + VSTabPath.Tests + v4.6.1 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + {e2f60519-d34f-4411-9ab7-7916fc004e9f} + VSTabPath + + + + + + + Данный проект ссылается на пакеты NuGet, отсутствующие на этом компьютере. Используйте восстановление пакетов NuGet, чтобы скачать их. Дополнительную информацию см. по адресу: http://go.microsoft.com/fwlink/?LinkID=322105. Отсутствует следующий файл: {0}. + + + + + + \ No newline at end of file diff --git a/VSTabPath.Tests/packages.config b/VSTabPath.Tests/packages.config new file mode 100644 index 0000000..2f7c5a1 --- /dev/null +++ b/VSTabPath.Tests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/VSTabPath.sln b/VSTabPath.sln index 3fce320..e4574dd 100644 --- a/VSTabPath.sln +++ b/VSTabPath.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2005 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VSTabPath", "VSTabPath\VSTabPath.csproj", "{E2F60519-D34F-4411-9AB7-7916FC004E9F}" EndProject @@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VSTabPath.Tests", "VSTabPath.Tests\VSTabPath.Tests.csproj", "{13611075-94B6-4EA9-B777-D56A83DB16D6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,6 +22,10 @@ Global {E2F60519-D34F-4411-9AB7-7916FC004E9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2F60519-D34F-4411-9AB7-7916FC004E9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2F60519-D34F-4411-9AB7-7916FC004E9F}.Release|Any CPU.Build.0 = Release|Any CPU + {13611075-94B6-4EA9-B777-D56A83DB16D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13611075-94B6-4EA9-B777-D56A83DB16D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13611075-94B6-4EA9-B777-D56A83DB16D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13611075-94B6-4EA9-B777-D56A83DB16D6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/VSTabPath/Models/DisplayPathResolver.cs b/VSTabPath/Models/DisplayPathResolver.cs new file mode 100644 index 0000000..1b8195d --- /dev/null +++ b/VSTabPath/Models/DisplayPathResolver.cs @@ -0,0 +1,81 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; + +namespace VSTabPath.Models +{ + public class DisplayPathResolver : IEnumerable + { + private readonly List _models = new List(); + + public void Add(TabModel model) + { + _models.Add(model); + + model.PropertyChanged += OnModelPropertyChanged; + + UpdateModels(model.FileName); + } + + public void Remove(TabModel model) + { + model.PropertyChanged -= OnModelPropertyChanged; + + _models.Remove(model); + + UpdateModels(model.FileName); + } + + public IEnumerator GetEnumerator() + { + return _models.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private void OnModelPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if(args.PropertyName == nameof(TabModel.FullPath)) + UpdateModels(); + } + + private void UpdateModels(string fileName) + { + var modelsToUpdate = _models.Where(m => m.FileName == fileName).ToList(); + if (modelsToUpdate.Count == 1) + modelsToUpdate[0].DisplayPath = null; + else + { + foreach (var model in modelsToUpdate) + model.DisplayPath = GetParentDirectoryName(model); + } + } + + private void UpdateModels() + { + var modelsByFileName = _models.ToLookup(m => m.FileName); + + var modelsWithDuplicateFileName = modelsByFileName + .Where(g => g.Count() > 1) + .SelectMany(g => g); + foreach (var model in modelsWithDuplicateFileName) + model.DisplayPath = GetParentDirectoryName(model); + + var modelsWithUniqueFileName = modelsByFileName + .Where(g => g.Count() == 1) + .SelectMany(g => g); + foreach (var model in modelsWithUniqueFileName) + model.DisplayPath = null; + } + + private static string GetParentDirectoryName(TabModel model) + { + return Path.GetFileName(Path.GetDirectoryName(model.FullPath)); + } + } +} \ No newline at end of file diff --git a/VSTabPath/Models/ObservableModel.cs b/VSTabPath/Models/ObservableModel.cs new file mode 100644 index 0000000..e82f761 --- /dev/null +++ b/VSTabPath/Models/ObservableModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace VSTabPath.Models +{ + public abstract class ObservableModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/VSTabPath/Models/TabModel.cs b/VSTabPath/Models/TabModel.cs new file mode 100644 index 0000000..af43ab2 --- /dev/null +++ b/VSTabPath/Models/TabModel.cs @@ -0,0 +1,46 @@ +using System.IO; +using JetBrains.Annotations; + +namespace VSTabPath.Models +{ + public class TabModel : ObservableModel + { + private string _fullPath; + private string _displayPath; + + public string FullPath + { + get => _fullPath; + set + { + if (value == _fullPath) return; + _fullPath = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(FileName)); + } + } + + [CanBeNull] + public string DisplayPath + { + get => _displayPath; + set + { + if (value == _displayPath) return; + _displayPath = value; + OnPropertyChanged(); + } + } + + public string FileName => Path.GetFileName(FullPath); + + public TabModel() + { + } + + public TabModel(string fullPath) + { + FullPath = fullPath; + } + } +} \ No newline at end of file diff --git a/VSTabPath/TabPathPackage.cs b/VSTabPath/TabPathPackage.cs index f2c666d..af1e9ca 100644 --- a/VSTabPath/TabPathPackage.cs +++ b/VSTabPath/TabPathPackage.cs @@ -75,7 +75,7 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke private static void MergeResources() { - var resourceDictionary = LoadResourceDictionary("DataTemplates.xaml"); + var resourceDictionary = LoadResourceDictionary("Views/DataTemplates.xaml"); Application.Current.Resources.MergedDictionaries.Add(resourceDictionary); } diff --git a/VSTabPath/TabPathViewElementFactory.cs b/VSTabPath/TabPathViewElementFactory.cs index 18ac001..88ea5ea 100644 --- a/VSTabPath/TabPathViewElementFactory.cs +++ b/VSTabPath/TabPathViewElementFactory.cs @@ -57,19 +57,6 @@ public static void SetupExistingDocuments() SetupView(view); } - private static IEnumerable FindLogicalDescendants(DependencyObject obj) - where T : DependencyObject - { - foreach (DependencyObject child in LogicalTreeHelper.GetChildren(obj)) - { - if (child is T tChild) - yield return tChild; - - foreach (var descendant in FindLogicalDescendants(child)) - yield return descendant; - } - } - private static IEnumerable FindVisualDescendants(DependencyObject obj) where T : DependencyObject { diff --git a/VSTabPath/TabTitleManager.cs b/VSTabPath/TabTitleManager.cs index 0328779..b29a1fb 100644 --- a/VSTabPath/TabTitleManager.cs +++ b/VSTabPath/TabTitleManager.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Windows; using System.Windows.Data; using Microsoft.VisualStudio.Platform.WindowManagement; using Microsoft.VisualStudio.PlatformUI.Shell; +using VSTabPath.Models; +using VSTabPath.ViewModels; namespace VSTabPath { @@ -35,31 +36,30 @@ public static TabTitleManager EnsureTabTitleManager(ViewGroup target) #region TitleWithPathProperty - public static readonly DependencyProperty TitleWithPathProperty = DependencyProperty.RegisterAttached( - "TitleWithPath", typeof(WindowFrameTitleWithPath), typeof(TabTitleManager)); + public static readonly DependencyProperty TabViewModelProperty = DependencyProperty.RegisterAttached( + "TabViewModel", typeof(TabViewModel), typeof(TabTitleManager)); - public static void SetTitleWithPath(DependencyObject element, WindowFrameTitleWithPath value) + public static void SetTabViewModel(DependencyObject element, TabViewModel value) { - element.SetValue(TitleWithPathProperty, value); + element.SetValue(TabViewModelProperty, value); } - public static WindowFrameTitleWithPath GetTitleWithPath(DependencyObject element) + public static TabViewModel GetTabViewModel(DependencyObject element) { - return (WindowFrameTitleWithPath) element.GetValue(TitleWithPathProperty); + return (TabViewModel) element.GetValue(TabViewModelProperty); } #endregion - private readonly Dictionary _views = new Dictionary(); + private readonly Dictionary _viewModels = new Dictionary(); + private readonly DisplayPathResolver _displayPathResolver = new DisplayPathResolver(); public void RegisterDocumentView(DocumentView view) { - if (_views.ContainsKey(view)) + if (_viewModels.ContainsKey(view)) return; InstallTabTitlePath(view); - - UpdateTabTitles(); } private void InstallTabTitlePath(DocumentView view) @@ -71,36 +71,29 @@ private void InstallTabTitlePath(DocumentView view) if (!(bindingExpression?.DataItem is WindowFrame frame)) return; - var titleWithPath = new WindowFrameTitleWithPath(title, view.TabTitleTemplate, frame, this); - SetTitleWithPath(view, titleWithPath); + var model = new TabModel(frame.FrameMoniker.Filename); + frame.PropertyChanged += (sender, args) => + { + if (args.PropertyName == nameof(WindowFrame.AnnotatedTitle)) + model.FullPath = frame.FrameMoniker.Filename; + }; + + _displayPathResolver.Add(model); + + var viewModel = new TabViewModel(title, view.TabTitleTemplate, model); + SetTabViewModel(view, viewModel); + view.DocumentTabTitleTemplate = view.TabTitleTemplate = (DataTemplate) Application.Current.FindResource("TabPathTemplate"); - _views.Add(view, titleWithPath); + _viewModels.Add(view, viewModel); frame.FrameDestroyed += (sender, args) => { - _views.Remove(view); - UpdateTabTitles(); + _displayPathResolver.Remove(_viewModels[view].Model); + _viewModels.Remove(view); }; } - - public void UpdateTabTitles() - { - var modelsByTitle = _views.ToLookup(kv => kv.Value.OriginalTitle, kv => kv.Value); - - var modelsWithDuplicateTitles = modelsByTitle - .Where(g => g.Count() > 1) - .SelectMany(g => g); - foreach (var model in modelsWithDuplicateTitles) - model.IsPathVisible = true; - - var modelsWithUniqueTitles = modelsByTitle - .Where(g => g.Count() == 1) - .SelectMany(g => g); - foreach (var model in modelsWithUniqueTitles) - model.IsPathVisible = false; - } private static TProperty EnsurePropertyValue(T target, DependencyProperty property, Func factory) where T : DependencyObject diff --git a/VSTabPath/VSTabPath.csproj b/VSTabPath/VSTabPath.csproj index a310dd4..ea8cde3 100644 --- a/VSTabPath/VSTabPath.csproj +++ b/VSTabPath/VSTabPath.csproj @@ -55,11 +55,14 @@ + + + - + @@ -223,7 +226,7 @@ - + Designer MSBuild:Compile diff --git a/VSTabPath/ViewModels/TabViewModel.cs b/VSTabPath/ViewModels/TabViewModel.cs new file mode 100644 index 0000000..4065eee --- /dev/null +++ b/VSTabPath/ViewModels/TabViewModel.cs @@ -0,0 +1,35 @@ +using System.ComponentModel; +using System.Windows; +using Microsoft.VisualStudio.Platform.WindowManagement; +using VSTabPath.Models; + +namespace VSTabPath.ViewModels +{ + public class TabViewModel : ObservableModel + { + public WindowFrameTitle Title { get; } + public DataTemplate TitleTemplate { get; } + public TabModel Model { get; } + + public string DisplayPath => Model.DisplayPath; + + public bool IsPathVisible => DisplayPath != null; + + public TabViewModel(WindowFrameTitle title, DataTemplate titleTemplate, TabModel model) + { + Title = title; + TitleTemplate = titleTemplate; + Model = model; + Model.PropertyChanged += OnModelPropertyChanged; + } + + private void OnModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TabModel.DisplayPath)) + { + OnPropertyChanged(nameof(DisplayPath)); + OnPropertyChanged(nameof(IsPathVisible)); + } + } + } +} \ No newline at end of file diff --git a/VSTabPath/DataTemplates.xaml b/VSTabPath/Views/DataTemplates.xaml similarity index 89% rename from VSTabPath/DataTemplates.xaml rename to VSTabPath/Views/DataTemplates.xaml index d9c711e..0c6d061 100644 --- a/VSTabPath/DataTemplates.xaml +++ b/VSTabPath/Views/DataTemplates.xaml @@ -2,9 +2,10 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:VSTabPath;assembly=VSTabPath" + xmlns:viewModels="clr-namespace:VSTabPath.ViewModels;assembly=VSTabPath" xmlns:wm="clr-namespace:Microsoft.VisualStudio.Platform.WindowManagement;assembly=Microsoft.VisualStudio.Platform.WindowManagement"> - + + Text="{Binding DisplayPath}" /> @@ -29,6 +30,6 @@ + DataContext="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.(local:TabTitleManager.TabViewModel)}" /> \ No newline at end of file diff --git a/VSTabPath/WindowFrameTitleWithPath.cs b/VSTabPath/WindowFrameTitleWithPath.cs deleted file mode 100644 index d252c27..0000000 --- a/VSTabPath/WindowFrameTitleWithPath.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.ComponentModel; -using System.IO; -using System.Runtime.CompilerServices; -using System.Windows; -using JetBrains.Annotations; -using Microsoft.VisualStudio.Platform.WindowManagement; - -namespace VSTabPath -{ - public class WindowFrameTitleWithPath : INotifyPropertyChanged - { - private readonly WindowFrame _frame; - private readonly TabTitleManager _titleManager; - public WindowFrameTitle Title { get; private set; } - public DataTemplate TitleTemplate { get; private set; } - - public string OriginalTitle => _frame.Title; - - public string DirectoryPath => Path.GetFileName(Path.GetDirectoryName(_frame.FrameMoniker.Filename)); - - private bool _isPathVisible; - public bool IsPathVisible - { - get => _isPathVisible; - set - { - if (value == _isPathVisible) return; - _isPathVisible = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(Title)); - } - } - - public WindowFrameTitleWithPath(WindowFrameTitle title, DataTemplate titleTemplate, - WindowFrame frame, TabTitleManager titleManager) - { - Title = title; - TitleTemplate = titleTemplate; - _frame = frame; - _frame.PropertyChanged += OnFramePropertyChanged; - _titleManager = titleManager; - } - - private void OnFramePropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(WindowFrame.AnnotatedTitle)) - { - OnPropertyChanged(nameof(Title)); - - // File rename may change tab title uniqueness. - _titleManager.UpdateTabTitles(); - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - [NotifyPropertyChangedInvocator] - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } -} \ No newline at end of file