feat: show line number in search results using match position
This commit is contained in:
parent
2b287ccb35
commit
c6cc9b7146
@ -36,14 +36,21 @@ class SearchClient:
|
|||||||
"highlightPostTag": "</em>",
|
"highlightPostTag": "</em>",
|
||||||
"attributesToCrop": ["content"],
|
"attributesToCrop": ["content"],
|
||||||
"cropLength": 30,
|
"cropLength": 30,
|
||||||
|
"showMatchesPosition": True,
|
||||||
})
|
})
|
||||||
results = []
|
results = []
|
||||||
for hit in response.get("hits", []):
|
for hit in response.get("hits", []):
|
||||||
formatted = hit.get("_formatted", {})
|
formatted = hit.get("_formatted", {})
|
||||||
|
line_number = None
|
||||||
|
matches = hit.get("_matchesPosition", {}).get("content", [])
|
||||||
|
if matches and hit.get("content"):
|
||||||
|
start = matches[0]["start"]
|
||||||
|
line_number = hit["content"][:start].count("\n") + 1
|
||||||
results.append({
|
results.append({
|
||||||
"key": hit["key"],
|
"key": hit["key"],
|
||||||
"title": hit["title"],
|
"title": hit["title"],
|
||||||
"path": hit["path"],
|
"path": hit["path"],
|
||||||
"snippet": formatted.get("content", hit.get("content", "")),
|
"snippet": formatted.get("content", hit.get("content", "")),
|
||||||
|
"lineNumber": line_number,
|
||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
|
|||||||
@ -104,6 +104,7 @@ export interface SearchResult {
|
|||||||
title: string;
|
title: string;
|
||||||
path: string;
|
path: string;
|
||||||
snippet: string;
|
snippet: string;
|
||||||
|
lineNumber: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResponse {
|
export interface SearchResponse {
|
||||||
|
|||||||
@ -32,6 +32,9 @@ export default function SearchResults({ results, query, onJumpToResult }: Search
|
|||||||
<div className="result-body">
|
<div className="result-body">
|
||||||
<div className="result-path">
|
<div className="result-path">
|
||||||
<span className="result-title">{result.title}</span>
|
<span className="result-title">{result.title}</span>
|
||||||
|
{result.lineNumber != null && (
|
||||||
|
<span className="result-line">第 {result.lineNumber} 行</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="result-snippet" dangerouslySetInnerHTML={{ __html: result.snippet }} />
|
<div className="result-snippet" dangerouslySetInnerHTML={{ __html: result.snippet }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -664,6 +664,17 @@ body {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-line {
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ash);
|
||||||
|
background: var(--paper-alt);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.result-snippet {
|
.result-snippet {
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
color: var(--ash);
|
color: var(--ash);
|
||||||
|
|||||||
@ -85,7 +85,7 @@ test('selecting a file from the tree loads it into the editor', async () => {
|
|||||||
test('searching shows the results overlay, and clicking a result opens the document', async () => {
|
test('searching shows the results overlay, and clicking a result opens the document', async () => {
|
||||||
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
|
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
|
||||||
vi.mocked(search).mockResolvedValue({
|
vi.mocked(search).mockResolvedValue({
|
||||||
results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...<em>检索</em>...' }],
|
results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...<em>检索</em>...', lineNumber: 5 }],
|
||||||
});
|
});
|
||||||
vi.mocked(getDocument).mockResolvedValue({
|
vi.mocked(getDocument).mockResolvedValue({
|
||||||
key: '产品文档/架构设计.md',
|
key: '产品文档/架构设计.md',
|
||||||
@ -121,7 +121,7 @@ test('searching shows the results overlay, and clicking a result opens the docum
|
|||||||
test('jumping to a search result clears the search input so it does not show stale text', async () => {
|
test('jumping to a search result clears the search input so it does not show stale text', async () => {
|
||||||
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
|
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
|
||||||
vi.mocked(search).mockResolvedValue({
|
vi.mocked(search).mockResolvedValue({
|
||||||
results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...<em>检索</em>...' }],
|
results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...<em>检索</em>...', lineNumber: 5 }],
|
||||||
});
|
});
|
||||||
vi.mocked(getDocument).mockResolvedValue({
|
vi.mocked(getDocument).mockResolvedValue({
|
||||||
key: '产品文档/架构设计.md',
|
key: '产品文档/架构设计.md',
|
||||||
|
|||||||
@ -12,6 +12,7 @@ test('renders result count, titles, and highlighted snippets', () => {
|
|||||||
title: '架构设计.md',
|
title: '架构设计.md',
|
||||||
path: '产品文档',
|
path: '产品文档',
|
||||||
snippet: '检索:Meilisearch,支持 <em>中文全文检索</em>。',
|
snippet: '检索:Meilisearch,支持 <em>中文全文检索</em>。',
|
||||||
|
lineNumber: null,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onJumpToResult={vi.fn()}
|
onJumpToResult={vi.fn()}
|
||||||
@ -28,7 +29,7 @@ test('clicking a result calls onJumpToResult with its key', () => {
|
|||||||
render(
|
render(
|
||||||
<SearchResults
|
<SearchResults
|
||||||
query="检索"
|
query="检索"
|
||||||
results={[{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...' }]}
|
results={[{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...', lineNumber: null }]}
|
||||||
onJumpToResult={onJumpToResult}
|
onJumpToResult={onJumpToResult}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user