Skip to content

Commit a68a4b4

Browse files
authored
Show more search results when searching code (#1167)
* Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code * Add option to show more results after searching code
1 parent 4e500ed commit a68a4b4

File tree

14 files changed

+194
-68
lines changed

14 files changed

+194
-68
lines changed

src/Controller/App/Search/SearchCodeController.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace DR\Review\Controller\App\Search;
55

66
use DR\Review\Controller\AbstractController;
7+
use DR\Review\Model\Search\SearchResultCollection;
78
use DR\Review\Repository\Config\RepositoryRepository;
89
use DR\Review\Request\Search\SearchCodeRequest;
910
use DR\Review\Security\Role\Roles;
@@ -39,19 +40,19 @@ public function __invoke(SearchCodeRequest $request): array
3940
$extensions = $request->getExtensions();
4041
if (strlen($searchQuery) < 5) {
4142
$this->addFlash('error', $this->translator->trans('search.much.be.minimum.5.characters'));
42-
$files = [];
43+
$results = new SearchResultCollection([], false);
4344
} else {
4445
$this->stopwatch?->start('file-search');
4546

4647
$repositories = $this->repositoryRepository->findBy(['active' => true]);
47-
$files = $this->fileSearcher->find($searchQuery, $extensions, $repositories);
48+
$results = $this->fileSearcher->find($searchQuery, $extensions, $repositories, $request->isShowAll() ? null : 100);
4849

4950
$this->stopwatch?->stop('file-search');
5051
}
5152

5253
return [
53-
'page_title' => $this->translator->trans('code.search'),
54-
'viewModel' => new SearchCodeViewModel($files, $searchQuery, $extensions === null ? null : implode(',', $extensions))
54+
'page_title' => $this->translator->trans('code.search'),
55+
'viewModel' => new SearchCodeViewModel($results, $searchQuery, $extensions === null ? null : implode(',', $extensions))
5556
];
5657
}
5758
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace DR\Review\Model\Search;
5+
6+
readonly class SearchResultCollection
7+
{
8+
/**
9+
* @codeCoverageIgnore Simple DTO
10+
*
11+
* @param SearchResult[] $results
12+
*/
13+
public function __construct(public array $results, public bool $moreResultsAvailable)
14+
{
15+
}
16+
17+
/**
18+
* @return iterable<int, SearchResult[]>
19+
*/
20+
public function iteratePerRepository(): iterable
21+
{
22+
$grouped = [];
23+
foreach ($this->results as $result) {
24+
$repoId = (int)$result->repository->getId();
25+
$grouped[$repoId] ??= [];
26+
$grouped[$repoId][] = $result;
27+
}
28+
foreach ($grouped as $repoId => $results) {
29+
yield $repoId => $results;
30+
}
31+
}
32+
}

src/Request/Search/SearchCodeRequest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ public function getExtensions(): ?array
2424
return count($extensions) === 0 ? null : $extensions;
2525
}
2626

27+
public function isShowAll(): bool
28+
{
29+
return $this->request->query->getBoolean('all');
30+
}
31+
2732
protected function getValidationRules(): ?ValidationRules
2833
{
2934
return new ValidationRules(
3035
[
3136
'query' => [
3237
'search' => 'required|string',
3338
'extension' => 'string|regex:/^[a-zA-Z0-9]{1,5}(,[a-zA-Z0-9]{1,5})*$/',
39+
'all' => 'string',
3440
]
3541
]
3642
);

src/Service/Search/RipGrep/GitFileSearcher.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace DR\Review\Service\Search\RipGrep;
55

66
use DR\Review\Entity\Repository\Repository;
7-
use DR\Review\Model\Search\SearchResult;
7+
use DR\Review\Model\Search\SearchResultCollection;
88
use DR\Review\Service\Search\RipGrep\Command\RipGrepCommandBuilderFactory;
99
use DR\Review\Service\Search\RipGrep\Command\RipGrepProcessExecutor;
1010
use DR\Review\Service\Search\RipGrep\Iterator\JsonDecodeIterator;
@@ -22,10 +22,8 @@ public function __construct(
2222
/**
2323
* @param non-empty-array<string> $extensions
2424
* @param Repository[] $repositories
25-
*
26-
* @return SearchResult[]
2725
*/
28-
public function find(string $searchQuery, ?array $extensions, array $repositories): array
26+
public function find(string $searchQuery, ?array $extensions, array $repositories, ?int $limit = null): SearchResultCollection
2927
{
3028
$command = $this->commandBuilderFactory->default();
3129
$command->search($searchQuery);
@@ -35,6 +33,6 @@ public function find(string $searchQuery, ?array $extensions, array $repositorie
3533

3634
$jsonIterator = new JsonDecodeIterator($this->executor->execute($command, $this->gitCacheDirectory));
3735

38-
return $this->parser->parse($jsonIterator, $repositories);
36+
return $this->parser->parse($jsonIterator, $repositories, $limit);
3937
}
4038
}

src/Service/Search/RipGrep/SearchResultLineFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class SearchResultLineFactory
1414
public function createContextFromEntry(array $entry): SearchResultLine
1515
{
1616
return new SearchResultLine(
17-
$entry['data']['lines']['text'],
17+
$entry['data']['lines']['text'] ?? '',
1818
$entry['data']['line_number'],
1919
SearchResultLineTypeEnum::Context
2020
);
@@ -26,7 +26,7 @@ public function createContextFromEntry(array $entry): SearchResultLine
2626
public function createMatchFromEntry(array $entry): SearchResultLine
2727
{
2828
return new SearchResultLine(
29-
$entry['data']['lines']['text'],
29+
$entry['data']['lines']['text'] ?? '',
3030
$entry['data']['line_number'],
3131
SearchResultLineTypeEnum::Match
3232
);

src/Service/Search/RipGrep/SearchResultLineParser.php

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace DR\Review\Service\Search\RipGrep;
55

66
use DR\Review\Entity\Repository\Repository;
7-
use DR\Review\Model\Search\SearchResult;
7+
use DR\Review\Model\Search\SearchResultCollection;
88
use DR\Review\Service\Search\RipGrep\Iterator\JsonDecodeIterator;
99

1010
/**
@@ -22,13 +22,12 @@ public function __construct(
2222
/**
2323
* @param iterable<int, SearchResultEntry> $iterator
2424
* @param Repository[] $repositories
25-
*
26-
* @return SearchResult[]
2725
*/
28-
public function parse(iterable $iterator, array $repositories): array
26+
public function parse(iterable $iterator, array $repositories, ?int $limit = null): SearchResultCollection
2927
{
30-
$results = [];
31-
$current = null;
28+
$results = [];
29+
$current = null;
30+
$moreResultsAvailable = false;
3231
foreach ($iterator as $entry) {
3332
if ($entry['type'] === 'begin') {
3433
$current = $this->resultFactory->create($entry['data']['path']['text'], $this->gitCacheDirectory, $repositories);
@@ -48,13 +47,12 @@ public function parse(iterable $iterator, array $repositories): array
4847
$current->addLine($this->resultLineFactory->createMatchFromEntry($entry));
4948
}
5049

51-
// @codeCoverageIgnoreStart
52-
if (count($results) >= 100) {
50+
if ($limit !== null && count($results) >= $limit) {
51+
$moreResultsAvailable = true;
5352
break;
5453
}
55-
// @codeCoverageIgnoreEnd
5654
}
5755

58-
return $results;
56+
return new SearchResultCollection($results, $moreResultsAvailable);
5957
}
6058
}

src/ViewModel/App/Search/SearchCodeViewModel.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33

44
namespace DR\Review\ViewModel\App\Search;
55

6-
use DR\Review\Model\Search\SearchResult;
6+
use DR\Review\Model\Search\SearchResultCollection;
77

88
readonly class SearchCodeViewModel
99
{
1010
/**
1111
* @codeCoverageIgnore Simple DTO
12-
*
13-
* @param SearchResult[] $files
1412
*/
15-
public function __construct(public array $files, public string $searchQuery, public ?string $fileExtension)
13+
public function __construct(public SearchResultCollection $searchResults, public string $searchQuery, public ?string $fileExtension)
1614
{
1715
}
1816
}

templates/app/search/code.search.html.twig

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,35 +33,50 @@
3333
</form>
3434
</div>
3535

36-
{% if viewModel.files %}
37-
<div class="accordion" {{ stimulus_controller('accordion') }}>
38-
{% for resultFile in viewModel.files %}
39-
<div class="accordion-item" data-role="accordion-item">
40-
<h2 class="accordion-header">
41-
<button class="accordion-button collapsed" type="button" {{ stimulus_action('accordion', 'toggle', 'click') }}>
36+
{% if viewModel.searchResults.results|length > 0 %}
37+
<div class="mb-3">
38+
{% for results in viewModel.searchResults.iteratePerRepository %}
39+
<div>
40+
<div class="mt-2 mb-1">
4241
<span class="badge rounded-pill text-bg-primary me-2">
43-
{{ resultFile.repository.displayName }}
42+
{{ results[0].repository.displayName }}
4443
</span>
45-
{{ resultFile.file.relativePathname }}
46-
</button>
47-
</h2>
48-
<div class="accordion-collapse collapse" data-role="accordion-collapse">
49-
<div class="accordion-body">
50-
<pre class="d-block">
51-
{%- for line in resultFile.lines -%}
52-
<div {% if line.type.value == 'match' %}class="bg-success-subtle"{% endif %}>
53-
{#- show code line -#}
54-
<span class="text-danger bg-light-gray d-inline-block code-search-line-number">
55-
{{- line.lineNumber -}}
56-
</span> {{ line.line -}}
44+
</div>
45+
<div class="accordion" {{ stimulus_controller('accordion') }}>
46+
{% for resultFile in results %}
47+
<div class="accordion-item" data-role="accordion-item">
48+
<h2 class="accordion-header">
49+
<button class="accordion-button collapsed" type="button" {{ stimulus_action('accordion', 'toggle', 'click') }}>
50+
{{ resultFile.file.relativePathname }}
51+
</button>
52+
</h2>
53+
<div class="accordion-collapse collapse" data-role="accordion-collapse">
54+
<div class="accordion-body">
55+
<pre class="d-block">
56+
{%- for line in resultFile.lines -%}
57+
<div {% if line.type.value == 'match' %}class="bg-success-subtle"{% endif %}>
58+
{#- show code line -#}
59+
<span class="text-danger bg-light-gray d-inline-block code-search-line-number">
60+
{{- line.lineNumber -}}
61+
</span> {{ line.line -}}
62+
</div>
63+
{%- endfor -%}
64+
</pre>
65+
</div>
5766
</div>
58-
{%- endfor -%}
59-
</pre>
67+
</div>
68+
{% endfor %}
6069
</div>
6170
</div>
62-
</div>
63-
{% endfor %}
64-
</div>
71+
{% endfor %}
72+
{% if viewModel.searchResults.moreResultsAvailable %}
73+
<div class="mt-2">
74+
<a href="?search={{ viewModel.searchQuery|escape('url') }}&extension={{ viewModel.fileExtension|escape('url') }}&all=true"
75+
type="button"
76+
class="btn btn-outline-primary">{{ 'show.all'|trans }}</a>
77+
</div>
78+
{% endif %}
79+
</div>
6580
{% else %}
6681
<div class="alert alert-warning">
6782
{{ 'no.matches.found'|trans }}

tests/Unit/Controller/App/Search/SearchCodeControllerTest.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
use DR\PHPUnitExtensions\Symfony\AbstractControllerTestCase;
77
use DR\Review\Controller\App\Search\SearchCodeController;
88
use DR\Review\Entity\Repository\Repository;
9-
use DR\Review\Model\Search\SearchResult;
9+
use DR\Review\Model\Search\SearchResultCollection;
1010
use DR\Review\Repository\Config\RepositoryRepository;
1111
use DR\Review\Request\Search\SearchCodeRequest;
1212
use DR\Review\Service\Search\RipGrep\GitFileSearcher;
1313
use DR\Review\ViewModel\App\Search\SearchCodeViewModel;
1414
use PHPUnit\Framework\Attributes\CoversClass;
1515
use PHPUnit\Framework\MockObject\MockObject;
1616
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
17-
use Symfony\Component\Finder\SplFileInfo;
1817
use Symfony\Contracts\Translation\TranslatorInterface;
1918
use function DR\PHPUnitExtensions\Mock\consecutive;
2019

@@ -41,6 +40,7 @@ public function testInvokeWithTooShortQuery(): void
4140
$request = $this->createMock(SearchCodeRequest::class);
4241
$request->method('getSearchQuery')->willReturn('fail');
4342
$request->method('getExtensions')->willReturn(null);
43+
$request->method('isShowAll')->willReturn(false);
4444

4545
$this->translator->expects($this->exactly(2))->method('trans')
4646
->with(...consecutive(['search.much.be.minimum.5.characters'], ['code.search']))
@@ -51,7 +51,7 @@ public function testInvokeWithTooShortQuery(): void
5151
$result = ($this->controller)($request);
5252

5353
static::assertEquals(
54-
['page_title' => 'translation2', 'viewModel' => new SearchCodeViewModel([], 'fail', null)],
54+
['page_title' => 'translation2', 'viewModel' => new SearchCodeViewModel(new SearchResultCollection([], false), 'fail', null)],
5555
$result
5656
);
5757
}
@@ -61,13 +61,16 @@ public function testInvokeWithSearch(): void
6161
$request = $this->createMock(SearchCodeRequest::class);
6262
$request->method('getSearchQuery')->willReturn('success');
6363
$request->method('getExtensions')->willReturn(['json', 'yaml']);
64+
$request->method('isShowAll')->willReturn(false);
6465

6566
$repository = new Repository();
66-
$searchResults = [new SearchResult($repository, new SplFileInfo('file', '', ''))];
67+
$searchResults = static::createStub(SearchResultCollection::class);
6768

6869
$this->translator->expects($this->once())->method('trans')->with('code.search')->willReturn('translation');
6970
$this->repositoryRepository->expects($this->once())->method('findBy')->with(['active' => true])->willReturn([$repository]);
70-
$this->fileSearcher->expects($this->once())->method('find')->with('success', ['json', 'yaml'], [$repository])->willReturn($searchResults);
71+
$this->fileSearcher->expects($this->once())->method('find')
72+
->with('success', ['json', 'yaml'], [$repository], 100)
73+
->willReturn($searchResults);
7174

7275
$result = ($this->controller)($request);
7376

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace DR\Review\Tests\Unit\Model\Search;
5+
6+
use DR\Review\Entity\Repository\Repository;
7+
use DR\Review\Model\Search\SearchResult;
8+
use DR\Review\Model\Search\SearchResultCollection;
9+
use DR\Review\Tests\AbstractTestCase;
10+
use PHPUnit\Framework\Attributes\CoversClass;
11+
use Symfony\Component\Finder\SplFileInfo;
12+
13+
#[CoversClass(SearchResultCollection::class)]
14+
class SearchResultCollectionTest extends AbstractTestCase
15+
{
16+
public function testIteratePerRepositoryEmptyCollection(): void
17+
{
18+
$collection = new SearchResultCollection([], false);
19+
$results = iterator_to_array($collection->iteratePerRepository());
20+
static::assertCount(0, $results);
21+
}
22+
23+
public function testIteratePerRepository(): void
24+
{
25+
$repository = (new Repository())->setId(123);
26+
$result = new SearchResult($repository, new SplFileInfo('file', '', ''));
27+
28+
$collection = new SearchResultCollection([$result], false);
29+
30+
$results = iterator_to_array($collection->iteratePerRepository());
31+
static::assertSame([123 => [$result]], $results);
32+
}
33+
}

0 commit comments

Comments
 (0)