From 0f985e0a5163c6b5d700e0ce3a66ceca895a8cf8 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Mon, 15 Feb 2021 15:33:49 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=81=20When=20adding=20new=20file,=20se?= =?UTF-8?q?t=20as=20startup=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We previously generated one entry in launchSettings.json for each top-level file. This was problematic because now we had to find a way to automatically select the right entry whenever the active document changed. This was not possible using VS extensibility APIs as-is, so a better and simpler approach is to just generate ONE entry in the file, which matches the current active document. Also, we update the .user with the ActiveDebugProfile at the same time. This avoids some timing issues we had in the past, and simplifies further. This means we now don't really need source generator capability anymore, since we do everything from MSBuild now. We'll still check for the right C# version since otherwise folks could install the package on lower versions of VS and expect it to work (which it wouldn't). Fixes #9. --- SmallSharp.sln | 17 +- src/SmallSharp.Build/MonitorActiveDocument.cs | 59 ------- src/SmallSharp.Build/OpenStartupFile.cs | 34 ---- src/SmallSharp.Build/SmallSharp.Build.csproj | 25 --- .../ActiveDocumentMonitor.cs | 154 +++++++++--------- .../AnalyzerConfigOptionsExtensions.cs | 16 -- src/SmallSharp/DebuggerExtension.cs | 23 --- .../DisposableAction.cs | 0 src/SmallSharp/LaunchSettingsGenerator.cs | 47 ------ src/SmallSharp/MonitorActiveDocument.cs | 70 ++++++++ .../NativeMethods.cs | 16 ++ src/SmallSharp/SmallSharp.csproj | 20 ++- .../SmallSharp.targets | 85 ++++------ .../WindowsInterop.cs | 33 ---- 14 files changed, 219 insertions(+), 380 deletions(-) delete mode 100644 src/SmallSharp.Build/MonitorActiveDocument.cs delete mode 100644 src/SmallSharp.Build/OpenStartupFile.cs delete mode 100644 src/SmallSharp.Build/SmallSharp.Build.csproj rename src/{SmallSharp.Build => SmallSharp}/ActiveDocumentMonitor.cs (56%) delete mode 100644 src/SmallSharp/AnalyzerConfigOptionsExtensions.cs delete mode 100644 src/SmallSharp/DebuggerExtension.cs rename src/{SmallSharp.Build => SmallSharp}/DisposableAction.cs (100%) delete mode 100644 src/SmallSharp/LaunchSettingsGenerator.cs create mode 100644 src/SmallSharp/MonitorActiveDocument.cs rename src/{SmallSharp.Build => SmallSharp}/NativeMethods.cs (88%) rename src/{SmallSharp.Build => SmallSharp}/SmallSharp.targets (61%) rename src/{SmallSharp.Build => SmallSharp}/WindowsInterop.cs (86%) diff --git a/SmallSharp.sln b/SmallSharp.sln index acae116..6640e99 100644 --- a/SmallSharp.sln +++ b/SmallSharp.sln @@ -3,11 +3,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30516.212 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmallSharp", "src\SmallSharp\SmallSharp.csproj", "{F87C7A13-669C-4F18-9266-B256F254DFA3}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5420D663-3EA6-419B-8F73-C7EA374CFBBE}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .netconfig = .netconfig .github\dependabot.yml = .github\dependabot.yml src\Directory.Build.props = src\Directory.Build.props src\Directory.Build.targets = src\Directory.Build.targets @@ -27,7 +26,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\sponsors.yml = .github\workflows\sponsors.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmallSharp.Build", "src\SmallSharp.Build\SmallSharp.Build.csproj", "{62834B0C-A2C2-4449-9E2A-00CC390A79BE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmallSharp", "src\SmallSharp\SmallSharp.csproj", "{97648980-AA30-4AC0-B8E9-FCF6359F56A0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -35,14 +34,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F87C7A13-669C-4F18-9266-B256F254DFA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F87C7A13-669C-4F18-9266-B256F254DFA3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F87C7A13-669C-4F18-9266-B256F254DFA3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F87C7A13-669C-4F18-9266-B256F254DFA3}.Release|Any CPU.Build.0 = Release|Any CPU - {62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Release|Any CPU.Build.0 = Release|Any CPU + {97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SmallSharp.Build/MonitorActiveDocument.cs b/src/SmallSharp.Build/MonitorActiveDocument.cs deleted file mode 100644 index eb8085d..0000000 --- a/src/SmallSharp.Build/MonitorActiveDocument.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Diagnostics; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace SmallSharp.Build -{ - public class MonitorActiveDocument : Task - { - [Required] - public string? FlagFile { get; set; } - - [Required] - public string? LaunchProfiles { get; set; } - - [Required] - public string? UserFile { get; set; } - - public override bool Execute() - { - if (LaunchProfiles == null || UserFile == null || FlagFile == null) - return true; - - try - { - if (BuildEngine4.GetRegisteredTaskObject(nameof(ActiveDocumentMonitor), RegisteredTaskObjectLifetime.AppDomain) is not ActiveDocumentMonitor monitor) - { - if (WindowsInterop.GetServiceProvider() is IServiceProvider services) - { - var documentMonitor = new ActiveDocumentMonitor(LaunchProfiles, UserFile, FlagFile, services); - - BuildEngine4.RegisterTaskObject(nameof(ActiveDocumentMonitor), documentMonitor, - RegisteredTaskObjectLifetime.AppDomain, false); - - // Start monitoring at the end of the build, to avoid slowing down the DTB - BuildEngine4.RegisterTaskObject("StartMonitor", - new DisposableAction(() => documentMonitor.Start()), - RegisteredTaskObjectLifetime.Build, false); - } - else - { - Debug.Fail("Failed to get IServiceProvider to monitor for active document."); - } - } - else - { - // NOTE: this means we only support ONE project/launchProfiles per IDE. - monitor.Refresh(LaunchProfiles, UserFile, FlagFile); - } - } - catch (Exception e) - { - Log.LogWarning($"Failed to start active document monitoring: {e}"); - } - - return true; - } - } -} diff --git a/src/SmallSharp.Build/OpenStartupFile.cs b/src/SmallSharp.Build/OpenStartupFile.cs deleted file mode 100644 index c14e796..0000000 --- a/src/SmallSharp.Build/OpenStartupFile.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.IO; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace SmallSharp.Build -{ - public class OpenStartupFile : Task - { - [Required] - public string? FlagFile { get; set; } - - public string? StartupFile { get; set; } - - public override bool Execute() - { - if (string.IsNullOrEmpty(FlagFile) || string.IsNullOrEmpty(StartupFile)) - return true; - - if (!File.Exists(FlagFile) || - File.ReadAllText(FlagFile) != StartupFile) - { - // This defers the opening until the build completes. - BuildEngine4.RegisterTaskObject( - StartupFile, - new DisposableAction(() => WindowsInterop.EnsureOpened(StartupFile!)), - RegisteredTaskObjectLifetime.Build, false); - - File.WriteAllText(FlagFile, StartupFile); - } - - return true; - } - } -} diff --git a/src/SmallSharp.Build/SmallSharp.Build.csproj b/src/SmallSharp.Build/SmallSharp.Build.csproj deleted file mode 100644 index c02eae2..0000000 --- a/src/SmallSharp.Build/SmallSharp.Build.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net472 - build\netstandard2.0 - true - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SmallSharp.Build/ActiveDocumentMonitor.cs b/src/SmallSharp/ActiveDocumentMonitor.cs similarity index 56% rename from src/SmallSharp.Build/ActiveDocumentMonitor.cs rename to src/SmallSharp/ActiveDocumentMonitor.cs index d51e9e4..be8a94e 100644 --- a/src/SmallSharp.Build/ActiveDocumentMonitor.cs +++ b/src/SmallSharp/ActiveDocumentMonitor.cs @@ -6,121 +6,104 @@ using System.Threading; using System.Xml.Linq; using Microsoft.VisualStudio.Shell.Interop; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace SmallSharp.Build { - class ActiveDocumentMonitor : MarshalByRefObject, IDisposable, IVsRunningDocTableEvents, IVsSelectionEvents + class ActiveDocumentMonitor : MarshalByRefObject, IDisposable, IVsRunningDocTableEvents, IVsSelectionEvents, IVsSolutionEvents { - FileSystemWatcher watcher; - readonly IServiceProvider services; - + IVsSolution? solution; IVsRunningDocumentTable? rdt; IVsMonitorSelection? selection; + + uint solutionCookie; uint rdtCookie; uint selectionCookie; string launchProfilesPath; string userFile; - string flagFile; - Dictionary startupFiles = new(); + Dictionary startupFiles; + + string? activeFile; - public ActiveDocumentMonitor(string launchProfilesPath, string userFile, string flagFile, IServiceProvider services) + public ActiveDocumentMonitor(string launchProfilesPath, string userFile, + string[] startupFiles, IServiceProvider services) { this.launchProfilesPath = launchProfilesPath; this.userFile = userFile; - this.flagFile = flagFile; - this.services = services; - - watcher = new FileSystemWatcher(Path.GetDirectoryName(launchProfilesPath)) - { - NotifyFilter = NotifyFilters.LastWrite, - Filter = "launchSettings.json", - }; - - watcher.Changed += (_, _) => ReloadProfiles(); - watcher.Created += (_, _) => ReloadProfiles(); - watcher.EnableRaisingEvents = true; - ReloadProfiles(); - } + this.startupFiles = startupFiles.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase); - public void Start() - { + solution = (IVsSolution)services.GetService(typeof(SVsSolution)); rdt = (IVsRunningDocumentTable)services.GetService(typeof(SVsRunningDocumentTable)); - if (rdt != null) - rdt.AdviseRunningDocTableEvents(this, out rdtCookie); - selection = (IVsMonitorSelection)services.GetService(typeof(SVsShellMonitorSelection)); - if (selection != null) - selection.AdviseSelectionEvents(this, out selectionCookie); + + EnsureMonitoring(); } - public void Refresh(string launchProfiles, string userFile, string flagFile) + public void Refresh(string launchProfiles, string userFile, string[] startupFiles) { launchProfilesPath = launchProfiles; this.userFile = userFile; - this.flagFile = flagFile; - watcher.Path = Path.GetDirectoryName(launchProfiles); - ReloadProfiles(); - } + this.startupFiles = startupFiles.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase); - void ReloadProfiles() - { - if (!File.Exists(launchProfilesPath)) - return; - - var maxAttempts = 5; - var exceptions = new List(); + EnsureMonitoring(); - for (var i = 0; i < maxAttempts; i++) - { - try - { - var json = JObject.Parse(File.ReadAllText(launchProfilesPath)); - if (json.Property("profiles") is not JProperty prop || - prop.Value is not JObject profiles) - return; + // For new files, we get the update before the new item is added to + // msbuild top-level files, so we retry on refresh + UpdateStartupFile(activeFile); + } - startupFiles = profiles.Properties().Select(p => p.Name) - .ToDictionary(x => x, StringComparer.OrdinalIgnoreCase); + void EnsureMonitoring() + { + if (solutionCookie == 0 && solution != null) + solution.AdviseSolutionEvents(this, out solutionCookie); - return; - } - catch (Exception e) - { - exceptions.Add(e); - Thread.Sleep(500); - } - } + if (rdtCookie == 0 && rdt != null) + rdt.AdviseRunningDocTableEvents(this, out rdtCookie); - // NOTE: check exceptions list to see why. - Debug.Fail("Could not read launchSettings.json"); + if (selectionCookie == 0 && selection != null) + selection.AdviseSelectionEvents(this, out selectionCookie); } void UpdateStartupFile(string? path) { + activeFile = path; + if (!string.IsNullOrEmpty(path) && path!.IndexOfAny(Path.GetInvalidPathChars()) == -1 && - Path.GetFileName(path) is string startupFile && - startupFiles.ContainsKey(startupFile)) + startupFiles.TryGetValue(Path.GetFileName(path), out var startupFile)) { + var settings = new JObject( + new JProperty("profiles", new JObject( + new JProperty(startupFile, new JObject( + new JProperty("commandName", "Project") + )) + )) + ); + + var json = settings.ToString(Formatting.Indented); + + // Only write if different content. + if (File.Exists(launchProfilesPath) && + File.ReadAllText(launchProfilesPath) == json) + return; + + File.WriteAllText(launchProfilesPath, json); + try { // Get the value as it was exists in the original dictionary, // since it has to match what the source generator created in the // launch profiles. - startupFile = startupFiles[startupFile]; var xdoc = XDocument.Load(userFile); var active = xdoc .Descendants("{http://schemas.microsoft.com/developer/msbuild/2003}ActiveDebugProfile") .FirstOrDefault(); - if (active != null && active.Value != startupFile) + if (active != null && !startupFile.Equals(active.Value, StringComparison.OrdinalIgnoreCase)) { active.Value = startupFile; - // First save to flag file so we don't cause another open - // attempt via the OpenStartupFile task. - File.WriteAllText(flagFile, startupFile); xdoc.Save(userFile); } } @@ -131,15 +114,22 @@ void UpdateStartupFile(string? path) } } - void IDisposable.Dispose() + public void Dispose() { + if (solutionCookie != 0 && solution != null) + Try(() => solution.UnadviseSolutionEvents(solutionCookie)); + + solutionCookie = 0; + if (rdtCookie != 0 && rdt != null) Try(() => rdt.UnadviseRunningDocTableEvents(rdtCookie)); + rdtCookie = 0; + if (selectionCookie != 0 && selection != null) Try(() => selection.UnadviseSelectionEvents(selectionCookie)); - watcher.Dispose(); + selectionCookie = 0; } void Try(Action action) @@ -176,18 +166,32 @@ int IVsSelectionEvents.OnSelectionChanged(IVsHierarchy pHierOld, uint itemidOld, return 0; } - int IVsRunningDocTableEvents.OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0; + int IVsSolutionEvents.OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) + { + Dispose(); + return 0; + } - int IVsRunningDocTableEvents.OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0; + int IVsSolutionEvents.OnBeforeCloseSolution(object pUnkReserved) + { + Dispose(); + return 0; + } + int IVsRunningDocTableEvents.OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0; + int IVsRunningDocTableEvents.OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0; int IVsRunningDocTableEvents.OnAfterSave(uint docCookie) => 0; - int IVsRunningDocTableEvents.OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame) => 0; - int IVsRunningDocTableEvents.OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame) => 0; - int IVsSelectionEvents.OnElementValueChanged(uint elementid, object varValueOld, object varValueNew) => 0; - int IVsSelectionEvents.OnCmdUIContextChanged(uint dwCmdUICookie, int fActive) => 0; + int IVsSolutionEvents.OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) => throw new NotImplementedException(); + int IVsSolutionEvents.OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) => throw new NotImplementedException(); + int IVsSolutionEvents.OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) => throw new NotImplementedException(); + int IVsSolutionEvents.OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) => throw new NotImplementedException(); + int IVsSolutionEvents.OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) => throw new NotImplementedException(); + int IVsSolutionEvents.OnAfterOpenSolution(object pUnkReserved, int fNewSolution) => throw new NotImplementedException(); + int IVsSolutionEvents.OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) => throw new NotImplementedException(); + int IVsSolutionEvents.OnAfterCloseSolution(object pUnkReserved) => throw new NotImplementedException(); } } diff --git a/src/SmallSharp/AnalyzerConfigOptionsExtensions.cs b/src/SmallSharp/AnalyzerConfigOptionsExtensions.cs deleted file mode 100644 index 4875954..0000000 --- a/src/SmallSharp/AnalyzerConfigOptionsExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.CodeAnalysis.Diagnostics; - -namespace SmallSharp -{ - static class AnalyzerConfigOptionsExtensions - { - public static bool IsEnabled(this AnalyzerOptions options, string optionName) - => IsEnabled(options.AnalyzerConfigOptionsProvider.GlobalOptions, optionName); - - public static bool IsEnabled(this AnalyzerConfigOptionsProvider options, string optionName) - => IsEnabled(options.GlobalOptions, optionName); - - public static bool IsEnabled(this AnalyzerConfigOptions options, string optionName) - => options.TryGetValue("build_property." + optionName, out var value) && bool.TryParse(value, out var enabled) && enabled; - } -} diff --git a/src/SmallSharp/DebuggerExtension.cs b/src/SmallSharp/DebuggerExtension.cs deleted file mode 100644 index bac4ca8..0000000 --- a/src/SmallSharp/DebuggerExtension.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace SmallSharp -{ - static class DebuggerExtension - { - //[Conditional("DEBUG")] - public static void CheckDebugger(this GeneratorExecutionContext context, string generatorName = nameof(SmallSharp)) - => context.AnalyzerConfigOptions.CheckDebugger(generatorName); - - public static void CheckDebugger(this AnalyzerConfigOptionsProvider provider, string generatorName = nameof(SmallSharp)) - { - if (Process.GetCurrentProcess().ProcessName == "devenv") - return; - - if (provider.IsEnabled("DebugSourceGenerators") || - provider.IsEnabled("Debug" + generatorName)) - Debugger.Launch(); - } - } -} diff --git a/src/SmallSharp.Build/DisposableAction.cs b/src/SmallSharp/DisposableAction.cs similarity index 100% rename from src/SmallSharp.Build/DisposableAction.cs rename to src/SmallSharp/DisposableAction.cs diff --git a/src/SmallSharp/LaunchSettingsGenerator.cs b/src/SmallSharp/LaunchSettingsGenerator.cs deleted file mode 100644 index 51e3e04..0000000 --- a/src/SmallSharp/LaunchSettingsGenerator.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.IO; -using System.Linq; -using Microsoft.CodeAnalysis; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace SmallSharp -{ - [Generator] - public class LaunchSettingsGenerator : ISourceGenerator - { - public void Initialize(GeneratorInitializationContext context) { } - - public void Execute(GeneratorExecutionContext context) - { - context.CheckDebugger(); - - var documents = from additional in context.AdditionalFiles - let options = context.AnalyzerConfigOptions.GetOptions(additional) - let compile = options.TryGetValue("build_metadata.AdditionalFiles.SourceItemType", out var itemType) && itemType == "Compile" - where compile - select additional.Path; - - var settings = new JObject( - new JProperty("profiles", new JObject( - documents.OrderBy(path => Path.GetFileName(path)).Select(path => new JProperty(Path.GetFileName(path), new JObject( - new JProperty("commandName", "Project") - ))) - )) - ); - - if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var directory)) - { - Directory.CreateDirectory(Path.Combine(directory, "Properties")); - var filePath = Path.Combine(directory, "Properties", "launchSettings.json"); - var json = settings.ToString(Formatting.Indented); - - // Only write if different content. - if (File.Exists(filePath) && - File.ReadAllText(filePath) == json) - return; - - File.WriteAllText(filePath, json); - } - } - } -} diff --git a/src/SmallSharp/MonitorActiveDocument.cs b/src/SmallSharp/MonitorActiveDocument.cs new file mode 100644 index 0000000..e056bd3 --- /dev/null +++ b/src/SmallSharp/MonitorActiveDocument.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace SmallSharp.Build +{ + public class MonitorActiveDocument : Task + { + [Required] + public string? LaunchProfiles { get; set; } + + [Required] + public string? UserFile { get; set; } + + [Required] + public ITaskItem[] StartupFiles { get; set; } = Array.Empty(); + + public override bool Execute() + { + if (string.IsNullOrEmpty(LaunchProfiles) || + string.IsNullOrEmpty(UserFile)) + { + Debug.Fail("Should have gotten something to monitor"); + return true; + } + + try + { + if (BuildEngine4.GetRegisteredTaskObject(nameof(ActiveDocumentMonitor), RegisteredTaskObjectLifetime.AppDomain) is not ActiveDocumentMonitor monitor) + { + var maxAttempts = 5; + for (var i = 0; i < maxAttempts; i++) + { + if (WindowsInterop.GetServiceProvider() is IServiceProvider services) + { + BuildEngine4.RegisterTaskObject(nameof(ActiveDocumentMonitor), + new ActiveDocumentMonitor(LaunchProfiles!, UserFile!, + StartupFiles.Select(x => x.ItemSpec).ToArray(), services), + RegisteredTaskObjectLifetime.AppDomain, false); + + return true; + } + else + { + BuildEngine4.Yield(); + Thread.Sleep(200); + } + } + + Debug.Fail("Failed to get IServiceProvider to monitor for active document."); + } + else + { + // NOTE: this means we only support ONE project/launchProfiles per IDE. + monitor.Refresh(LaunchProfiles!, UserFile!, + StartupFiles.Select(x => x.ItemSpec).ToArray()); + } + } + catch (Exception e) + { + Log.LogWarning($"Failed to start active document monitoring: {e}"); + } + + return true; + } + } +} diff --git a/src/SmallSharp.Build/NativeMethods.cs b/src/SmallSharp/NativeMethods.cs similarity index 88% rename from src/SmallSharp.Build/NativeMethods.cs rename to src/SmallSharp/NativeMethods.cs index 4b645b3..1ac2ba9 100644 --- a/src/SmallSharp.Build/NativeMethods.cs +++ b/src/SmallSharp/NativeMethods.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; +using System.Text; internal static class NativeMethods { @@ -15,6 +16,21 @@ internal static class NativeMethods public static readonly Guid IID_IObjectWithSite = typeof(Microsoft.VisualStudio.OLE.Interop.IObjectWithSite).GUID; public static Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046"); + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + internal static extern int GetShortPathName( + [MarshalAs(UnmanagedType.LPTStr)] + string path, + [MarshalAs(UnmanagedType.LPTStr)] + StringBuilder shortPath, + int shortPathLength); + + internal static string GetShortPath(string path) + { + var shortPath = new StringBuilder(MAX_PATH); + GetShortPathName(path, shortPath, MAX_PATH); + return shortPath.ToString(); + } + [DllImport("ole32.dll")] internal static extern int CoRegisterMessageFilter(IMessageFilter lpMessageFilter, out IMessageFilter lplpMessageFilter); [DllImport("ole32.dll")] diff --git a/src/SmallSharp/SmallSharp.csproj b/src/SmallSharp/SmallSharp.csproj index 016c170..2aac7af 100644 --- a/src/SmallSharp/SmallSharp.csproj +++ b/src/SmallSharp/SmallSharp.csproj @@ -1,26 +1,32 @@  - netstandard2.0 + net472 SmallSharp Create, edit and run multiple C# 9.0 top-level programs in the same project 😍 - analyzers\dotnet\cs + build\netstandard2.0 + true + - + + + + + - + + - - + - + \ No newline at end of file diff --git a/src/SmallSharp.Build/SmallSharp.targets b/src/SmallSharp/SmallSharp.targets similarity index 61% rename from src/SmallSharp.Build/SmallSharp.targets rename to src/SmallSharp/SmallSharp.targets index d8023db..142c723 100644 --- a/src/SmallSharp.Build/SmallSharp.targets +++ b/src/SmallSharp/SmallSharp.targets @@ -1,23 +1,23 @@ - - + $(MSBuildVersion.TrimEnd('0123456789').TrimEnd('.')) + + *$(DefaultLanguageSourceExtension) + $(ActiveDebugProfile) + CollectStartupFile;SelectStartupFile;SelectTopLevelCompile - - - - - - + + + @@ -27,23 +27,38 @@ - + + + + + + + - - - + - + + + + + + + + + true - - + %(ReversedCompile.Identity) @@ -117,7 +132,7 @@ @@ -126,39 +141,9 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/SmallSharp.Build/WindowsInterop.cs b/src/SmallSharp/WindowsInterop.cs similarity index 86% rename from src/SmallSharp.Build/WindowsInterop.cs rename to src/SmallSharp/WindowsInterop.cs index b727acc..734dadb 100644 --- a/src/SmallSharp.Build/WindowsInterop.cs +++ b/src/SmallSharp/WindowsInterop.cs @@ -14,39 +14,6 @@ static class WindowsInterop { static readonly Regex versionExpr = new Regex(@"Microsoft Visual Studio (?\d\d\.\d)", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - public static void EnsureOpened(string filePath, TimeSpan delay = default) - { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) - return; - - if (delay != default) - Thread.Sleep(delay); - - var dte = GetDTE(); - if (dte == null) - return; - - var maxAttempts = 5; - var exceptions = new List(); - - for (var i = 0; i < maxAttempts; i++) - { - try - { - dte.ExecuteCommand("File.OpenFile", filePath); - return; - } - catch (Exception e) - { - exceptions.Add(e); - Thread.Sleep(500); - } - } - - // NOTE: inspect exceptions variable - Debug.Fail($"Failed to open {filePath} after 5 attempts."); - } - public static IServiceProvider? GetServiceProvider(TimeSpan delay = default) { if (Environment.OSVersion.Platform != PlatformID.Win32NT)