Skip to content

Commit 1845e49

Browse files
committed
Copy watch expression to clipboard -- #13, #63
1 parent c146b50 commit 1845e49

File tree

6 files changed

+112
-32
lines changed

6 files changed

+112
-32
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Window x:Class="ExpressionTreeVisualizer.ExpressionRootPrompt" Title="Input" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen" ContentRendered="Window_ContentRendered" MaxWidth="300"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:ExpressionTreeVisualizer">
3+
<StackPanel Orientation="Vertical" Margin="12">
4+
<TextBlock Text="Enter the expression currently being debugged:" />
5+
<TextBox Name="txbExpression" Margin="0,5,0,0" />
6+
<TextBlock Margin="0,5,0,0" TextWrapping="Wrap">
7+
<Run Text="The custom visualizer API currently doesn't expose the debugged expression; there is a (currently closed)" />
8+
<Hyperlink NavigateUri="https://developercommunity.visualstudio.com/idea/503782/api-for-visualized-expression-in-custom-visualizer.html" Name="link">
9+
request on Visual Studio Developer Community
10+
</Hyperlink>
11+
<Run Text="to allow this"/>
12+
</TextBlock>
13+
<Button HorizontalAlignment="Right" Content="OK" IsDefault="True" Click="OK_Click" Margin="0,18,0,0" Width="75" Height="23" />
14+
</StackPanel>
15+
</Window>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using System.Windows;
8+
9+
namespace ExpressionTreeVisualizer {
10+
public partial class ExpressionRootPrompt {
11+
public string Expression { get; private set; }
12+
13+
public ExpressionRootPrompt() {
14+
InitializeComponent();
15+
16+
Closing += (s, e) => Expression = txbExpression.Text;
17+
link.RequestNavigate += (s, e) => Process.Start(link.NavigateUri.ToString());
18+
}
19+
20+
private void Window_ContentRendered(object sender, EventArgs e) {
21+
txbExpression.Focus();
22+
}
23+
24+
private void OK_Click(object sender, RoutedEventArgs e) => Close();
25+
}
26+
}

Visualizer.Shared/Visualizer.Shared.projitems

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
</PropertyGroup>
1111
<ItemGroup>
1212
<Compile Include="$(MSBuildThisFileDirectory)Converters.cs" />
13+
<Compile Include="$(MSBuildThisFileDirectory)ExpressionRootPrompt.xaml.cs" />
1314
<Compile Include="$(MSBuildThisFileDirectory)Util\Extensions\CallSiteBinder.cs" />
1415
<Compile Include="$(MSBuildThisFileDirectory)Util\Extensions\DependencyObject.cs" />
1516
<Compile Include="$(MSBuildThisFileDirectory)Util\Extensions\MultiSelector.cs" />
@@ -34,6 +35,10 @@
3435
<Content Include="$(MSBuildThisFileDirectory)WpfAutoGrid\license.txt" />
3536
</ItemGroup>
3637
<ItemGroup>
38+
<Page Include="$(MSBuildThisFileDirectory)ExpressionRootPrompt.xaml">
39+
<Generator>MSBuild:Compile</Generator>
40+
<SubType>Designer</SubType>
41+
</Page>
3742
<Page Include="$(MSBuildThisFileDirectory)VisualizerDataControl.xaml">
3843
<Generator>MSBuild:Compile</Generator>
3944
<SubType>Designer</SubType>
@@ -43,4 +48,9 @@
4348
<SubType>Designer</SubType>
4449
</Page>
4550
</ItemGroup>
51+
<ItemGroup>
52+
<Compile Update="C:\Users\Spitz\source\repos\zspitz\ExpressionToString\Visualizer.Shared\ExpressionRootPrompt.xaml.cs">
53+
<DependentUpon>ExpressionRootPrompt.xaml</DependentUpon>
54+
</Compile>
55+
</ItemGroup>
4656
</Project>

Visualizer.Shared/VisualizerData.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public class ExpressionNodeData : INotifyPropertyChanged {
129129

130130
private List<(string @namespace, string typename)> _baseTypes;
131131
public List<(string @namespace, string typename)> BaseTypes => _baseTypes;
132+
public string WatchExpressionFormatString { get; set; }
132133

133134
public EndNodeData EndNodeData => new EndNodeData {
134135
Closure = Closure,
@@ -142,7 +143,7 @@ public ExpressionNodeData() { }
142143

143144
private static HashSet<Type> propertyTypes = NodeTypes.SelectMany(x => new[] { x, typeof(IEnumerable<>).MakeGenericType(x) }).ToHashSet();
144145

145-
internal ExpressionNodeData(object o, (string aggregatePath, string pathFromParent) path, VisualizerData visualizerData, bool isParameterDeclaration = false, PropertyInfo pi = null) {
146+
internal ExpressionNodeData(object o, (string aggregatePath, string pathFromParent) path, VisualizerData visualizerData, bool isParameterDeclaration = false, PropertyInfo pi = null, string parentWatchExpression = "") {
146147
var (aggregatePath, pathFromParent) = path;
147148
PathFromParent = pathFromParent;
148149
if (aggregatePath.IsNullOrWhitespace() || pathFromParent.IsNullOrWhitespace()) {
@@ -217,6 +218,18 @@ internal ExpressionNodeData(object o, (string aggregatePath, string pathFromPare
217218
Span = span;
218219
}
219220

221+
if (parentWatchExpression.IsNullOrWhitespace()) {
222+
WatchExpressionFormatString = "{0}";
223+
} else if (pi != null) {
224+
var watchPathFromParent = PathFromParent;
225+
if (visualizerData.Options.Language==CSharp) {
226+
WatchExpressionFormatString = $"(({pi.DeclaringType.FullName}){parentWatchExpression}).{watchPathFromParent}";
227+
} else { //VisualBasic
228+
watchPathFromParent = watchPathFromParent.Replace("[", "(").Replace("]", ")");
229+
WatchExpressionFormatString = $"CType({parentWatchExpression}, {pi.DeclaringType.FullName}).{watchPathFromParent}";
230+
}
231+
}
232+
220233
// populate Children
221234
var type = o.GetType();
222235
var preferredOrder = preferredPropertyOrders.FirstOrDefault(x => x.Item1.IsAssignableFrom(type)).Item2;
@@ -238,13 +251,13 @@ internal ExpressionNodeData(object o, (string aggregatePath, string pathFromPare
238251
}
239252
})
240253
.Where(x => x.Item2 != null)
241-
.Select(x => new ExpressionNodeData(x.Item2, (FullPath ?? "", x.Item1), visualizerData, false, x.Item3))
254+
.Select(x => new ExpressionNodeData(x.Item2, (FullPath ?? "", x.Item1), visualizerData, false, x.Item3, WatchExpressionFormatString))
242255
.ToList();
243256

244257
// populate URLs
245258
if (pi != null) {
246259
ParentProperty = (pi.DeclaringType.Namespace, pi.DeclaringType.Name, pi.Name);
247-
}
260+
}
248261

249262
if (!baseTypes.TryGetValue(o.GetType(), out _baseTypes)) {
250263
_baseTypes = o.GetType().BaseTypes(true, true).Where(x => x != typeof(object) && x.IsPublic).Select(x => (x.Namespace, x.Name)).Distinct().ToList();

Visualizer.Shared/VisualizerDataControl.xaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
</TextBlock.ToolTip>
2323
<TextBlock.ContextMenu>
2424
<ContextMenu>
25+
<MenuItem Header="Copy watch expression" Click="CopyWatchExpression_Click" />
2526
<MenuItem Header="Help" Loaded="HelpContextMenu_Loaded" />
2627
</ContextMenu>
2728
</TextBlock.ContextMenu>
@@ -110,6 +111,10 @@
110111
<TextBox Name="source" Margin="0,0,0,12" MinWidth="500" BorderBrush="Gray" BorderThickness="1" FontFamily="Consolas" FontSize="14" IsInactiveSelectionHighlightEnabled="True" IsReadOnly="True" IsReadOnlyCaretVisible="True" SelectionBrush="Blue" Text="{Binding Source}" TextWrapping="Wrap" />
111112
</DockPanel>
112113
<DockPanel Margin="12">
114+
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0">
115+
<TextBlock Text="Expression:" Margin="0,0,12,0" VerticalAlignment="Center" />
116+
<TextBox Name="txbRootExpression" VerticalContentAlignment="Center" />
117+
</DockPanel>
113118
<StackPanel Margin="0,0,0,5" VerticalAlignment="Bottom" DockPanel.Dock="Top" Orientation="Horizontal">
114119
<TextBlock VerticalAlignment="Bottom" Text="Tree: " />
115120
<TextBlock FontSize="15" Foreground="Blue" Text="&#128712;">

Visualizer.Shared/VisualizerDataControl.xaml.cs

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -132,56 +132,67 @@ public void LoadDataContext() {
132132
private void HelpContextMenu_Loaded(object sender, RoutedEventArgs e) {
133133
var menu = (MenuItem)sender;
134134
var node = (ExpressionNodeData)menu.DataContext;
135-
MenuItem mi;
136135

137136
if (menu.Items.Any()) { return; }
138137

138+
var listData = new List<(string header, string url)>();
139+
139140
if (node.ParentProperty.HasValue) {
140141
var (@namespace, typename, propertyname) = node.ParentProperty.Value;
141-
mi = new MenuItem() {
142-
Header = $"Property: {typename}.{propertyname}"
143-
};
144-
mi.Click += (s1, e1) => {
145-
var url = $"{BaseUrl}{new[] { @namespace, typename, propertyname }.Joined(".")}";
146-
Process.Start(url);
147-
};
148-
menu.Items.Add(mi);
142+
listData.Add(
143+
$"Property: {typename}.{propertyname}",
144+
$"{BaseUrl}{new[] { @namespace, typename, propertyname }.Joined(".")}"
145+
);
149146
}
150147

151148
if (node.ParentProperty.HasValue && node.NodeTypeParts.HasValue) {
152-
menu.Items.Add(new Separator());
149+
listData.Add("---", "");
153150
}
154151

155152
if (node.NodeTypeParts.HasValue) {
156153
var (@namespace, typename, membername) = node.NodeTypeParts.Value;
157-
mi = new MenuItem() {
158-
Header = $"Node type: {typename}.{membername}"
159-
};
160-
mi.Click += (s1, e1) => {
161-
var url = $"{BaseUrl}{new[] { @namespace, typename }.Joined(".")}#{new[] { @namespace.Replace(".","_"), typename, membername }.Joined("_")}";
162-
Process.Start(url);
163-
};
164-
menu.Items.Add(mi);
154+
listData.Add(
155+
$"Node type: {typename}.{membername}",
156+
$"{BaseUrl}{new[] { @namespace, typename }.Joined(".")}#{new[] { @namespace.Replace(".", "_"), typename, membername }.Joined("_")}"
157+
);
165158
}
166159

167160
if ((node.ParentProperty.HasValue || node.NodeTypeParts.HasValue) && node.BaseTypes.Any()) {
168-
menu.Items.Add(new Separator());
161+
listData.Add("---", "");
169162
}
170163

171164
if (node.BaseTypes != null) {
172-
node.BaseTypes.ForEachT((@namespace, typename) => {
173-
mi = new MenuItem() {
174-
Header = $"Base type: {typename}"
175-
};
176-
mi.Click += (s1, e1) => {
177-
var url = $"{BaseUrl}{@namespace}.{typename.Replace("~", "-")}";
178-
Process.Start(url);
179-
};
180-
menu.Items.Add(mi);
181-
});
165+
node.BaseTypes.SelectT((@namespace, typename) => (
166+
$"Base type: {typename}",
167+
$"{BaseUrl}{@namespace}.{typename.Replace("~", "-")}"
168+
)).AddRangeTo(listData);
182169
}
170+
171+
listData.ForEachT((header, url) => {
172+
if (header == "---") {
173+
menu.Items.Add(new Separator());
174+
return;
175+
}
176+
177+
var mi = new MenuItem() {
178+
Header = header
179+
};
180+
mi.Click += (s1, e1) => Process.Start(url);
181+
menu.Items.Add(mi);
182+
});
183183
}
184184

185185
private const string BaseUrl = "https://docs.microsoft.com/dotnet/api/";
186+
187+
private void CopyWatchExpression_Click(object sender, RoutedEventArgs e) {
188+
if (txbRootExpression.Text.IsNullOrWhitespace()) {
189+
var dlg = new ExpressionRootPrompt();
190+
dlg.ShowDialog();
191+
txbRootExpression.Text = dlg.Expression;
192+
}
193+
194+
var node = (ExpressionNodeData)((MenuItem)sender).DataContext;
195+
Clipboard.SetText(string.Format(node.WatchExpressionFormatString, txbRootExpression.Text));
196+
}
186197
}
187198
}

0 commit comments

Comments
 (0)