Skip to content

Commit 30a74a9

Browse files
Add Find bar (Ctrl+F) for output text and SQL builds tree search
- New reusable FindBar control with Next/Prev/Close, match count, Ctrl+F/F3/Shift+F3/Escape keyboard support, case-insensitive search - Integrated into ResolvePage and ClassicView output panes - Refactored SQLBuildsDialog to use same FindBar-style UX with bidirectional wrapping tree search (replaces old inline search) - Enabled IsInactiveSelectionHighlightEnabled on output TextBoxes - Ensure ModernTests is excluded from final build artifact
1 parent b17a864 commit 30a74a9

10 files changed

Lines changed: 274 additions & 20 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
Target/Release
3030
!Target/Release/*.xml
3131
!Target/Release/Tests
32+
!Target/Release/ModernTests
3233
- name: Prep for running tests
3334
run: ./downloadsyms.ps1
3435
working-directory: Tests/TestCases

Modern/Controls/FindBar.xaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<UserControl x:Class="Microsoft.SqlServer.Utils.Misc.SQLCallStackResolver.Modern.FindBar"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
Visibility="Collapsed">
5+
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
6+
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
7+
BorderThickness="1" CornerRadius="4" Padding="6,4" Margin="0,0,0,4">
8+
<DockPanel>
9+
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="6,0,0,0">
10+
<TextBlock x:Name="MatchInfo" VerticalAlignment="Center" FontSize="11" Opacity="0.6" Margin="0,0,8,0"/>
11+
<Button x:Name="PrevButton" Click="Prev_Click" Padding="4,2" ToolTip="Previous match (Shift+F3)"
12+
Background="Transparent" BorderThickness="0">
13+
<TextBlock Text="&#xE70E;" FontFamily="Segoe MDL2 Assets" FontSize="12"/>
14+
</Button>
15+
<Button x:Name="NextButton" Click="Next_Click" Padding="4,2" ToolTip="Next match (F3)"
16+
Background="Transparent" BorderThickness="0">
17+
<TextBlock Text="&#xE70D;" FontFamily="Segoe MDL2 Assets" FontSize="12"/>
18+
</Button>
19+
<Button Click="Close_Click" Padding="4,2" Margin="4,0,0,0" ToolTip="Close (Esc)"
20+
Background="Transparent" BorderThickness="0">
21+
<TextBlock Text="&#xE711;" FontFamily="Segoe MDL2 Assets" FontSize="12"/>
22+
</Button>
23+
</StackPanel>
24+
<TextBox x:Name="SearchBox" VerticalAlignment="Center" FontSize="13"
25+
KeyDown="SearchBox_KeyDown" TextChanged="SearchBox_TextChanged"/>
26+
</DockPanel>
27+
</Border>
28+
</UserControl>

Modern/Controls/FindBar.xaml.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License - see LICENSE file in this repo.
3+
namespace Microsoft.SqlServer.Utils.Misc.SQLCallStackResolver.Modern {
4+
public partial class FindBar : UserControl {
5+
private TextBox _targetTextBox;
6+
private List<int> _matchPositions = new List<int>();
7+
private int _currentMatchIndex = -1;
8+
9+
public FindBar() {
10+
InitializeComponent();
11+
}
12+
13+
/// <summary>Attach this find bar to a target TextBox for searching.</summary>
14+
public void Attach(TextBox target) {
15+
_targetTextBox = target;
16+
}
17+
18+
/// <summary>Show the find bar and focus the search box.</summary>
19+
public void Open() {
20+
Visibility = Visibility.Visible;
21+
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, new Action(() => {
22+
SearchBox.Focus();
23+
Keyboard.Focus(SearchBox);
24+
SearchBox.SelectAll();
25+
}));
26+
}
27+
28+
/// <summary>Hide the find bar and return focus to the target.</summary>
29+
public void Close() {
30+
Visibility = Visibility.Collapsed;
31+
_matchPositions.Clear();
32+
_currentMatchIndex = -1;
33+
MatchInfo.Text = "";
34+
_targetTextBox?.Focus();
35+
}
36+
37+
public bool IsOpen => Visibility == Visibility.Visible;
38+
39+
private void FindMatches() {
40+
_matchPositions.Clear();
41+
_currentMatchIndex = -1;
42+
var searchText = SearchBox.Text;
43+
if (_targetTextBox == null || string.IsNullOrEmpty(searchText)) {
44+
MatchInfo.Text = "";
45+
return;
46+
}
47+
48+
var content = _targetTextBox.Text ?? "";
49+
int pos = 0;
50+
while ((pos = content.IndexOf(searchText, pos, StringComparison.OrdinalIgnoreCase)) >= 0) {
51+
_matchPositions.Add(pos);
52+
pos += searchText.Length;
53+
}
54+
55+
if (_matchPositions.Count > 0) {
56+
_currentMatchIndex = 0;
57+
HighlightCurrent();
58+
} else {
59+
MatchInfo.Text = "No matches";
60+
}
61+
}
62+
63+
private void HighlightCurrent() {
64+
if (_targetTextBox == null || _currentMatchIndex < 0 || _currentMatchIndex >= _matchPositions.Count) return;
65+
var pos = _matchPositions[_currentMatchIndex];
66+
_targetTextBox.Focus();
67+
_targetTextBox.Select(pos, SearchBox.Text.Length);
68+
// Scroll to selection by getting the character rect
69+
var rect = _targetTextBox.GetRectFromCharacterIndex(pos);
70+
_targetTextBox.ScrollToLine(_targetTextBox.GetLineIndexFromCharacterIndex(pos));
71+
MatchInfo.Text = $"{_currentMatchIndex + 1} of {_matchPositions.Count}";
72+
// Return focus to search box so user can keep typing
73+
SearchBox.Focus();
74+
}
75+
76+
private void Next_Click(object sender, RoutedEventArgs e) => FindNext();
77+
private void Prev_Click(object sender, RoutedEventArgs e) => FindPrevious();
78+
79+
public void FindNext() {
80+
if (_matchPositions.Count == 0) return;
81+
_currentMatchIndex = (_currentMatchIndex + 1) % _matchPositions.Count;
82+
HighlightCurrent();
83+
}
84+
85+
public void FindPrevious() {
86+
if (_matchPositions.Count == 0) return;
87+
_currentMatchIndex = (_currentMatchIndex - 1 + _matchPositions.Count) % _matchPositions.Count;
88+
HighlightCurrent();
89+
}
90+
91+
private void Close_Click(object sender, RoutedEventArgs e) => Close();
92+
93+
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) {
94+
FindMatches();
95+
}
96+
97+
private void SearchBox_KeyDown(object sender, KeyEventArgs e) {
98+
if (e.Key == Key.Enter || e.Key == Key.F3) {
99+
if (Keyboard.Modifiers == ModifierKeys.Shift)
100+
FindPrevious();
101+
else
102+
FindNext();
103+
e.Handled = true;
104+
} else if (e.Key == Key.Escape) {
105+
Close();
106+
e.Handled = true;
107+
}
108+
}
109+
}
110+
}

Modern/Dialogs/SQLBuildsDialog.xaml

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
ui:WindowHelper.UseModernWindowStyle="True"
66
Title="SQL Server Builds - Download Public PDBs" Height="550" Width="700"
77
WindowStartupLocation="CenterOwner" ResizeMode="CanResizeWithGrip">
8+
<Window.InputBindings>
9+
<KeyBinding Gesture="Ctrl+F" Command="Find"/>
10+
</Window.InputBindings>
11+
<Window.CommandBindings>
12+
<CommandBinding Command="Find" Executed="Find_Executed"/>
13+
</Window.CommandBindings>
814
<Grid Margin="10">
915
<Grid.RowDefinitions>
1016
<RowDefinition Height="Auto"/>
@@ -13,11 +19,30 @@
1319
<RowDefinition Height="Auto"/>
1420
</Grid.RowDefinitions>
1521

16-
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
17-
<TextBlock Text="Search:" VerticalAlignment="Center" Margin="0,0,8,0"/>
18-
<TextBox x:Name="searchText" Width="200" Margin="0,0,8,0"/>
19-
<Button Content="Find Next" Width="80" Click="FindNext_Click"/>
20-
</StackPanel>
22+
<Border x:Name="findBarBorder" Grid.Row="0" Visibility="Collapsed"
23+
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
24+
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
25+
BorderThickness="1" CornerRadius="4" Padding="6,4" Margin="0,0,0,8">
26+
<DockPanel>
27+
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="6,0,0,0">
28+
<TextBlock x:Name="matchInfo" VerticalAlignment="Center" FontSize="11" Opacity="0.6" Margin="0,0,8,0"/>
29+
<Button Click="FindPrev_Click" Padding="4,2" ToolTip="Previous match (Shift+F3)"
30+
Background="Transparent" BorderThickness="0">
31+
<TextBlock Text="&#xE70E;" FontFamily="Segoe MDL2 Assets" FontSize="12"/>
32+
</Button>
33+
<Button Click="FindNext_Click" Padding="4,2" ToolTip="Next match (F3)"
34+
Background="Transparent" BorderThickness="0">
35+
<TextBlock Text="&#xE70D;" FontFamily="Segoe MDL2 Assets" FontSize="12"/>
36+
</Button>
37+
<Button Click="FindClose_Click" Padding="4,2" Margin="4,0,0,0" ToolTip="Close (Esc)"
38+
Background="Transparent" BorderThickness="0">
39+
<TextBlock Text="&#xE711;" FontFamily="Segoe MDL2 Assets" FontSize="12"/>
40+
</Button>
41+
</StackPanel>
42+
<TextBox x:Name="searchText" VerticalAlignment="Center" FontSize="13"
43+
KeyDown="SearchBox_KeyDown"/>
44+
</DockPanel>
45+
</Border>
2146

2247
<TreeView Grid.Row="1" x:Name="treeviewSyms" Margin="0,0,0,8"/>
2348

Modern/Dialogs/SQLBuildsDialog.xaml.cs

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,25 +103,76 @@ private async void CheckPDBAvail_Click(object sender, RoutedEventArgs e) {
103103
downloadStatus.Text = failedUrls.Count > 0 ? string.Join(",", failedUrls) : "All PDBs for this build are available!";
104104
}
105105

106-
private void FindNext_Click(object sender, RoutedEventArgs e) {
107-
if (string.IsNullOrWhiteSpace(searchText.Text)) return;
108-
var found = SearchTree(treeviewSyms.Items, searchText.Text.Trim().ToLower(CultureInfo.CurrentCulture));
109-
downloadStatus.Text = found ? "Found match!" : "No matches found.";
106+
private void FindNext_Click(object sender, RoutedEventArgs e) => FindInTree(forward: true);
107+
private void FindPrev_Click(object sender, RoutedEventArgs e) => FindInTree(forward: false);
108+
109+
private void Find_Executed(object sender, ExecutedRoutedEventArgs e) {
110+
findBarBorder.Visibility = Visibility.Visible;
111+
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, new Action(() => {
112+
searchText.Focus();
113+
Keyboard.Focus(searchText);
114+
searchText.SelectAll();
115+
}));
110116
}
111117

112-
private bool SearchTree(ItemCollection items, string searchTerm) {
113-
foreach (TreeViewItem item in items) {
114-
if (item.Tag is SQLBuildInfo bld && bld.ToString().ToLower(CultureInfo.CurrentCulture).Contains(searchTerm)) {
115-
item.IsSelected = true;
116-
item.BringIntoView();
117-
// expand parent chain
118+
private void FindClose_Click(object sender, RoutedEventArgs e) {
119+
findBarBorder.Visibility = Visibility.Collapsed;
120+
matchInfo.Text = "";
121+
}
122+
123+
private void SearchBox_KeyDown(object sender, KeyEventArgs e) {
124+
if (e.Key == Key.Enter || e.Key == Key.F3) {
125+
FindInTree(forward: Keyboard.Modifiers != ModifierKeys.Shift);
126+
e.Handled = true;
127+
} else if (e.Key == Key.Escape) {
128+
FindClose_Click(sender, e);
129+
e.Handled = true;
130+
}
131+
}
132+
133+
private List<TreeViewItem> _flatItems;
134+
private int _lastFoundIndex = -1;
135+
136+
private void FindInTree(bool forward) {
137+
if (string.IsNullOrWhiteSpace(searchText.Text)) return;
138+
var term = searchText.Text.Trim().ToLower(CultureInfo.CurrentCulture);
139+
140+
// Build flat list of all leaf items
141+
_flatItems = new List<TreeViewItem>();
142+
CollectLeafItems(treeviewSyms.Items, _flatItems);
143+
144+
if (_flatItems.Count == 0) { matchInfo.Text = "No items"; return; }
145+
146+
int start = forward ? _lastFoundIndex + 1 : _lastFoundIndex - 1;
147+
int count = _flatItems.Count;
148+
149+
for (int i = 0; i < count; i++) {
150+
int idx = forward
151+
? (start + i + count) % count
152+
: (start - i + count) % count;
153+
var item = _flatItems[idx];
154+
if (item.Tag is SQLBuildInfo bld && bld.ToString().ToLower(CultureInfo.CurrentCulture).Contains(term)) {
155+
// Expand parent chain
118156
var parent = item.Parent as TreeViewItem;
119157
while (parent != null) { parent.IsExpanded = true; parent = parent.Parent as TreeViewItem; }
120-
return true;
158+
item.IsSelected = true;
159+
item.BringIntoView();
160+
_lastFoundIndex = idx;
161+
matchInfo.Text = "Found";
162+
downloadStatus.Text = "";
163+
return;
121164
}
122-
if (item.Items.Count > 0 && SearchTree(item.Items, searchTerm)) return true;
123165
}
124-
return false;
166+
matchInfo.Text = "No matches";
167+
}
168+
169+
private void CollectLeafItems(ItemCollection items, List<TreeViewItem> result) {
170+
foreach (TreeViewItem item in items) {
171+
if (item.Items.Count > 0)
172+
CollectLeafItems(item.Items, result);
173+
else
174+
result.Add(item);
175+
}
125176
}
126177
}
127178
}

Modern/Pages/ResolvePage.xaml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@
66
<local:BoolToVisibilityConverter x:Key="BoolToVis"/>
77
<local:InverseBoolToVisibilityConverter x:Key="InvBoolToVis"/>
88
</UserControl.Resources>
9+
<UserControl.InputBindings>
10+
<KeyBinding Gesture="Ctrl+F" Command="Find"/>
11+
</UserControl.InputBindings>
12+
<UserControl.CommandBindings>
13+
<CommandBinding Command="Find" Executed="Find_Executed"/>
14+
</UserControl.CommandBindings>
915
<Grid>
1016
<Grid.RowDefinitions>
1117
<RowDefinition Height="Auto"/>
1218
<RowDefinition Height="Auto"/>
1319
<RowDefinition Height="Auto"/>
20+
<RowDefinition Height="Auto"/>
1421
<RowDefinition Height="*"/>
1522
</Grid.RowDefinitions>
1623

@@ -27,9 +34,19 @@
2734
<TextBlock Text="Copy All"/>
2835
</StackPanel>
2936
</Button>
37+
<Button Padding="16,6" Margin="8,0,0,0" Click="Find_Click"
38+
Visibility="{Binding HasOutput, Converter={StaticResource BoolToVis}}">
39+
<StackPanel Orientation="Horizontal">
40+
<TextBlock Text="&#xE721;" FontFamily="Segoe MDL2 Assets" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
41+
<TextBlock Text="Find"/>
42+
</StackPanel>
43+
</Button>
3044
</StackPanel>
3145

32-
<TextBox Grid.Row="3" IsReadOnly="True" AcceptsReturn="True"
46+
<local:FindBar x:Name="findBar" Grid.Row="2"/>
47+
48+
<TextBox x:Name="outputTextBox" Grid.Row="4" IsReadOnly="True" AcceptsReturn="True"
49+
IsInactiveSelectionHighlightEnabled="True"
3350
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
3451
FontFamily="Consolas" FontSize="13" TextWrapping="NoWrap"
3552
Text="{Binding OutputText, Mode=OneWay}"/>

Modern/Pages/ResolvePage.xaml.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace Microsoft.SqlServer.Utils.Misc.SQLCallStackResolver.Modern {
44
public partial class ResolvePage : UserControl {
55
public ResolvePage() {
66
InitializeComponent();
7+
findBar.Attach(outputTextBox);
78
}
89

910
private void CopyAll_Click(object sender, RoutedEventArgs e) {
@@ -12,5 +13,8 @@ private void CopyAll_Click(object sender, RoutedEventArgs e) {
1213
vm.StatusMessage = "Output copied to clipboard.";
1314
}
1415
}
16+
17+
private void Find_Executed(object sender, ExecutedRoutedEventArgs e) => findBar.Open();
18+
private void Find_Click(object sender, RoutedEventArgs e) => findBar.Open();
1519
}
1620
}

Modern/SQLCallstackResolver.Modern.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
<SubType>Designer</SubType>
122122
<Generator>MSBuild:Compile</Generator>
123123
</Page>
124+
<Page Include="Controls\FindBar.xaml">
125+
<SubType>Designer</SubType>
126+
<Generator>MSBuild:Compile</Generator>
127+
</Page>
124128
</ItemGroup>
125129
<ItemGroup>
126130
<Compile Include="App.xaml.cs">
@@ -171,6 +175,9 @@
171175
</Compile>
172176
<Compile Include="ResolverViewModel.cs" />
173177
<Compile Include="Utils.cs" />
178+
<Compile Include="Controls\FindBar.xaml.cs">
179+
<DependentUpon>FindBar.xaml</DependentUpon>
180+
</Compile>
174181
<Compile Include="Views\ClassicView.xaml.cs">
175182
<DependentUpon>ClassicView.xaml</DependentUpon>
176183
</Compile>

Modern/Views/ClassicView.xaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
<UserControl.Resources>
66
<local:BoolToVisibilityConverter x:Key="BoolToVis"/>
77
</UserControl.Resources>
8+
<UserControl.InputBindings>
9+
<KeyBinding Gesture="Ctrl+F" Command="Find"/>
10+
</UserControl.InputBindings>
11+
<UserControl.CommandBindings>
12+
<CommandBinding Command="Find" Executed="Find_Executed"/>
13+
</UserControl.CommandBindings>
814
<Grid>
915
<Grid.RowDefinitions>
1016
<RowDefinition Height="*" MinHeight="100"/>
@@ -35,7 +41,9 @@
3541

3642
<DockPanel Grid.Column="2">
3743
<TextBlock DockPanel.Dock="Top" Text="Output" FontWeight="SemiBold" Margin="0,0,0,2"/>
38-
<TextBox IsReadOnly="True" AcceptsReturn="True"
44+
<local:FindBar x:Name="findBar" DockPanel.Dock="Top"/>
45+
<TextBox x:Name="outputTextBox" IsReadOnly="True" AcceptsReturn="True"
46+
IsInactiveSelectionHighlightEnabled="True"
3947
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
4048
FontFamily="Consolas" FontSize="13" TextWrapping="NoWrap"
4149
Text="{Binding OutputText, Mode=OneWay}"/>

0 commit comments

Comments
 (0)