diff --git a/backend/app/search_client.py b/backend/app/search_client.py
index f9f0bd1..0a2aa7d 100644
--- a/backend/app/search_client.py
+++ b/backend/app/search_client.py
@@ -36,14 +36,21 @@ class SearchClient:
"highlightPostTag": "",
"attributesToCrop": ["content"],
"cropLength": 30,
+ "showMatchesPosition": True,
})
results = []
for hit in response.get("hits", []):
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({
"key": hit["key"],
"title": hit["title"],
"path": hit["path"],
"snippet": formatted.get("content", hit.get("content", "")),
+ "lineNumber": line_number,
})
return results
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
index db08fe7..bdf19d8 100644
--- a/frontend/src/api/client.ts
+++ b/frontend/src/api/client.ts
@@ -104,6 +104,7 @@ export interface SearchResult {
title: string;
path: string;
snippet: string;
+ lineNumber: number | null;
}
export interface SearchResponse {
diff --git a/frontend/src/components/SearchResults.tsx b/frontend/src/components/SearchResults.tsx
index ec58ef3..c87a653 100644
--- a/frontend/src/components/SearchResults.tsx
+++ b/frontend/src/components/SearchResults.tsx
@@ -32,6 +32,9 @@ export default function SearchResults({ results, query, onJumpToResult }: Search
{result.title}
+ {result.lineNumber != null && (
+ 第 {result.lineNumber} 行
+ )}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index c7a8abc..8ba3ad8 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -664,6 +664,17 @@ body {
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 {
font-size: 12.5px;
color: var(--ash);
diff --git a/frontend/tests/App.test.tsx b/frontend/tests/App.test.tsx
index fe8ec53..b9cf783 100644
--- a/frontend/tests/App.test.tsx
+++ b/frontend/tests/App.test.tsx
@@ -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 () => {
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
vi.mocked(search).mockResolvedValue({
- results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...' }],
+ results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...', lineNumber: 5 }],
});
vi.mocked(getDocument).mockResolvedValue({
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 () => {
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
vi.mocked(search).mockResolvedValue({
- results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...' }],
+ results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...', lineNumber: 5 }],
});
vi.mocked(getDocument).mockResolvedValue({
key: '产品文档/架构设计.md',
diff --git a/frontend/tests/components/SearchResults.test.tsx b/frontend/tests/components/SearchResults.test.tsx
index b440e56..0662853 100644
--- a/frontend/tests/components/SearchResults.test.tsx
+++ b/frontend/tests/components/SearchResults.test.tsx
@@ -12,6 +12,7 @@ test('renders result count, titles, and highlighted snippets', () => {
title: '架构设计.md',
path: '产品文档',
snippet: '检索:Meilisearch,支持 中文全文检索。',
+ lineNumber: null,
},
]}
onJumpToResult={vi.fn()}
@@ -28,7 +29,7 @@ test('clicking a result calls onJumpToResult with its key', () => {
render(
);