search v2

This commit is contained in:
Matt Parker
2025-09-23 22:37:02 +10:00
parent 6a95ea892c
commit 96dd10d296
8 changed files with 135 additions and 6 deletions

View File

@@ -0,0 +1,10 @@
using SharpIDE.Application.Features.SolutionDiscovery;
namespace SharpIDE.Application.Features.Search;
public class SearchResult
{
public required SharpIdeFile File { get; set; }
public required int LineNumber { get; set; }
public required string LineText { get; set; }
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO.MemoryMappedFiles;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
@@ -6,24 +7,34 @@ namespace SharpIDE.Application.Features.Search;
public static class SearchService
{
public static async Task FindInFiles(SharpIdeSolutionModel solutionModel, string searchTerm)
public static async Task<List<SearchResult>> FindInFiles(SharpIdeSolutionModel solutionModel, string searchTerm)
{
if (searchTerm.Length < 4)
{
return;
return [];
}
var timer = Stopwatch.StartNew();
var files = solutionModel.AllFiles;
ConcurrentBag<string> results = [];
ConcurrentBag<SearchResult> results = [];
await Parallel.ForEachAsync(files, async (file, ct) =>
{
await foreach (var (index, line) in File.ReadLinesAsync(file.Path, ct).Index().WithCancellation(ct))
{
if (line.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
{
results.Add($"{file.Path} (Line {index + 1}): {line.Trim()}");
results.Add(new SearchResult
{
File = file,
LineNumber = index + 1,
LineText = line.Trim()
});
}
}
}
);
timer.Stop();
Console.WriteLine($"Search completed in {timer.ElapsedMilliseconds} ms. Found {results.Count} results.");
return results.ToList();
}
}

View File

@@ -0,0 +1,29 @@
using Godot;
using SharpIDE.Application.Features.Search;
namespace SharpIDE.Godot.Features.Search;
public partial class SearchResultComponent : MarginContainer
{
private Label _matchingLineLabel = null!;
private Label _fileNameLabel = null!;
private Label _lineNumberLabel = null!;
public SearchResult Result { get; set; } = null!;
public override void _Ready()
{
_matchingLineLabel = GetNode<Label>("%MatchingLineLabel");
_fileNameLabel = GetNode<Label>("%FileNameLabel");
_lineNumberLabel = GetNode<Label>("%LineNumberLabel");
SetValue(Result);
}
private void SetValue(SearchResult result)
{
if (result is null) return;
_matchingLineLabel.Text = result.LineText;
_fileNameLabel.Text = result.File.Name;
_lineNumberLabel.Text = result.LineNumber.ToString();
}
}

View File

@@ -0,0 +1 @@
uid://6sdu34jtdrux

View File

@@ -0,0 +1,30 @@
[gd_scene load_steps=2 format=3 uid="uid://d358tex0duum8"]
[ext_resource type="Script" uid="uid://6sdu34jtdrux" path="res://Features/Search/SearchResultComponent.cs" id="1_6ov2c"]
[node name="SearchResultComponent" type="MarginContainer"]
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 23.0
grow_horizontal = 2
script = ExtResource("1_6ov2c")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="MatchingLineLabel" type="Label" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Line matching search result"
text_overrun_behavior = 3
[node name="FileNameLabel" type="Label" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "FileName.cs"
[node name="LineNumberLabel" type="Label" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "24"

View File

@@ -1,8 +1,41 @@
using Godot;
using SharpIDE.Application.Features.Search;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Godot.Features.Search;
public partial class SearchWindow : PopupPanel
{
private LineEdit _lineEdit = null!;
private VBoxContainer _searchResultsContainer = null!;
public SharpIdeSolutionModel Solution { get; set; } = null!;
private readonly PackedScene _searchResultEntryScene = ResourceLoader.Load<PackedScene>("res://Features/Search/SearchResultComponent.tscn");
public override void _Ready()
{
_lineEdit = GetNode<LineEdit>("%SearchLineEdit");
_searchResultsContainer = GetNode<VBoxContainer>("%SearchResultsVBoxContainer");
_lineEdit.TextChanged += OnTextChanged;
}
private async void OnTextChanged(string newText)
{
GD.Print("Search text changed");
await Task.GodotRun(() => Search(newText));
}
private async Task Search(string text)
{
var result = await SearchService.FindInFiles(Solution, text);
await this.InvokeAsync(() =>
{
_searchResultsContainer.GetChildren().ToList().ForEach(s => s.QueueFree());
foreach (var searchResult in result)
{
var result = _searchResultEntryScene.Instantiate<SearchResultComponent>();
result.Result = searchResult;
_searchResultsContainer.AddChild(result);
}
});
}
}

View File

@@ -1,6 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://8lk0qj233a7p"]
[gd_scene load_steps=3 format=3 uid="uid://8lk0qj233a7p"]
[ext_resource type="Script" uid="uid://bah6tmifl41ce" path="res://Features/Search/SearchWindow.cs" id="1_ft33p"]
[ext_resource type="PackedScene" uid="uid://d358tex0duum8" path="res://Features/Search/SearchResultComponent.tscn" id="2_cuaw5"]
[node name="SearchWindow" type="PopupPanel"]
oversampling_override = 1.0
@@ -40,6 +41,19 @@ layout_mode = 2
theme_override_font_sizes/font_size = 14
text = " 30 matches in 5 files"
[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer"]
[node name="SearchLineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Test"
[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="SearchResultsVBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="SearchResultComponent" parent="MarginContainer/VBoxContainer/ScrollContainer/SearchResultsVBoxContainer" instance=ExtResource("2_cuaw5")]
layout_mode = 2

View File

@@ -80,6 +80,7 @@ public partial class IdeRoot : Control
_solutionExplorerPanel.SolutionModel = solutionModel;
_codeEditorPanel.Solution = solutionModel;
_bottomPanelManager.Solution = solutionModel;
_searchWindow.Solution = solutionModel;
Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred();
RoslynAnalysis.StartSolutionAnalysis(solutionModel);