refactor: consolidate note-list body search
This commit is contained in:
@@ -118,10 +118,8 @@ interface NoteListSearchMockResult {
|
||||
}
|
||||
|
||||
function installFullTextSearchMocks({
|
||||
contentByPath,
|
||||
resultsByVault,
|
||||
}: {
|
||||
contentByPath: Record<string, string>
|
||||
resultsByVault: Record<string, NoteListSearchMockResult[]>
|
||||
}) {
|
||||
const originalContentHandler = window.__mockHandlers?.get_note_content
|
||||
@@ -132,7 +130,9 @@ function installFullTextSearchMocks({
|
||||
query: args?.query,
|
||||
results: resultsByVault[String(args?.vaultPath ?? '')] ?? [],
|
||||
}))
|
||||
const getNoteContent = vi.fn((args?: Record<string, unknown>) => contentByPath[String(args?.path ?? '')] ?? '')
|
||||
const getNoteContent = vi.fn(() => {
|
||||
throw new Error('Note-list full-text search should not read note content in React')
|
||||
})
|
||||
|
||||
if (!window.__mockHandlers) window.__mockHandlers = {}
|
||||
window.__mockHandlers.search_vault = searchVault
|
||||
@@ -291,10 +291,7 @@ describe('NoteList rendering', () => {
|
||||
})
|
||||
|
||||
it('filters by full note content when the title and snippet do not match', async () => {
|
||||
const { restore, searchVault } = installFullTextSearchMocks({
|
||||
contentByPath: {
|
||||
'/vault/b.md': '# Beta Note\n\nA private subterranean-keyword body match.',
|
||||
},
|
||||
const { getNoteContent, restore, searchVault } = installFullTextSearchMocks({
|
||||
resultsByVault: {
|
||||
'/vault': [{
|
||||
note_type: 'Note',
|
||||
@@ -321,8 +318,10 @@ describe('NoteList rendering', () => {
|
||||
vaultPath: '/vault',
|
||||
query: 'subterranean-keyword',
|
||||
mode: 'keyword',
|
||||
excludeFrontmatter: true,
|
||||
}))
|
||||
})
|
||||
expect(getNoteContent).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('Beta Note')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Alpha Note')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Private body match is intentionally not rendered here.')).not.toBeInTheDocument()
|
||||
@@ -332,26 +331,9 @@ describe('NoteList rendering', () => {
|
||||
})
|
||||
|
||||
it('ignores full-content matches that only appear in hidden frontmatter', async () => {
|
||||
const { restore } = installFullTextSearchMocks({
|
||||
contentByPath: {
|
||||
'/vault/a.md': [
|
||||
'---',
|
||||
'Owner: hidden-frontmatter-keyword',
|
||||
'---',
|
||||
'',
|
||||
'# Alpha Note',
|
||||
'',
|
||||
'Public body text omits the private property value.',
|
||||
].join('\n'),
|
||||
},
|
||||
const { getNoteContent, restore, searchVault } = installFullTextSearchMocks({
|
||||
resultsByVault: {
|
||||
'/vault': [{
|
||||
note_type: 'Note',
|
||||
path: '/vault/a.md',
|
||||
score: 1,
|
||||
snippet: 'Owner: hidden-frontmatter-keyword',
|
||||
title: 'Alpha Note',
|
||||
}],
|
||||
'/vault': [],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -365,6 +347,12 @@ describe('NoteList rendering', () => {
|
||||
|
||||
await searchNoteList('hidden-frontmatter-keyword')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({
|
||||
excludeFrontmatter: true,
|
||||
}))
|
||||
})
|
||||
expect(getNoteContent).not.toHaveBeenCalled()
|
||||
expect(screen.queryByText('Alpha Note')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('No matching notes')).toBeInTheDocument()
|
||||
} finally {
|
||||
@@ -373,11 +361,7 @@ describe('NoteList rendering', () => {
|
||||
})
|
||||
|
||||
it('runs full-content note-list search across visible workspaces', async () => {
|
||||
const { restore, searchVault } = installFullTextSearchMocks({
|
||||
contentByPath: {
|
||||
'/personal/personal-note.md': '# Personal Note\n\nNo matching body token.',
|
||||
'/team/team-body-hit.md': '# Team Body Hit\n\nPrivate workspace-only-keyword body hit.',
|
||||
},
|
||||
const { getNoteContent, restore, searchVault } = installFullTextSearchMocks({
|
||||
resultsByVault: {
|
||||
'/team': [{
|
||||
note_type: 'Note',
|
||||
@@ -412,9 +396,10 @@ describe('NoteList rendering', () => {
|
||||
await searchNoteList('workspace-only-keyword')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/personal' }))
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/team' }))
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/personal', excludeFrontmatter: true }))
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/team', excludeFrontmatter: true }))
|
||||
})
|
||||
expect(getNoteContent).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('Team Body Hit')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Personal Note')).not.toBeInTheDocument()
|
||||
} finally {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import type { VaultEntry } from '../../types'
|
||||
import { isTauri, mockInvoke } from '../../mock-tauri'
|
||||
|
||||
type MarkdownContent = string
|
||||
type NoteListSearchQuery = string
|
||||
type NotePath = string
|
||||
type VaultPath = string
|
||||
@@ -22,6 +21,7 @@ interface SearchCommandArgs extends Record<string, unknown> {
|
||||
query: NoteListSearchQuery
|
||||
mode: 'keyword'
|
||||
limit: number
|
||||
excludeFrontmatter: true
|
||||
}
|
||||
|
||||
interface FullTextSearchRequest {
|
||||
@@ -85,51 +85,14 @@ function resolveNoteListSearchVaultPaths(entries: VaultEntry[]): VaultPath[] {
|
||||
return root ? [root] : []
|
||||
}
|
||||
|
||||
function stripFrontmatter(content: MarkdownContent): MarkdownContent {
|
||||
const lineEnding = content.startsWith('---\r\n')
|
||||
? '\r\n'
|
||||
: content.startsWith('---\n') ? '\n' : null
|
||||
if (!lineEnding) return content
|
||||
|
||||
const afterOpen = content.slice(3 + lineEnding.length)
|
||||
const closeMarker = `${lineEnding}---`
|
||||
const closeIndex = afterOpen.indexOf(closeMarker)
|
||||
if (closeIndex === -1) return content
|
||||
|
||||
return stripFrontmatterCloseLine(afterOpen.slice(closeIndex + closeMarker.length))
|
||||
}
|
||||
|
||||
function stripFrontmatterCloseLine(content: MarkdownContent): MarkdownContent {
|
||||
if (content.startsWith('\r\n')) return content.slice(2)
|
||||
return content.startsWith('\n') ? content.slice(1) : content
|
||||
}
|
||||
|
||||
function searchVault(args: SearchCommandArgs): Promise<SearchResponseData> {
|
||||
return isTauri()
|
||||
? invoke<SearchResponseData>('search_vault', args)
|
||||
: mockInvoke<SearchResponseData>('search_vault', args)
|
||||
}
|
||||
|
||||
function readNoteContent(path: NotePath): Promise<MarkdownContent> {
|
||||
return isTauri()
|
||||
? invoke<MarkdownContent>('get_note_content', { path })
|
||||
: mockInvoke<MarkdownContent>('get_note_content', { path })
|
||||
}
|
||||
|
||||
async function bodyContainsQuery({ path, query }: { path: NotePath; query: NoteListSearchQuery }): Promise<boolean> {
|
||||
try {
|
||||
const body = stripFrontmatter(await readNoteContent(path))
|
||||
return body.toLowerCase().includes(query)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveBodyResultPaths({ paths, query }: { paths: NotePath[]; query: NoteListSearchQuery }): Promise<Set<string>> {
|
||||
const matches = await Promise.all(unique(paths).map(async (path) => (
|
||||
await bodyContainsQuery({ path, query }) ? path : null
|
||||
)))
|
||||
return new Set(matches.filter((path): path is string => !!path))
|
||||
function resolveResultPaths(responses: SearchResponseData[]): Set<string> {
|
||||
return new Set(unique(responses.flatMap((response) => response.results.map((result) => result.path))))
|
||||
}
|
||||
|
||||
async function runFullTextSearch(request: FullTextSearchRequest): Promise<Set<string>> {
|
||||
@@ -138,11 +101,9 @@ async function runFullTextSearch(request: FullTextSearchRequest): Promise<Set<st
|
||||
query: request.query,
|
||||
mode: 'keyword',
|
||||
limit: request.limit,
|
||||
excludeFrontmatter: true,
|
||||
})))
|
||||
return resolveBodyResultPaths({
|
||||
paths: responses.flatMap((response) => response.results.map((result) => result.path)),
|
||||
query: request.query,
|
||||
})
|
||||
return resolveResultPaths(responses)
|
||||
}
|
||||
|
||||
function createSearchRequest({ entries, query }: { entries: VaultEntry[]; query: NoteListSearchQuery }): FullTextSearchRequest | null {
|
||||
|
||||
Reference in New Issue
Block a user