diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..f8b28ff --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,15 @@ +image: Visual Studio 2019 + +install: +- ps: (new-object Net.WebClient).DownloadString("https://raw.github.com/madskristensen/ExtensionScripts/master/AppVeyor/vsix.ps1") | iex + +before_build: + - ps: Vsix-IncrementVsixVersion | Vsix-UpdateBuildVersion + - ps: Vsix-TokenReplacement src\SaveAllTheTabs\source.extension.cs 'Version = "([0-9\\.]+)"' 'Version = "{version}"' + +build_script: + - nuget restore src\SaveAllTheTabs\SaveAllTheTabs.csproj -SolutionDirectory src -Verbosity quiet + - msbuild src\SaveAllTheTabs.sln /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m + +after_test: + - ps: Vsix-PushArtifacts | Vsix-PublishToGallery diff --git a/src/SaveAllTheTabs/Commands/PackageCommands.cs b/src/SaveAllTheTabs/Commands/PackageCommands.cs index 047f72f..d286440 100644 --- a/src/SaveAllTheTabs/Commands/PackageCommands.cs +++ b/src/SaveAllTheTabs/Commands/PackageCommands.cs @@ -58,6 +58,7 @@ public static void Initialize(SaveAllTheTabsPackage package) /// Owner package, not null. private PackageCommands(SaveAllTheTabsPackage package) { + ThreadHelper.ThrowIfNotOnUIThread(); if (package == null) { throw new ArgumentNullException(nameof(package)); @@ -74,6 +75,7 @@ private PackageCommands(SaveAllTheTabsPackage package) private void SetupCommands(OleMenuCommandService commandService) { + ThreadHelper.ThrowIfNotOnUIThread(); var guid = typeof(CommandIds).GUID; var commandId = new CommandID(guid, (int)CommandIds.SaveTabs); @@ -114,6 +116,7 @@ private void SetupCommands(OleMenuCommandService commandService) private void CommandOnBeforeQueryStatus(object sender, EventArgs eventArgs) { + ThreadHelper.ThrowIfNotOnUIThread(); var command = sender as OleMenuCommand; if (command == null) { @@ -138,6 +141,7 @@ private void ExecuteSaveTabsCommand(object sender, EventArgs e) private void ExecuteSavedTabsWindowCommand(object sender, EventArgs e) { + ThreadHelper.ThrowIfNotOnUIThread(); // Get the instance number 0 of this tool window. This window is single instance so this instance // is actually the only one. // The last flag is set to true so that if the tool window does not exists it will be created. diff --git a/src/SaveAllTheTabs/Commands/RestoreTabsListCommands.cs b/src/SaveAllTheTabs/Commands/RestoreTabsListCommands.cs index 73db40b..52dc225 100644 --- a/src/SaveAllTheTabs/Commands/RestoreTabsListCommands.cs +++ b/src/SaveAllTheTabs/Commands/RestoreTabsListCommands.cs @@ -25,6 +25,7 @@ public RestoreTabsListCommands(SaveAllTheTabsPackage package) public void SetupCommands(OleMenuCommandService commandService) { + ThreadHelper.ThrowIfNotOnUIThread(); if (Package.DocumentManager == null) { return; diff --git a/src/SaveAllTheTabs/Commands/SavedTabsWindowCommands.cs b/src/SaveAllTheTabs/Commands/SavedTabsWindowCommands.cs index c7f4cbb..44c6a35 100644 --- a/src/SaveAllTheTabs/Commands/SavedTabsWindowCommands.cs +++ b/src/SaveAllTheTabs/Commands/SavedTabsWindowCommands.cs @@ -81,6 +81,7 @@ private void SetupCommands(OleMenuCommandService commandService) private void SaveToCommandOnBeforeQueryStatus(object sender, EventArgs eventArgs) { + ThreadHelper.ThrowIfNotOnUIThread(); var command = sender as OleMenuCommand; if (command == null) { @@ -93,6 +94,7 @@ private void SaveToCommandOnBeforeQueryStatus(object sender, EventArgs eventArgs private void CloseCommandOnBeforeQueryStatus(object sender, EventArgs eventArgs) { + ThreadHelper.ThrowIfNotOnUIThread(); var command = sender as OleMenuCommand; if (command == null) { diff --git a/src/SaveAllTheTabs/DocumentManager.cs b/src/SaveAllTheTabs/DocumentManager.cs index dc8ef6b..3ca3740 100644 --- a/src/SaveAllTheTabs/DocumentManager.cs +++ b/src/SaveAllTheTabs/DocumentManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -8,8 +9,12 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Text; +using System.Windows.Forms; +using Microsoft; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Settings; +using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Shell.Settings; using Newtonsoft.Json; @@ -67,28 +72,39 @@ internal class DocumentManager : IDocumentManager private const string StorageCollectionPath = "SaveAllTheTabs"; private const string SavedTabsStoragePropertyFormat = "SavedTabs.{0}"; + private const string ProjectGroupsKeyPlaceholder = StorageCollectionPath + "\\{0}\\groups"; private SaveAllTheTabsPackage Package { get; } private IServiceProvider ServiceProvider => Package; private IVsUIShellDocumentWindowMgr DocumentWindowMgr { get; } - private string SolutionName => Package.Environment.Solution?.FullName; + private string SolutionName + { + get + { + ThreadHelper.ThrowIfNotOnUIThread(); + return Package.Environment.Solution?.FullName; + } + } public ObservableCollection Groups { get; private set; } public DocumentManager(SaveAllTheTabsPackage package) { + ThreadHelper.ThrowIfNotOnUIThread(); Package = package; package.SolutionChanged += (sender, args) => LoadGroups(); LoadGroups(); DocumentWindowMgr = ServiceProvider.GetService(typeof(IVsUIShellDocumentWindowMgr)) as IVsUIShellDocumentWindowMgr; + Assumes.Present(DocumentWindowMgr); } private IDisposable _changeSubscription; private void LoadGroups() { + ThreadHelper.ThrowIfNotOnUIThread(); _changeSubscription?.Dispose(); // Load presets for the current solution @@ -121,6 +137,7 @@ private void LoadGroups() public void SaveGroup(string name, int? slot = null) { + ThreadHelper.ThrowIfNotOnUIThread(); if (DocumentWindowMgr == null) { Debug.Assert(false, "IVsUIShellDocumentWindowMgr", String.Empty, 0); @@ -212,11 +229,13 @@ private void TrySetSlot(DocumentGroup group, int? slot) public void RestoreGroup(int slot) { + ThreadHelper.ThrowIfNotOnUIThread(); RestoreGroup(Groups.FindBySlot(slot)); } public void RestoreGroup(DocumentGroup group) { + ThreadHelper.ThrowIfNotOnUIThread(); if (group == null) { return; @@ -228,13 +247,15 @@ public void RestoreGroup(DocumentGroup group) SaveUndoGroup(); } - Package.Environment.Documents.CloseAll(); + if (windows.Any()) + windows.CloseAll(); OpenGroup(group); } public void OpenGroup(int slot) { + ThreadHelper.ThrowIfNotOnUIThread(); OpenGroup(Groups.FindBySlot(slot)); } @@ -247,6 +268,7 @@ public void OpenGroup(DocumentGroup group) using (var stream = new VsOleStream()) { + ThreadHelper.ThrowIfNotOnUIThread(); stream.Write(group.Positions, 0, group.Positions.Length); stream.Seek(0, SeekOrigin.Begin); @@ -265,6 +287,7 @@ public void CloseGroup(DocumentGroup group) return; } + ThreadHelper.ThrowIfNotOnUIThread(); var documents = from d in Package.Environment.GetDocuments() where @group.Files.Contains(d.FullName) select d; @@ -333,6 +356,7 @@ public void RemoveGroup(DocumentGroup group, bool confirm = true) private void SaveUndoGroup() { + ThreadHelper.ThrowIfNotOnUIThread(); SaveGroup(UndoGroupName); } @@ -362,16 +386,19 @@ private void SaveUndoGroup(DocumentGroup group) public void SaveStashGroup() { + ThreadHelper.ThrowIfNotOnUIThread(); SaveGroup(StashGroupName); } public void OpenStashGroup() { + ThreadHelper.ThrowIfNotOnUIThread(); OpenGroup(Groups.FindByName(StashGroupName)); } public void RestoreStashGroup() { + ThreadHelper.ThrowIfNotOnUIThread(); RestoreGroup(Groups.FindByName(StashGroupName)); } @@ -401,31 +428,49 @@ public void RestoreStashGroup() private List LoadGroupsForSolution() { + ThreadHelper.ThrowIfNotOnUIThread(); var solution = SolutionName; - if (!string.IsNullOrWhiteSpace(solution)) - { - try - { - var settingsMgr = new ShellSettingsManager(ServiceProvider); - var store = settingsMgr.GetReadOnlySettingsStore(SettingsScope.UserSettings); + List groups = new List(); + if (string.IsNullOrWhiteSpace(solution)) { + return groups; + } + + try { + var settingsMgr = new ShellSettingsManager(ServiceProvider); + var store = settingsMgr.GetReadOnlySettingsStore(SettingsScope.UserSettings); + + string projectKeyHash = GetHashString(solution); + string projectGroupsKey = string.Format(ProjectGroupsKeyPlaceholder, projectKeyHash); + if (!store.CollectionExists(projectGroupsKey)) + { + // try load tabs from older versions var propertyName = String.Format(SavedTabsStoragePropertyFormat, solution); if (store.PropertyExists(StorageCollectionPath, propertyName)) { var tabs = store.GetString(StorageCollectionPath, propertyName); - return JsonConvert.DeserializeObject>(tabs); + groups = JsonConvert.DeserializeObject>(tabs); } + + return groups; } - catch (Exception ex) + + var groupProperties = store.GetPropertyNamesAndValues(projectGroupsKey); + foreach (var groupProperty in groupProperties) { - Debug.Assert(false, nameof(LoadGroupsForSolution), ex.ToString()); + DocumentGroup group = JsonConvert.DeserializeObject(groupProperty.Value.ToString()); + groups.Add(group); } + } catch (Exception ex) { + Debug.Assert(false, nameof(LoadGroupsForSolution), ex.ToString()); + } + + return groups; } - return new List(); - } private void SaveGroupsForSolution(IList groups = null) { + ThreadHelper.ThrowIfNotOnUIThread(); var solution = SolutionName; if (string.IsNullOrWhiteSpace(solution)) { @@ -440,20 +485,36 @@ private void SaveGroupsForSolution(IList groups = null) var settingsMgr = new ShellSettingsManager(ServiceProvider); var store = settingsMgr.GetWritableSettingsStore(SettingsScope.UserSettings); - if (!store.CollectionExists(StorageCollectionPath)) + string projectKeyHash = GetHashString(solution); + string projectGroupsKey = string.Format(ProjectGroupsKeyPlaceholder, projectKeyHash); + + if (store.CollectionExists(projectGroupsKey)) { - store.CreateCollection(StorageCollectionPath); + store.DeleteProperty(StorageCollectionPath, projectGroupsKey); } + store.CreateCollection(projectGroupsKey); - var propertyName = String.Format(SavedTabsStoragePropertyFormat, solution); - if (!groups.Any()) + foreach (DocumentGroup group in groups) { - store.DeleteProperty(StorageCollectionPath, propertyName); - return; + var serializedGroup = JsonConvert.SerializeObject(group); + store.SetString(projectGroupsKey, group.Name, serializedGroup); } + } - var tabs = JsonConvert.SerializeObject(groups); - store.SetString(StorageCollectionPath, propertyName, tabs); + private static string GetHashString(string source) + { + using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) + { + byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(source); + byte[] hashBytes = md5.ComputeHash(inputBytes); + // Convert the byte array to hexadecimal string + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hashBytes.Length; i++) + { + sb.Append(hashBytes [i].ToString("X2")); + } + return sb.ToString(); + } } public static bool IsStashGroup(string name) diff --git a/src/SaveAllTheTabs/Polyfills/Extensions.cs b/src/SaveAllTheTabs/Polyfills/Extensions.cs index a771543..47049d1 100644 --- a/src/SaveAllTheTabs/Polyfills/Extensions.cs +++ b/src/SaveAllTheTabs/Polyfills/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Windows.Controls; @@ -33,11 +34,13 @@ public static Document GetActiveDocument(this DTE2 environment) public static IEnumerable GetDocumentFiles(this DTE2 environment) { + ThreadHelper.ThrowIfNotOnUIThread(); return from d in environment.GetDocuments() select GetExactPathName(d.FullName); } public static IEnumerable GetDocuments(this DTE2 environment) { + ThreadHelper.ThrowIfNotOnUIThread(); return from w in environment.GetDocumentWindows() where w.Document != null select w.Document; } @@ -49,6 +52,7 @@ public static IEnumerable GetDocumentWindows(this DTE2 environment) { try { + ThreadHelper.ThrowIfNotOnUIThread(); return !w.Linkable; } catch (ObjectDisposedException) @@ -60,16 +64,22 @@ public static IEnumerable GetDocumentWindows(this DTE2 environment) public static IEnumerable GetBreakpoints(this DTE2 environment) { + ThreadHelper.ThrowIfNotOnUIThread(); return environment.Debugger.Breakpoints.Cast(); } public static IEnumerable GetMatchingBreakpoints(this DTE2 environment, HashSet files) { - return environment.Debugger.Breakpoints.Cast().Where(bp => files.Contains(bp.File)); + ThreadHelper.ThrowIfNotOnUIThread(); + return environment.Debugger.Breakpoints.Cast().Where(bp => { + ThreadHelper.ThrowIfNotOnUIThread(); + return files.Contains(bp.File); + }); } public static void CloseAll(this IEnumerable windows, vsSaveChanges saveChanges = vsSaveChanges.vsSaveChangesPrompt) { + ThreadHelper.ThrowIfNotOnUIThread(); foreach (var w in windows) { w.Close(saveChanges); @@ -78,6 +88,7 @@ public static void CloseAll(this IEnumerable windows, vsSaveChanges save public static void CloseAll(this IEnumerable documents, vsSaveChanges saveChanges = vsSaveChanges.vsSaveChangesPrompt) { + ThreadHelper.ThrowIfNotOnUIThread(); foreach (var d in documents) { d.Close(saveChanges); @@ -86,6 +97,7 @@ public static void CloseAll(this IEnumerable documents, vsSaveChanges public static Command GetCommand(this DTE2 environment, OleMenuCommand command) { + ThreadHelper.ThrowIfNotOnUIThread(); return environment.Commands.Item(command.CommandID.Guid, command.CommandID.ID); } @@ -96,13 +108,20 @@ public static object[] GetKeyBindings(this DTE2 environment, OleMenuCommand comm public static void SetKeyBindings(this DTE2 environment, OleMenuCommand command, params object[] bindings) { + ThreadHelper.ThrowIfNotOnUIThread(); var dteCommand = environment.GetCommand(command); if (dteCommand == null) { return; } - dteCommand.Bindings = bindings; + try + { + dteCommand.Bindings = bindings; + } catch (System.Runtime.InteropServices.COMException ex) + { + Debug.Assert(false, nameof(SetKeyBindings), ex.ToString()); + } } public static void SetKeyBindings(this DTE2 environment, OleMenuCommand command, IEnumerable bindings) diff --git a/src/SaveAllTheTabs/Polyfills/Interop.cs b/src/SaveAllTheTabs/Polyfills/Interop.cs index 1b8442c..2cb4a30 100644 --- a/src/SaveAllTheTabs/Polyfills/Interop.cs +++ b/src/SaveAllTheTabs/Polyfills/Interop.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Runtime.InteropServices; using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell; namespace SaveAllTheTabs.Polyfills { @@ -75,11 +76,13 @@ void IStream.Clone(out IStream ppstm) void ISequentialStream.Read(byte[] pv, uint cb, out uint pcbRead) { + ThreadHelper.ThrowIfNotOnUIThread(); ((IStream)this).Read(pv, cb, out pcbRead); } void ISequentialStream.Write(byte[] pv, uint cb, out uint pcbWritten) { + ThreadHelper.ThrowIfNotOnUIThread(); ((IStream)this).Write(pv, cb, out pcbWritten); } } diff --git a/src/SaveAllTheTabs/SaveAllTheTabs.csproj b/src/SaveAllTheTabs/SaveAllTheTabs.csproj index 8d90030..e8d1cfd 100644 --- a/src/SaveAllTheTabs/SaveAllTheTabs.csproj +++ b/src/SaveAllTheTabs/SaveAllTheTabs.csproj @@ -26,6 +26,7 @@ false false true + false @@ -45,7 +46,7 @@ Properties SaveAllTheTabs SaveAllTheTabs - v4.5.2 + v4.8 true true true @@ -96,25 +97,24 @@ + + True + True + source.extension.vsixmanifest + Always true - - Designer - Designer + VsixManifestGenerator + source.extension.cs - - False - ..\packages\VSSDK.DTE.7.0.4\lib\net20\envdte.dll - True - False @@ -125,101 +125,14 @@ False - - ..\packages\VSSDK.GraphModel.12.0.4\lib\net45\Microsoft.VisualStudio.GraphModel.dll - True - - - ..\packages\VSSDK.OLE.Interop.7.0.4\lib\net20\Microsoft.VisualStudio.OLE.Interop.dll - True - - - ..\packages\VSSDK.Shell.12.12.0.4\lib\net45\Microsoft.VisualStudio.Shell.12.0.dll - True - - - ..\packages\VSSDK.Shell.Immutable.10.10.0.4\lib\net40\Microsoft.VisualStudio.Shell.Immutable.10.0.dll - True - - - ..\packages\VSSDK.Shell.Immutable.11.11.0.4\lib\net45\Microsoft.VisualStudio.Shell.Immutable.11.0.dll - True - - - ..\packages\VSSDK.Shell.Immutable.12.12.0.4\lib\net45\Microsoft.VisualStudio.Shell.Immutable.12.0.dll - True - - - ..\packages\VSSDK.Shell.Interop.7.0.4\lib\net20\Microsoft.VisualStudio.Shell.Interop.dll - True - - - False - ..\packages\VSSDK.Shell.Interop.10.10.0.4\lib\net20\Microsoft.VisualStudio.Shell.Interop.10.0.dll - True - - - False - ..\packages\VSSDK.Shell.Interop.11.11.0.4\lib\net20\Microsoft.VisualStudio.Shell.Interop.11.0.dll - True - - - ..\packages\VSSDK.Shell.Interop.8.8.0.4\lib\net20\Microsoft.VisualStudio.Shell.Interop.8.0.dll - True - - - ..\packages\VSSDK.Shell.Interop.9.9.0.4\lib\net20\Microsoft.VisualStudio.Shell.Interop.9.0.dll - True - - - ..\packages\VSSDK.TextManager.Interop.7.0.4\lib\net20\Microsoft.VisualStudio.TextManager.Interop.dll - True - - - ..\packages\VSSDK.TextManager.Interop.8.8.0.4\lib\net20\Microsoft.VisualStudio.TextManager.Interop.8.0.dll - True - - - ..\packages\VSSDK.Threading.12.0.4\lib\net45\Microsoft.VisualStudio.Threading.dll - True - - - ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True - - - False - ..\packages\VSSDK.DTE.7.0.4\lib\net20\stdole.dll - True - - - ..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll - True - - - ..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll - True - - - ..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll - True - - - ..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll - True - - - ..\packages\Rx-Xaml.2.2.5\lib\net45\System.Reactive.Windows.Threading.dll - True - @@ -274,15 +187,28 @@ false + + + 8.0.2 + + + 14.1.37 + + + 16.5.29911.84 + + + 12.0.3 + + + 2.2.5 + + + 11.0.4 + + - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - -