feat(search): implement KWIC context snippets and UI display
This commit is contained in:
parent
d7c50eaac0
commit
1bd4381077
|
|
@ -273,9 +273,9 @@ def search_notes(notes_dir: str, query: str) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Full-text search through note contents only.
|
Full-text search through note contents only.
|
||||||
Does NOT search in file names, folder names, or paths - only note content.
|
Does NOT search in file names, folder names, or paths - only note content.
|
||||||
|
Uses character-based context extraction for better snippet quality.
|
||||||
"""
|
"""
|
||||||
results = []
|
results = []
|
||||||
query_lower = query.lower()
|
|
||||||
notes_path = Path(notes_dir)
|
notes_path = Path(notes_dir)
|
||||||
|
|
||||||
for md_file in notes_path.rglob("*.md"):
|
for md_file in notes_path.rglob("*.md"):
|
||||||
|
|
@ -283,21 +283,40 @@ def search_notes(notes_dir: str, query: str) -> List[Dict]:
|
||||||
with open(md_file, 'r', encoding='utf-8') as f:
|
with open(md_file, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Only search in note content (not file name, folder name, or path)
|
# Find all matches using regex (case-insensitive)
|
||||||
if query_lower in content.lower():
|
matches = list(re.finditer(re.escape(query), content, re.IGNORECASE))
|
||||||
# Find context around match
|
|
||||||
lines = content.split('\n')
|
if matches:
|
||||||
matched_lines = []
|
matched_lines = []
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
for match in matches[:3]: # Limit to 3 matches per file
|
||||||
if query_lower in line.lower():
|
start_index = match.start()
|
||||||
# Get surrounding context
|
end_index = match.end()
|
||||||
start = max(0, i - 1)
|
|
||||||
end = min(len(lines), i + 2)
|
# Create slice window: ±30 characters around match
|
||||||
context = '\n'.join(lines[start:end])
|
context_start = max(0, start_index - 30)
|
||||||
|
context_end = min(len(content), end_index + 30)
|
||||||
|
|
||||||
|
# Extract snippet
|
||||||
|
snippet = content[context_start:context_end]
|
||||||
|
|
||||||
|
# Replace all newlines with spaces to ensure UI doesn't break
|
||||||
|
snippet = snippet.replace('\n', ' ')
|
||||||
|
|
||||||
|
# Add ellipsis if truncated at start
|
||||||
|
if context_start > 0:
|
||||||
|
snippet = '...' + snippet
|
||||||
|
|
||||||
|
# Add ellipsis if truncated at end
|
||||||
|
if context_end < len(content):
|
||||||
|
snippet = snippet + '...'
|
||||||
|
|
||||||
|
# Calculate line number by counting newlines up to match start
|
||||||
|
line_number = content.count('\n', 0, start_index) + 1
|
||||||
|
|
||||||
matched_lines.append({
|
matched_lines.append({
|
||||||
"line_number": i + 1,
|
"line_number": line_number,
|
||||||
"context": context[:200] # Limit context length
|
"context": snippet
|
||||||
})
|
})
|
||||||
|
|
||||||
relative_path = md_file.relative_to(notes_path)
|
relative_path = md_file.relative_to(notes_path)
|
||||||
|
|
@ -305,7 +324,7 @@ def search_notes(notes_dir: str, query: str) -> List[Dict]:
|
||||||
"name": md_file.stem,
|
"name": md_file.stem,
|
||||||
"path": str(relative_path.as_posix()),
|
"path": str(relative_path.as_posix()),
|
||||||
"folder": str(relative_path.parent.as_posix()) if str(relative_path.parent) != "." else "",
|
"folder": str(relative_path.parent.as_posix()) if str(relative_path.parent) != "." else "",
|
||||||
"matches": matched_lines[:3] # Limit to 3 matches per file
|
"matches": matched_lines
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -933,9 +933,10 @@
|
||||||
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
|
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
|
||||||
onmouseover="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='var(--bg-hover)'"
|
onmouseover="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='var(--bg-hover)'"
|
||||||
onmouseout="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='transparent'"
|
onmouseout="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='transparent'"
|
||||||
|
:title="note.path"
|
||||||
>
|
>
|
||||||
<div class="font-medium truncate" x-text="note.name"></div>
|
<div class="font-medium truncate" x-text="note.name"></div>
|
||||||
<div class="text-xs truncate" style="color: var(--text-tertiary);" x-text="note.folder || 'Root'"></div>
|
<div class="text-xs truncate" style="color: var(--text-tertiary);" x-text="(note.matches && note.matches.length > 0) ? note.matches[0].context : (note.folder || 'Root')"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue