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