Skip to content

Commit

Permalink
🗁 When adding new file, set as startup file
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kzu committed Feb 15, 2021
1 parent e8a4c28 commit 0f985e0
Show file tree
Hide file tree
Showing 14 changed files with 219 additions and 380 deletions.
17 changes: 6 additions & 11 deletions SmallSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,22 +26,18 @@ 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
Debug|Any CPU = Debug|Any CPU
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
Expand Down
59 changes: 0 additions & 59 deletions src/SmallSharp.Build/MonitorActiveDocument.cs

This file was deleted.

34 changes: 0 additions & 34 deletions src/SmallSharp.Build/OpenStartupFile.cs

This file was deleted.

25 changes: 0 additions & 25 deletions src/SmallSharp.Build/SmallSharp.Build.csproj

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> startupFiles = new();
Dictionary<string, string> 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<Exception>();
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);
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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();
}
}
Loading

0 comments on commit 0f985e0

Please sign in to comment.