Skip to content

Commit 0f985e0

Browse files
committed
🗁 When adding new file, set as startup file
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.
1 parent e8a4c28 commit 0f985e0

14 files changed

+219
-380
lines changed

SmallSharp.sln

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 16
44
VisualStudioVersion = 16.0.30516.212
55
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmallSharp", "src\SmallSharp\SmallSharp.csproj", "{F87C7A13-669C-4F18-9266-B256F254DFA3}"
7-
EndProject
86
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5420D663-3EA6-419B-8F73-C7EA374CFBBE}"
97
ProjectSection(SolutionItems) = preProject
108
.editorconfig = .editorconfig
9+
.netconfig = .netconfig
1110
.github\dependabot.yml = .github\dependabot.yml
1211
src\Directory.Build.props = src\Directory.Build.props
1312
src\Directory.Build.targets = src\Directory.Build.targets
@@ -27,22 +26,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
2726
.github\workflows\sponsors.yml = .github\workflows\sponsors.yml
2827
EndProjectSection
2928
EndProject
30-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmallSharp.Build", "src\SmallSharp.Build\SmallSharp.Build.csproj", "{62834B0C-A2C2-4449-9E2A-00CC390A79BE}"
29+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmallSharp", "src\SmallSharp\SmallSharp.csproj", "{97648980-AA30-4AC0-B8E9-FCF6359F56A0}"
3130
EndProject
3231
Global
3332
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3433
Debug|Any CPU = Debug|Any CPU
3534
Release|Any CPU = Release|Any CPU
3635
EndGlobalSection
3736
GlobalSection(ProjectConfigurationPlatforms) = postSolution
38-
{F87C7A13-669C-4F18-9266-B256F254DFA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39-
{F87C7A13-669C-4F18-9266-B256F254DFA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
40-
{F87C7A13-669C-4F18-9266-B256F254DFA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
41-
{F87C7A13-669C-4F18-9266-B256F254DFA3}.Release|Any CPU.Build.0 = Release|Any CPU
42-
{62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43-
{62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
44-
{62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
45-
{62834B0C-A2C2-4449-9E2A-00CC390A79BE}.Release|Any CPU.Build.0 = Release|Any CPU
37+
{97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38+
{97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
39+
{97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
40+
{97648980-AA30-4AC0-B8E9-FCF6359F56A0}.Release|Any CPU.Build.0 = Release|Any CPU
4641
EndGlobalSection
4742
GlobalSection(SolutionProperties) = preSolution
4843
HideSolutionNode = FALSE

src/SmallSharp.Build/MonitorActiveDocument.cs

Lines changed: 0 additions & 59 deletions
This file was deleted.

src/SmallSharp.Build/OpenStartupFile.cs

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/SmallSharp.Build/SmallSharp.Build.csproj

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/SmallSharp.Build/ActiveDocumentMonitor.cs renamed to src/SmallSharp/ActiveDocumentMonitor.cs

Lines changed: 79 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,121 +6,104 @@
66
using System.Threading;
77
using System.Xml.Linq;
88
using Microsoft.VisualStudio.Shell.Interop;
9+
using Newtonsoft.Json;
910
using Newtonsoft.Json.Linq;
1011

1112
namespace SmallSharp.Build
1213
{
13-
class ActiveDocumentMonitor : MarshalByRefObject, IDisposable, IVsRunningDocTableEvents, IVsSelectionEvents
14+
class ActiveDocumentMonitor : MarshalByRefObject, IDisposable, IVsRunningDocTableEvents, IVsSelectionEvents, IVsSolutionEvents
1415
{
15-
FileSystemWatcher watcher;
16-
readonly IServiceProvider services;
17-
16+
IVsSolution? solution;
1817
IVsRunningDocumentTable? rdt;
1918
IVsMonitorSelection? selection;
19+
20+
uint solutionCookie;
2021
uint rdtCookie;
2122
uint selectionCookie;
2223

2324
string launchProfilesPath;
2425
string userFile;
25-
string flagFile;
26-
Dictionary<string, string> startupFiles = new();
26+
Dictionary<string, string> startupFiles;
27+
28+
string? activeFile;
2729

28-
public ActiveDocumentMonitor(string launchProfilesPath, string userFile, string flagFile, IServiceProvider services)
30+
public ActiveDocumentMonitor(string launchProfilesPath, string userFile,
31+
string[] startupFiles, IServiceProvider services)
2932
{
3033
this.launchProfilesPath = launchProfilesPath;
3134
this.userFile = userFile;
32-
this.flagFile = flagFile;
33-
this.services = services;
34-
35-
watcher = new FileSystemWatcher(Path.GetDirectoryName(launchProfilesPath))
36-
{
37-
NotifyFilter = NotifyFilters.LastWrite,
38-
Filter = "launchSettings.json",
39-
};
40-
41-
watcher.Changed += (_, _) => ReloadProfiles();
42-
watcher.Created += (_, _) => ReloadProfiles();
43-
watcher.EnableRaisingEvents = true;
44-
ReloadProfiles();
45-
}
35+
this.startupFiles = startupFiles.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase);
4636

47-
public void Start()
48-
{
37+
solution = (IVsSolution)services.GetService(typeof(SVsSolution));
4938
rdt = (IVsRunningDocumentTable)services.GetService(typeof(SVsRunningDocumentTable));
50-
if (rdt != null)
51-
rdt.AdviseRunningDocTableEvents(this, out rdtCookie);
52-
5339
selection = (IVsMonitorSelection)services.GetService(typeof(SVsShellMonitorSelection));
54-
if (selection != null)
55-
selection.AdviseSelectionEvents(this, out selectionCookie);
40+
41+
EnsureMonitoring();
5642
}
5743

58-
public void Refresh(string launchProfiles, string userFile, string flagFile)
44+
public void Refresh(string launchProfiles, string userFile, string[] startupFiles)
5945
{
6046
launchProfilesPath = launchProfiles;
6147
this.userFile = userFile;
62-
this.flagFile = flagFile;
63-
watcher.Path = Path.GetDirectoryName(launchProfiles);
64-
ReloadProfiles();
65-
}
48+
this.startupFiles = startupFiles.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase);
6649

67-
void ReloadProfiles()
68-
{
69-
if (!File.Exists(launchProfilesPath))
70-
return;
71-
72-
var maxAttempts = 5;
73-
var exceptions = new List<Exception>();
50+
EnsureMonitoring();
7451

75-
for (var i = 0; i < maxAttempts; i++)
76-
{
77-
try
78-
{
79-
var json = JObject.Parse(File.ReadAllText(launchProfilesPath));
80-
if (json.Property("profiles") is not JProperty prop ||
81-
prop.Value is not JObject profiles)
82-
return;
52+
// For new files, we get the update before the new item is added to
53+
// msbuild top-level files, so we retry on refresh
54+
UpdateStartupFile(activeFile);
55+
}
8356

84-
startupFiles = profiles.Properties().Select(p => p.Name)
85-
.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase);
57+
void EnsureMonitoring()
58+
{
59+
if (solutionCookie == 0 && solution != null)
60+
solution.AdviseSolutionEvents(this, out solutionCookie);
8661

87-
return;
88-
}
89-
catch (Exception e)
90-
{
91-
exceptions.Add(e);
92-
Thread.Sleep(500);
93-
}
94-
}
62+
if (rdtCookie == 0 && rdt != null)
63+
rdt.AdviseRunningDocTableEvents(this, out rdtCookie);
9564

96-
// NOTE: check exceptions list to see why.
97-
Debug.Fail("Could not read launchSettings.json");
65+
if (selectionCookie == 0 && selection != null)
66+
selection.AdviseSelectionEvents(this, out selectionCookie);
9867
}
9968

10069
void UpdateStartupFile(string? path)
10170
{
71+
activeFile = path;
72+
10273
if (!string.IsNullOrEmpty(path) &&
10374
path!.IndexOfAny(Path.GetInvalidPathChars()) == -1 &&
104-
Path.GetFileName(path) is string startupFile &&
105-
startupFiles.ContainsKey(startupFile))
75+
startupFiles.TryGetValue(Path.GetFileName(path), out var startupFile))
10676
{
77+
var settings = new JObject(
78+
new JProperty("profiles", new JObject(
79+
new JProperty(startupFile, new JObject(
80+
new JProperty("commandName", "Project")
81+
))
82+
))
83+
);
84+
85+
var json = settings.ToString(Formatting.Indented);
86+
87+
// Only write if different content.
88+
if (File.Exists(launchProfilesPath) &&
89+
File.ReadAllText(launchProfilesPath) == json)
90+
return;
91+
92+
File.WriteAllText(launchProfilesPath, json);
93+
10794
try
10895
{
10996
// Get the value as it was exists in the original dictionary,
11097
// since it has to match what the source generator created in the
11198
// launch profiles.
112-
startupFile = startupFiles[startupFile];
11399
var xdoc = XDocument.Load(userFile);
114100
var active = xdoc
115101
.Descendants("{http://schemas.microsoft.com/developer/msbuild/2003}ActiveDebugProfile")
116102
.FirstOrDefault();
117103

118-
if (active != null && active.Value != startupFile)
104+
if (active != null && !startupFile.Equals(active.Value, StringComparison.OrdinalIgnoreCase))
119105
{
120106
active.Value = startupFile;
121-
// First save to flag file so we don't cause another open
122-
// attempt via the OpenStartupFile task.
123-
File.WriteAllText(flagFile, startupFile);
124107
xdoc.Save(userFile);
125108
}
126109
}
@@ -131,15 +114,22 @@ void UpdateStartupFile(string? path)
131114
}
132115
}
133116

134-
void IDisposable.Dispose()
117+
public void Dispose()
135118
{
119+
if (solutionCookie != 0 && solution != null)
120+
Try(() => solution.UnadviseSolutionEvents(solutionCookie));
121+
122+
solutionCookie = 0;
123+
136124
if (rdtCookie != 0 && rdt != null)
137125
Try(() => rdt.UnadviseRunningDocTableEvents(rdtCookie));
138126

127+
rdtCookie = 0;
128+
139129
if (selectionCookie != 0 && selection != null)
140130
Try(() => selection.UnadviseSelectionEvents(selectionCookie));
141131

142-
watcher.Dispose();
132+
selectionCookie = 0;
143133
}
144134

145135
void Try(Action action)
@@ -176,18 +166,32 @@ int IVsSelectionEvents.OnSelectionChanged(IVsHierarchy pHierOld, uint itemidOld,
176166
return 0;
177167
}
178168

179-
int IVsRunningDocTableEvents.OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0;
169+
int IVsSolutionEvents.OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy)
170+
{
171+
Dispose();
172+
return 0;
173+
}
180174

181-
int IVsRunningDocTableEvents.OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0;
175+
int IVsSolutionEvents.OnBeforeCloseSolution(object pUnkReserved)
176+
{
177+
Dispose();
178+
return 0;
179+
}
182180

181+
int IVsRunningDocTableEvents.OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0;
182+
int IVsRunningDocTableEvents.OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => 0;
183183
int IVsRunningDocTableEvents.OnAfterSave(uint docCookie) => 0;
184-
185184
int IVsRunningDocTableEvents.OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame) => 0;
186-
187185
int IVsRunningDocTableEvents.OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame) => 0;
188-
189186
int IVsSelectionEvents.OnElementValueChanged(uint elementid, object varValueOld, object varValueNew) => 0;
190-
191187
int IVsSelectionEvents.OnCmdUIContextChanged(uint dwCmdUICookie, int fActive) => 0;
188+
int IVsSolutionEvents.OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) => throw new NotImplementedException();
189+
int IVsSolutionEvents.OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) => throw new NotImplementedException();
190+
int IVsSolutionEvents.OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) => throw new NotImplementedException();
191+
int IVsSolutionEvents.OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) => throw new NotImplementedException();
192+
int IVsSolutionEvents.OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) => throw new NotImplementedException();
193+
int IVsSolutionEvents.OnAfterOpenSolution(object pUnkReserved, int fNewSolution) => throw new NotImplementedException();
194+
int IVsSolutionEvents.OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) => throw new NotImplementedException();
195+
int IVsSolutionEvents.OnAfterCloseSolution(object pUnkReserved) => throw new NotImplementedException();
192196
}
193197
}

0 commit comments

Comments
 (0)