feat(search): implement KWIC context snippets and UI display

This commit is contained in:
sudo-Harshk 2025-12-04 22:19:52 +05:30
parent d7c50eaac0
commit 1bd4381077
2 changed files with 37 additions and 17 deletions

View File

@ -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,29 +283,48 @@ 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)
matched_lines.append({ context_end = min(len(content), end_index + 30)
"line_number": i + 1,
"context": context[:200] # Limit context length # 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({
"line_number": line_number,
"context": snippet
})
relative_path = md_file.relative_to(notes_path) relative_path = md_file.relative_to(notes_path)
results.append({ results.append({
"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

View File

@ -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>