-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Add Sidebar, Search, and Tags - Epic 3 Organization Features #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…arching and filtering
… command integration and settings
… stop word filtering
… performance improvements - Updated changelog for version 0.3.0 to reflect new testing and performance metrics. - Increased unit test coverage for SearchManager, adding 35 new tests across various functionalities including indexing, full-text search, regex search, filtering, caching, and history management. - Improved performance benchmarks for search operations with detailed logging. - Added new test cases for edge scenarios, including handling of stop words, empty queries, and special characters. - Updated user story documentation to reflect progress and completed testing phases. - Created a new test suite for SearchManager, ensuring robust validation of search functionalities.
- Added unit tests for TagManager covering validation, normalization, filtering, and statistics. - Enhanced SearchManager tests to include tag filtering and indexing scenarios. - Updated StorageManager tests to ensure proper serialization and deserialization of notes with tags. - Modified Note interface to include optional tags property for better note management. - Ensured tag handling accommodates various edge cases, including special characters and empty arrays.
Complete the documentation task for the tags and categories feature with comprehensive examples and usage guide. - Add comprehensive Tags & Categories section to README.md (169 lines) - Document all 7 predefined categories with colors, icons, and purposes - Add step-by-step guide for adding tags to notes - Include examples of tag usage across different scenarios - Explain tag autocomplete functionality - Document tag filtering in sidebar and search - Add tag validation rules and best practices - Include keyboard workflows and use case examples - Update Features section to highlight new capabilities - Update storage format example to include tags field - Update Roadmap to show tags as implemented feature - Update user story to mark all tasks complete (11/11 - 100%) - Clean up changelog formatting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe PR introduces comprehensive tag/category management and advanced search capabilities to the Code Context Notes extension. Changes include new search indexing with full-text and regex support, a sidebar tree view provider for note navigation, tag management utilities with validation and filtering, UI components for tag input and search, updated type definitions to support tags, extended configuration options, and extensive documentation with user stories and architecture details. Changes
Sequence DiagramssequenceDiagram
participant User
participant CommentCtrl as CommentController
participant TagInputUI
participant TagManager
participant NoteManager
participant StorageManager
User->>CommentCtrl: Create Note
CommentCtrl->>TagInputUI: Dynamic import & show tag input
TagInputUI->>TagManager: Get predefined categories & existing tags
TagManager-->>TagInputUI: Return categories with styling
User->>TagInputUI: Select/input tags
TagInputUI->>TagManager: Validate & normalize tags
TagManager-->>TagInputUI: Return normalized tags
TagInputUI-->>CommentCtrl: Return selected tags[]
CommentCtrl->>NoteManager: Create note with tags
NoteManager->>StorageManager: Save note (includes tags)
StorageManager->>StorageManager: Serialize tags to markdown
StorageManager-->>NoteManager: Note created
NoteManager-->>CommentCtrl: Return Note with tags
sequenceDiagram
participant User
participant SearchUI
participant SearchManager
participant StorageManager
participant NoteManager
Note over SearchManager: Background: Build index on activation
StorageManager->>SearchManager: Load all notes
SearchManager->>SearchManager: Index: tokenize text, build inverted index<br/>populate tag index, cache metadata
User->>SearchUI: Open search (Ctrl+Shift+F)
SearchUI->>NoteManager: Load all notes for suggestions
SearchUI->>User: Show QuickPick with filters
User->>SearchUI: Type query + select filters (tags, author, date)
SearchUI->>SearchUI: Debounce input (300ms)
SearchUI->>SearchManager: Execute search with query, filters, tags
SearchManager->>SearchManager: Cache lookup
alt Cache Hit
SearchManager-->>SearchUI: Return cached results
else Cache Miss
SearchManager->>SearchManager: Query inverted index<br/>Apply tag filter (OR/AND)<br/>Apply other filters<br/>Rank by TF + recency
SearchManager->>SearchManager: Cache results
SearchManager-->>SearchUI: Return ranked results
end
SearchUI->>SearchUI: Render results with separators<br/>& active filters indicator
SearchUI-->>User: Display results
User->>SearchUI: Select result
SearchUI->>NoteManager: Open note in editor
NoteManager-->>User: Navigate to note location
sequenceDiagram
participant User
participant Sidebar as Sidebar (NotesSidebarProvider)
participant NoteManager
participant TagManager
participant StorageManager
StorageManager->>NoteManager: Load all notes
NoteManager->>Sidebar: Initialize TreeDataProvider
Sidebar->>Sidebar: Build tree (root → files → notes)
Sidebar-->>User: Render sidebar with all notes
User->>Sidebar: Click filter by tags (command)
Sidebar->>TagManager: Suggest tags from all notes
TagManager-->>Sidebar: Return tag list with counts
Sidebar-->>User: Show tag picker QuickPick
User->>Sidebar: Select tags + OR/AND mode
Sidebar->>Sidebar: setTagFilters(tags, mode)
Sidebar->>Sidebar: Debounce refresh (300ms)
Sidebar->>Sidebar: Filter files: retain only files<br/>with notes matching tag filter
Sidebar->>Sidebar: Emit onDidChangeTreeData
Sidebar-->>User: Refresh tree view (filtered)
User->>Sidebar: Click clear filters
Sidebar->>Sidebar: clearTagFilters()
Sidebar->>Sidebar: Refresh (300ms debounce)
Sidebar-->>User: Restore full tree view
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧬 Code graph analysis (2)src/notesSidebarProvider.ts (2)
src/searchManager.ts (1)
🔇 Additional comments (17)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/commentController.ts (3)
178-182: Remove unsupported property and avoid trusting untrusted markdownMarkdownString has no supportHtml; setting isTrusted=true on user content enables command URIs.
Apply:
- const markdownBody = new vscode.MarkdownString(note.content); - markdownBody.isTrusted = true; - markdownBody.supportHtml = true; - markdownBody.supportThemeIcons = true; + const markdownBody = new vscode.MarkdownString(note.content); + markdownBody.isTrusted = false; // do not trust user-provided markdown + markdownBody.supportThemeIcons = true;
803-823: Fix retrieval of currently editing commentcommentThreads are keyed by threadKey/tempId, not noteId. Current lookup will always fail.
Apply:
- getCurrentlyEditingComment(): vscode.Comment | null { - if (!this.currentlyEditingNoteId) { - return null; - } - const thread = this.commentThreads.get(this.currentlyEditingNoteId); - if (!thread || thread.comments.length === 0) { - return null; - } - const comment = thread.comments[0]; - if (comment.mode !== vscode.CommentMode.Editing) { - return null; - } - return comment; - } + getCurrentlyEditingComment(): vscode.Comment | null { + if (!this.currentlyEditingNoteId) return null; + // Find the thread that contains this noteId + const entry = Array.from(this.commentThreads.entries()).find(([threadKey]) => { + const state = this.threadStates.get(threadKey); + return state?.noteIds.includes(this.currentlyEditingNoteId!); + }); + if (!entry) return null; + const [, thread] = entry; + const comment = thread.comments[0]; + return comment && comment.mode === vscode.CommentMode.Editing ? comment : null; + }
632-639: Return the noteId, not the map key, from thread lookupThis currently returns the threadKey. Use threadStates to map back to the current note id.
Apply:
- getNoteIdFromThread(thread: vscode.CommentThread): string | undefined { - for (const [noteId, commentThread] of this.commentThreads.entries()) { - if (commentThread === thread) { - return noteId; - } - } - return undefined; - } + getNoteIdFromThread(thread: vscode.CommentThread): string | undefined { + for (const [threadKey, commentThread] of this.commentThreads.entries()) { + if (commentThread === thread) { + const state = this.threadStates.get(threadKey); + return state ? state.noteIds[state.currentIndex] : undefined; + } + } + return undefined; + }src/noteManager.ts (2)
271-310: Moved notes don’t update the search index or notify listenersWhen positions change, the sidebar/search may show stale ranges. Update the index, clear workspace caches, and emit a change event per updated note.
if (!isValid) { // Try to find the content at a new location const result = await this.hashTracker.findContentByHash( document, note.contentHash, note.lineRange ); if (result.found && result.newLineRange) { // Update note position note.lineRange = result.newLineRange; note.updatedAt = new Date().toISOString(); // Save updated note await this.storage.saveNote(note); + // Keep indexes and views in sync + if (this.searchManager) { + await this.searchManager.updateIndex(note); + } + this.clearWorkspaceCache(); + this.emit('noteChanged', { type: 'positionUpdated', note }); updatedNotes.push(note); } }
372-375: clearAllCache should also clear workspace cachesOnly noteCache is cleared; workspace caches remain stale.
clearAllCache(): void { this.noteCache.clear(); + this.clearWorkspaceCache(); }docs/architecture/ARCHITECTURE.md (1)
837-861: Add.jsextensions to local imports per ESM requirementsSix imports are missing required
.jsextensions:
src/searchManager.ts:2:import { Note } from './types'→'./types.js'src/searchTypes.ts:1:import { Note } from './types'→'./types.js'src/searchUI.ts:2-5:
import { SearchManager } from './searchManager'→'./searchManager.js'import { NoteManager } from './noteManager'→'./noteManager.js'import { SearchQuery, SearchResult } from './searchTypes'→'./searchTypes.js'import { Note } from './types'→'./types.js'The documented rule "All local imports MUST include
.jsextension" is correct and enforced throughout the codebase (91% compliance). These violations require fixing to maintain consistency.
🧹 Nitpick comments (20)
src/codeLensProvider.ts (1)
175-204: Consider consistency in multi-note tag display logic.The multi-note path limits tags to 2 with a
+Nindicator, which is good UX. However, the single-note path shows all tags without limit. For consistency and to prevent excessively long CodeLens titles, consider applying a similar tag display limit to single-note cases.src/test/suite/searchManager.test.ts (2)
416-429: Clarify date-range boundary inclusivity with explicit testsFilter logic appears inclusive on start/end. Add boundary tests where note date equals start or end to lock behavior.
565-575: Hard-coding MAX_HISTORY_SIZE in tests couples to implementationIf constant changes, tests break. Prefer importing the value (if exported) or deriving via pushing until length stops increasing.
src/commentController.ts (1)
223-229: Avoid opening documents just to update UI; drop unused parameterupdateThreadDisplay(document) doesn't use document. Opening files in navigateNext/Previous is unnecessary IO.
Refactor:
- Remove document parameter from updateThreadDisplay.
- Call updateThreadDisplay(threadKey) directly without openTextDocument.
Also applies to: 240-248, 124-172
src/searchTypes.ts (1)
16-21: Make dateRange.field optional (defaults to 'created')SearchManager treats 'created' as default; typing should reflect that to reduce friction.
Apply:
- dateRange?: { - start?: Date; - end?: Date; - field: 'created' | 'updated'; - }; + dateRange?: { + start?: Date; + end?: Date; + field?: 'created' | 'updated'; + };src/noteTreeItem.ts (1)
46-58: Prefer workspace-aware relative path helpervscode.workspace.asRelativePath handles multi-root/workspaces and separators better than path.relative.
Apply:
- const relativePath = path.relative(workspaceRoot, filePath); + const relativePath = vscode.workspace.asRelativePath(filePath, false) || path.relative(workspaceRoot, filePath);src/test/suite/tagManager.test.ts (1)
384-399: Avoid asserting order for equal-count tagsWhen counts tie (BUG vs FIXME = 1), relying on sort stability can be flaky. Assert membership and counts, not relative order.
Example:
assert.strictEqual(stats.topTags[0].tag, 'TODO'); const rest = new Map(stats.topTags.slice(1).map(x => [x.tag, x.count])); assert.strictEqual(rest.get('BUG'), 1); assert.strictEqual(rest.get('FIXME'), 1);src/tagInputUI.ts (2)
78-85: Preselect should normalize tagsexistingTags comparison is case‑sensitive and may miss predefined categories entered in different casing. Normalize before comparing.
- quickPick.selectedItems = items.filter((item) => { - const tagName = item.label.replace('$(tag) ', '').trim(); - return existingTags.includes(tagName); - }); + const normalizedExisting = existingTags.map(TagManager.normalizeTag); + quickPick.selectedItems = items.filter((item) => { + const tagName = item.label.replace('$(tag) ', '').trim(); + return normalizedExisting.includes(TagManager.normalizeTag(tagName)); + });
159-164: Remove unused variableallSuggestions is computed but never used.
- // Combine suggestions - const allSuggestions = [ - ...predefinedCategories, - ...suggestedTags.filter((tag) => !TagManager.isPredefinedCategory(tag)), - ]; + // (Suggestions shown via input placeholder; no pre-populated list needed here)src/searchUI.ts (1)
156-160: Avoid saving a history entry per keystrokeSaving on every input change quickly floods history. Persist on accept or after debounce when query stabilizes.
- // Save search to history - if (searchText.trim().length > 0) { - await this.searchManager.saveSearch(query, results.length); - } + // Consider saving only on accept or when executing an explicit search action.Follow‑up: move saveSearch into handleItemSelection when a result is opened, or add a “Run search” action. Based on learnings.
docs/architecture/ARCHITECTURE.md (1)
875-879: Tags listed as “Planned” but implementedTags UI/manager are part of this PR. Update “Planned Features” to reflect current status to avoid confusion.
Also applies to: 880-888
src/extension.ts (1)
120-149: Index build UX: skip progress toast for tiny workspacesOptional: avoid showing a notification when notes < N to reduce noise; log to console instead.
src/notesSidebarProvider.ts (2)
139-149: Author sort uses first note only; derive a stable primary authorFiles with mixed authors sort by a[0].author, which is unstable and arbitrary. Compute the most frequent author per file and sort by that; tie‑break on path.
- case 'author': - // Sort by author name (alphabetically), then by file path - fileNodes.sort((a, b) => { - const aAuthor = a.notes[0]?.author || ''; - const bAuthor = b.notes[0]?.author || ''; - if (aAuthor === bAuthor) { - return a.filePath.localeCompare(b.filePath); - } - return aAuthor.localeCompare(bAuthor); - }); + case 'author': { + const primaryAuthor = (notes: Note[]) => { + const counts = new Map<string, number>(); + for (const n of notes) counts.set(n.author, (counts.get(n.author) || 0) + 1); + return Array.from(counts.entries()).sort((x, y) => y[1] - x[1] || x[0].localeCompare(y[0]))[0]?.[0] ?? ''; + }; + fileNodes.sort((a, b) => { + const aAuthor = primaryAuthor(a.notes); + const bAuthor = primaryAuthor(b.notes); + return aAuthor === bAuthor ? a.filePath.localeCompare(b.filePath) : aAuthor.localeCompare(bAuthor); + }); + } break;
187-190: Unused configuration: sidebar.autoExpandgetAutoExpand() is defined but unused. Either remove it or honor it (e.g., expand file nodes on first load via TreeView.reveal).
src/tagManager.ts (1)
96-107: Optional: accept hashtag prefixes and trim internal whitespaceUsers often type “#todo” or “bug fix”. Consider normalizing “#” prefix and collapsing internal spaces.
static normalizeTag(tag: string): string { - const trimmed = tag.trim(); + const trimmed = tag.trim().replace(/^#/, '').replace(/\s+/g, ' ');src/searchManager.ts (4)
806-814: User-supplied glob -> regex can be abused (ReDoS); bound complexityConverting globs to unbounded '.*' can lead to catastrophic backtracking on very long inputs. Add limits and/or validate patterns; prefer non-backtracking where possible.
- private globToRegex(pattern: string): RegExp { + private globToRegex(pattern: string): RegExp { // Escape special regex characters except * and ? - let regex = pattern + const MAX_LEN = 200; + const safePattern = pattern.substring(0, MAX_LEN); + let regex = safePattern .replace(/[.+^${}()|[\]\\]/g, '\\$&') .replace(/\*/g, '.*') .replace(/\?/g, '.'); - - return new RegExp(`^${regex}$`, 'i'); + // Use case-insensitive, non-unicode flags only; caller controls case elsewhere if needed + return new RegExp(`^${regex}$`, 'i'); }Optionally validate with a safety check (e.g., safe-regex) before construction. Do you want me to wire that in?
870-887: Index size estimate omits several indexes; document or extendOnly content and author indexes are counted. Either document as approximate or include file/tag/date sizes for more accurate telemetry.
235-237: Regex ReDoS risk (variable patterns over arbitrary content)Two hotspots: constructing RegExp from user-supplied input and applying it across all notes can hang the extension on pathological patterns. Add safeguards:
- Size/time bounds (e.g., cap content length per regex scan; bail out after N ms).
- Optional safety check via a lightweight validator before execution.
Also applies to: 686-696, 806-814
1-3: Import inconsistency confirmed, but risk overstated for this configThe codebase does have inconsistent import patterns:
searchUI.ts,searchTypes.ts, andsearchManager.ts(under review) lack.jssuffixes, while most other files include them. However, the tsconfig uses"moduleResolution": "bundler", which is more lenient than"nodenext"and doesn't strictly require.jsextensions at runtime. Adding.jsimproves consistency and aligns with ESM best practices, but the review comment's claim about "breaking NodeNext/ESM" is inaccurate for the current configuration.-import { Note } from './types'; +import { Note } from './types.js';To be fully consistent with the majority of the codebase, also fix:
src/searchUI.ts(lines 2–5) andsrc/searchTypes.ts(line 1).package.json (1)
418-429: sidebar.autoExpand setting is unused in codeProvider never reads this setting to expand nodes. Either implement behavior or drop the setting for now.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
images/task.pngis excluded by!**/*.png
📒 Files selected for processing (32)
.gitignore(1 hunks).vscodeignore(2 hunks)CLAUDE.md(1 hunks)README.md(7 hunks)docs/architecture/ARCHITECTURE.md(9 hunks)docs/changelogs/v0.2.0.md(1 hunks)docs/changelogs/v0.3.0.md(1 hunks)docs/changelogs/v0.4.0.md(1 hunks)docs/search-and-filter-notes/USER_STORY.md(1 hunks)docs/sidebar-view-for-browsing-all-notes/USER_STORY.md(1 hunks)docs/tags-and-categories/USER_STORY.md(1 hunks)package.json(7 hunks)src/codeLensProvider.ts(1 hunks)src/commentController.ts(3 hunks)src/extension.ts(9 hunks)src/noteManager.ts(8 hunks)src/noteTreeItem.ts(1 hunks)src/notesSidebarProvider.ts(1 hunks)src/searchManager.ts(1 hunks)src/searchTypes.ts(1 hunks)src/searchUI.ts(1 hunks)src/storageManager.ts(2 hunks)src/tagInputUI.ts(1 hunks)src/tagManager.ts(1 hunks)src/tagTypes.ts(1 hunks)src/test/runUnitTests.ts(1 hunks)src/test/suite/noteTreeItem.test.ts(1 hunks)src/test/suite/notesSidebarProvider.test.ts(1 hunks)src/test/suite/searchManager.test.ts(1 hunks)src/test/suite/storageManager.test.ts(1 hunks)src/test/suite/tagManager.test.ts(1 hunks)src/types.ts(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (21)
docs/search-and-filter-notes/USER_STORY.md (1)
src/test/suite/noteManager.test.ts (10)
doc(151-174)doc(409-423)doc(284-296)doc1(425-452)doc(93-105)doc(265-282)doc(45-66)doc(192-211)doc(246-261)doc(391-407)
src/types.ts (1)
src/test/suite/noteManager.test.ts (2)
test(150-262)doc(151-174)
src/extension.ts (6)
src/noteManager.ts (1)
NoteManager(19-546)src/commentController.ts (1)
CommentController(15-925)src/notesSidebarProvider.ts (1)
NotesSidebarProvider(15-261)src/searchManager.ts (1)
SearchManager(17-917)src/searchUI.ts (1)
SearchUI(35-697)src/tagInputUI.ts (1)
TagInputUI(11-232)
src/searchUI.ts (3)
src/types.ts (1)
Note(37-60)src/searchTypes.ts (2)
SearchResult(45-60)SearchQuery(6-40)src/searchManager.ts (1)
SearchManager(17-917)
src/noteTreeItem.ts (1)
src/types.ts (1)
Note(37-60)
src/test/suite/storageManager.test.ts (1)
src/types.ts (1)
Note(37-60)
docs/sidebar-view-for-browsing-all-notes/USER_STORY.md (1)
src/test/suite/noteManager.test.ts (8)
doc(151-174)doc(409-423)doc(93-105)doc(265-282)doc(45-66)doc(192-211)doc(246-261)doc(391-407)
src/searchTypes.ts (1)
src/types.ts (1)
Note(37-60)
src/test/suite/tagManager.test.ts (2)
src/types.ts (1)
Note(37-60)src/tagManager.ts (1)
TagManager(20-278)
src/tagInputUI.ts (3)
src/types.ts (1)
Note(37-60)src/tagManager.ts (1)
TagManager(20-278)src/tagTypes.ts (1)
CATEGORY_STYLES(33-69)
src/test/suite/searchManager.test.ts (3)
src/searchManager.ts (1)
SearchManager(17-917)src/types.ts (1)
Note(37-60)src/searchTypes.ts (1)
SearchQuery(6-40)
src/test/suite/noteTreeItem.test.ts (2)
src/noteTreeItem.ts (3)
NoteTreeItem(66-158)FileTreeItem(37-59)RootTreeItem(21-31)src/types.ts (1)
Note(37-60)
README.md (1)
web/src/pages/DocsPage.tsx (1)
DocsPage(32-678)
src/noteManager.ts (3)
src/storageManager.ts (1)
StorageManager(15-384)src/searchManager.ts (1)
SearchManager(17-917)src/types.ts (1)
Note(37-60)
src/commentController.ts (1)
src/tagInputUI.ts (1)
TagInputUI(11-232)
docs/changelogs/v0.4.0.md (1)
src/test/suite/noteManager.test.ts (9)
doc(151-174)doc(284-296)doc(323-340)doc(409-423)noteManager(238-241)doc(176-190)doc(93-105)doc(265-282)doc(45-66)
src/test/suite/notesSidebarProvider.test.ts (3)
src/notesSidebarProvider.ts (1)
NotesSidebarProvider(15-261)src/types.ts (1)
Note(37-60)src/noteTreeItem.ts (3)
RootTreeItem(21-31)FileTreeItem(37-59)NoteTreeItem(66-158)
src/notesSidebarProvider.ts (3)
src/noteManager.ts (1)
NoteManager(19-546)src/noteTreeItem.ts (3)
RootTreeItem(21-31)FileTreeItem(37-59)NoteTreeItem(66-158)src/types.ts (1)
Note(37-60)
src/searchManager.ts (2)
src/searchTypes.ts (7)
InvertedIndexEntry(136-148)SearchCacheEntry(153-165)SearchHistoryEntry(96-111)SearchStats(116-131)SearchQuery(6-40)SearchResult(45-60)SearchMatch(65-77)src/types.ts (1)
Note(37-60)
docs/architecture/ARCHITECTURE.md (1)
src/test/suite/noteManager.test.ts (8)
doc(409-423)doc1(425-452)doc(93-105)doc(265-282)doc(45-66)doc(192-211)doc(246-261)doc(391-407)
src/tagManager.ts (2)
src/tagTypes.ts (6)
TagStyle(21-28)CATEGORY_STYLES(33-69)DEFAULT_TAG_COLOR(74-74)TagValidationResult(91-98)TagFilterParams(79-86)TagStatistics(103-110)src/types.ts (1)
Note(37-60)
🪛 ast-grep (0.39.6)
src/searchManager.ts
[warning] 687-687: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(query.regex.source, query.regex.flags + 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 812-812: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regex}$, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 LanguageTool
docs/search-and-filter-notes/USER_STORY.md
[grammar] ~142-~142: Ensure spelling is correct
Context: ...Performance - [ ] Search completes in < 500ms with 100 notes - [ ] Search completes i...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
docs/changelogs/v0.2.0.md
[grammar] ~50-~50: Ensure spelling is correct
Context: ... refresh behavior - Debouncing logic (300ms delay) - All tests compile successfully...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
docs/sidebar-view-for-browsing-all-notes/USER_STORY.md
[uncategorized] ~117-~117: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...Test stripMarkdown() static method (all markdown formats) - [x] Test truncateText() stat...
(MARKDOWN_NNP)
docs/tags-and-categories/USER_STORY.md
[uncategorized] ~14-~14: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...face to include tags field - [x] Update markdown parser to support tags in frontmatter -...
(MARKDOWN_NNP)
[uncategorized] ~28-~28: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...ed and custom) - [x] Tags are stored in markdown frontmatter and persisted correctly - [...
(MARKDOWN_NNP)
README.md
[uncategorized] ~137-~137: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...d+Alt+Non Mac) 3. Type your note with markdown formatting 4. Click Save or pressCtrl...
(MARKDOWN_NNP)
[style] ~450-~450: Consider removing “of” to be more concise
Context: ... - AND Logic (advanced): Notes with ALL of the selected tags - Configurable in filter ...
(ALL_OF_THE)
docs/changelogs/v0.4.0.md
[uncategorized] ~42-~42: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...geManager` to parse and persist tags in markdown format - Add sidebar tag filtering with...
(MARKDOWN_NNP)
[uncategorized] ~119-~119: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...ined and custom) - ✅ Tags are stored in markdown and persisted correctly - ✅ Predefined ...
(MARKDOWN_NNP)
docs/architecture/ARCHITECTURE.md
[grammar] ~300-~300: Ensure spelling is correct
Context: ...hods**: - getChildren(element?) - Get tree children (lazy loading) - `getTreeItem(...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[uncategorized] ~360-~360: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...ods**: - stripMarkdown(text) - Remove markdown formatting from preview - `truncateText...
(MARKDOWN_NNP)
🪛 markdownlint-cli2 (0.18.1)
docs/search-and-filter-notes/USER_STORY.md
161-161: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
182-182: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
194-194: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
212-212: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
424-424: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
427-427: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
430-430: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
433-433: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
436-436: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
docs/sidebar-view-for-browsing-all-notes/USER_STORY.md
188-188: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
241-241: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
251-251: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
334-334: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
337-337: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
340-340: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
343-343: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
README.md
241-241: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
251-251: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
334-334: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
337-337: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
340-340: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
343-343: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
docs/architecture/ARCHITECTURE.md
334-334: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
337-337: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
340-340: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
343-343: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🔇 Additional comments (22)
.gitignore (1)
13-13: Good addition for artifact exclusion.The
.code-notesentry appropriately excludes generated artifacts from version control, aligning with the sidebar, search indexing, and tag persistence features introduced in this PR..vscodeignore (2)
32-32: Good addition to exclude prettier from the package.Excluding prettier reduces the extension package size since it's a development dependency not needed at runtime.
63-64: Appropriate exclusion of local notes directory.Excluding the
.code-notesdirectory prevents user-specific local data from being packaged in the extension, which is the correct approach for local storage directories.CLAUDE.md (1)
8-8: LGTM! Good process improvement.Adding a guideline to update user stories after completion helps maintain documentation hygiene and aligns well with the comprehensive user story tracking demonstrated in this PR.
docs/changelogs/v0.2.0.md (1)
1-121: Excellent changelog documentation!This changelog is comprehensive and well-structured with:
- Clear feature descriptions with examples
- Detailed testing coverage (78 tests documented)
- Honest known limitations
- Migration guidance confirming no breaking changes
- Benefits for both users and teams
The level of detail here will be very helpful for users upgrading to v0.2.0.
docs/sidebar-view-for-browsing-all-notes/USER_STORY.md (1)
1-364: Outstanding user story documentation!This is an exemplary user story with:
- ✅ Complete phase tracking (100% done with 71 tasks)
- Clear Epic alignment and acceptance criteria
- Comprehensive UI/UX design examples
- Detailed technical implementation and data flow diagrams
- Timeline estimates and risk mitigation strategies
- Success metrics defined
This level of planning and documentation makes the feature implementation much easier to review and understand. Great work following the user story template!
docs/search-and-filter-notes/USER_STORY.md (1)
1-482: Excellent search feature planning!This user story demonstrates strong architectural thinking with:
- Clear phase tracking (83% done, honest about deferred items)
- Well-designed search architecture (inverted index, TF-IDF scoring)
- Realistic performance targets (< 100ms for 1000+ notes)
- Comprehensive filter combinations documented
- Alternative approaches evaluated with pros/cons
The "Alternative Approaches Considered" section is particularly valuable for understanding the design decisions. This shows thoughtful analysis of tradeoffs.
README.md (4)
39-46: Great feature summary in The Solution section!The updated solution highlights clearly communicate the new capabilities:
- Workspace sidebar for browsing
- Tags & Categories for organization
- Advanced search with filters
These additions fit well with the existing benefits and maintain the clear, user-focused messaging.
105-520: Comprehensive Tags & Categories documentation!This section is exceptionally well-documented with:
- Clear predefined categories table with colors and icons
- Step-by-step tag creation workflow
- Visual examples of tag display (CodeLens, sidebar, search)
- Tag validation rules clearly stated
- Best practices and keyboard workflow guidance
- Real-world examples by use case
Users will find this very helpful for understanding how to organize their notes effectively.
236-350: Excellent Sidebar and Search documentation!Both features are thoroughly documented with:
Sidebar View:
- Clear structure explanation (Root → File → Note nodes)
- Quick actions documented (+ button, Collapse All, Refresh)
- Context menu options clearly listed
- Sorting and collapsing behavior explained
Search & Filter:
- Search syntax examples (text, regex, patterns)
- All filter types documented with examples
- Search history and performance details included
- Integration points (keyboard shortcut, sidebar button) noted
This makes the features very approachable for new users.
549-620: Well-organized configuration section!The configuration options are clearly documented with:
- JSON examples for each setting
- Descriptions and default values
- Logical grouping (Sidebar, Search categories)
- Sensible defaults (50 char preview, 200ms debounce, 100 max results)
This helps users customize the extension to their workflow preferences.
src/test/runUnitTests.ts (1)
38-38: Good clarification comment!The comment explaining why searchManager.test.js runs as an integration test (due to VSCode API dependency) is helpful for understanding the test organization. This makes the filtration logic clearer.
src/storageManager.ts (2)
213-215: Correct tag serialization implementation!The tag serialization logic:
- Properly checks for tags existence and non-empty array
- Uses readable comma-space separator (", ")
- Integrates well with existing metadata section
296-299: Correct tag deserialization implementation!The tag parsing logic:
- Correctly parses the "Tags:" line
- Handles empty strings with ternary (returns empty array)
- Split on comma with trim handles the comma-space format from serialization
- Maintains consistency with serialization format
The serialization/deserialization pair is well-implemented and handles edge cases properly.
src/types.ts (2)
58-59: Well-implemented tag type support!Adding the optional
tags?: string[]field to the Note interface:
- Maintains backward compatibility (optional field)
- Includes clear documentation comment
- Uses appropriate type (array of strings)
80-82: Consistent tag field additions across interfaces!The tags field is correctly added to:
- NoteMetadata (line 80-81)
- CreateNoteParams (line 120-121)
- UpdateNoteParams (line 134-135)
All use the same optional type (
tags?: string[]) and include documentation. This consistency ensures tags flow properly through note creation, updates, and storage.Also applies to: 120-122, 134-136
src/test/suite/storageManager.test.ts (1)
323-489: LGTM! Comprehensive tag serialization test coverage.The test suite thoroughly validates tag persistence across storage operations with excellent edge case coverage:
- Multiple tags and order preservation
- Single tag and empty array handling
- Special characters and spaces in tags
- Markdown formatting validation
- High-volume (20 tags) and maximum-length (50 chars) scenarios
src/test/suite/noteTreeItem.test.ts (1)
1-419: LGTM! Excellent test coverage for tree item components.The test suite provides comprehensive validation of:
- Markdown stripping across all common formatting constructs (bold, italic, code, links, lists, etc.)
- Text truncation with proper ellipsis handling
- Tree item construction and UI property configuration for Root, File, and Note items
- Tooltip content, icons, commands, and collapsible states
All edge cases are well-covered and tests follow VS Code testing best practices.
src/tagTypes.ts (1)
1-110: LGTM! Well-structured tag type definitions.The type system cleanly separates concerns:
- Predefined categories with semantic meaning (TODO, FIXME, etc.)
- Visual styling configuration (colors, icons, descriptions)
- Flexible filtering parameters (include/exclude with AND/OR logic)
- Validation and statistics interfaces for tag operations
Color choices and icon mappings align well with their semantic purposes.
src/test/suite/notesSidebarProvider.test.ts (1)
1-402: LGTM! Comprehensive sidebar provider test coverage.The test suite thoroughly validates:
- Tree structure and hierarchy (Root → File → Note items)
- Lazy loading and sorting behavior (alphabetical by file, by line number)
- Debounced refresh mechanism (300ms delay, timer reset on rapid calls)
- Real-time event handling (noteChanged, noteFileChanged events)
- Edge cases (empty state, single/multiple notes, no children for leaf nodes)
The MockNoteManager and helper functions provide clean, maintainable test infrastructure.
src/searchManager.ts (1)
245-281: SearchFullText AND semantics are correct; consider early exit on missing termMinor: you already short-circuit on empty set. LGTM otherwise.
src/tagManager.ts (1)
31-33: No issues found—NoteCategory is a string enumVerification confirms NoteCategory is a string enum with all string literal values (TODO = 'TODO', FIXME = 'FIXME', etc.), not numeric. For string enums, Object.values() returns only the string values and includes() works reliably. The code at lines 31–33 is correct as written.
| { | ||
| "command": "codeContextNotes.searchNotes", | ||
| "key": "ctrl+shift+f", | ||
| "mac": "cmd+shift+f", | ||
| "when": "!searchViewletFocus && !replaceInputBoxFocus" | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keybinding conflicts with VS Code “Find in Files” (Ctrl/Cmd+Shift+F)
This will shadow a core binding globally. Recommend a non-conflicting chord or scope the when-clause to your view/context only.
- {
- "command": "codeContextNotes.searchNotes",
- "key": "ctrl+shift+f",
- "mac": "cmd+shift+f",
- "when": "!searchViewletFocus && !replaceInputBoxFocus"
- },
+ {
+ "command": "codeContextNotes.searchNotes",
+ "key": "ctrl+alt+shift+f",
+ "mac": "cmd+alt+shift+f",
+ "when": "editorTextFocus && !searchViewletFocus && !replaceInputBoxFocus"
+ },Alternative: remove the keybinding and expose via view/title and command palette only.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| "command": "codeContextNotes.searchNotes", | |
| "key": "ctrl+shift+f", | |
| "mac": "cmd+shift+f", | |
| "when": "!searchViewletFocus && !replaceInputBoxFocus" | |
| }, | |
| { | |
| "command": "codeContextNotes.searchNotes", | |
| "key": "ctrl+alt+shift+f", | |
| "mac": "cmd+alt+shift+f", | |
| "when": "editorTextFocus && !searchViewletFocus && !replaceInputBoxFocus" | |
| }, |
🤖 Prompt for AI Agents
In package.json around lines 232 to 237, the keybinding "ctrl+shift+f" /
"cmd+shift+f" conflicts with VS Code's core "Find in Files"; change it to a
non-conflicting chord (e.g., "ctrl+k ctrl+f") or scope it to your extension's
view by tightening the "when" clause to a specific view/context (for example
only active when your custom view has focus), or remove the global keybinding
and expose the command via the view/title and the command palette only. Ensure
the chosen fix avoids shadowing built-in bindings and update package.json
accordingly.
| // Format tags for display | ||
| let tagsDisplay = ''; | ||
| if (note.tags && note.tags.length > 0) { | ||
| tagsDisplay = note.tags.map(tag => `[${tag}]`).join(' ') + ' '; | ||
| } | ||
|
|
||
| // Strip markdown formatting and get first line | ||
| const plainText = this.stripMarkdown(note.content); | ||
| const firstLine = plainText.split('\n')[0]; | ||
| const preview = firstLine.length > 50 | ||
| ? firstLine.substring(0, 47) + '...' | ||
|
|
||
| // Calculate available space for preview (account for tags) | ||
| const maxPreviewLength = 50 - tagsDisplay.length; | ||
| const preview = firstLine.length > maxPreviewLength | ||
| ? firstLine.substring(0, maxPreviewLength - 3) + '...' | ||
| : firstLine; | ||
|
|
||
| // Format: "📝 Note: preview text (by author)" | ||
| return `📝 Note: ${preview} (${note.author})`; | ||
| // Format: "📝 [TODO] [bug] Note: preview text (by author)" | ||
| return `📝 ${tagsDisplay}Note: ${preview} (${note.author})`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify preview length calculation with many/long tags.
The preview length is calculated as 50 - tagsDisplay.length, which could potentially result in a very small or negative maxPreviewLength if a note has many or lengthy tags. Consider adding a minimum preview length guard.
🏁 Script executed:
#!/bin/bash
# Check if there's any validation for minimum preview length in the codebase
rg -n "maxPreviewLength|MAX_PREVIEW" --type=ts -C2Length of output: 971
---
🏁 Script executed:
```shell
#!/bin/bash
# Get full context around both maxPreviewLength occurrences
sed -n '155,170p' src/codeLensProvider.ts
echo "---"
sed -n '190,210p' src/codeLensProvider.ts
Length of output: 137
🏁 Script executed:
#!/bin/bash
# Get full context around both maxPreviewLength occurrences
echo "=== First occurrence (around line 161) ==="
sed -n '155,170p' src/codeLensProvider.ts
echo ""
echo "=== Second occurrence (around line 198) ==="
sed -n '190,210p' src/codeLensProvider.tsLength of output: 1460
Add minimum preview length guards at both calculation sites.
The concern is valid. Both locations (lines 161 and 198) calculate maxPreviewLength without protecting against negative values when tags are long or numerous. In such cases, substring(0, maxPreviewLength - 3) with a negative index produces an empty preview. Add a minimum guard:
const maxPreviewLength = Math.max(10, 50 - tagsDisplay.length); // Line 161
const maxPreviewLength = Math.max(10, 35 - tagsDisplay.length); // Line 198This ensures the preview always displays meaningful content even with many or lengthy tags.
| // Prompt for tags (async, so import at top of file is needed) | ||
| const { TagInputUI } = await import('./tagInputUI.js'); | ||
| const allNotes = await this.noteManager.getAllNotes(); | ||
| const tags = await TagInputUI.showTagInput(undefined, allNotes); | ||
|
|
||
| // If user cancelled tag input, cancel note creation | ||
| if (tags === undefined) { | ||
| thread.dispose(); | ||
| if (tempId) { | ||
| this.commentThreads.delete(tempId); | ||
| } | ||
| this.currentlyCreatingThreadId = null; | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden dynamic import and tag prompt failure handling
Wrap the dynamic import/UI in try/catch and fall back to empty tags instead of cancelling on errors. Preserve cancel behavior when user explicitly dismisses.
Apply:
- // Prompt for tags (async, so import at top of file is needed)
- const { TagInputUI } = await import('./tagInputUI.js');
- const allNotes = await this.noteManager.getAllNotes();
- const tags = await TagInputUI.showTagInput(undefined, allNotes);
+ // Prompt for tags with robust fallback
+ let tags: string[] | undefined;
+ try {
+ const { TagInputUI } = await import('./tagInputUI.js');
+ const allNotes = await this.noteManager.getAllNotes();
+ tags = await TagInputUI.showTagInput(undefined, allNotes);
+ } catch (e) {
+ // Non-interactive failure: proceed with empty tags
+ tags = [];
+ }🤖 Prompt for AI Agents
In src/commentController.ts around lines 528 to 542, the dynamic import and tag
prompt can throw and currently any error cancels note creation; wrap the import
and TagInputUI.showTagInput call in a try/catch so exceptions do not treat as a
user cancel, and set tags = [] on error; keep the existing check that if tags
=== undefined (explicit user dismissal) then dispose the thread, delete tempId,
clear currentlyCreatingThreadId and return. Ensure the try block contains both
the await import(...) and await TagInputUI.showTagInput(...) and the catch
assigns an empty array to tags while letting undefined continue to trigger
cancellation.
| interface SearchQuickPickItem extends vscode.QuickPickItem { | ||
| note?: Note; | ||
| result?: SearchResult; | ||
| type: 'result' | 'filter' | 'action' | 'separator'; | ||
| action?: 'clearFilters' | 'showHistory' | 'advancedSearch'; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filter actions clear filters instead of opening dialogs
createActionItems assigns action: 'clearFilters' to all filter items; handleAction then clears filters. Wire explicit actions and switch on them.
interface SearchQuickPickItem extends vscode.QuickPickItem {
note?: Note;
result?: SearchResult;
type: 'result' | 'filter' | 'action' | 'separator';
- action?: 'clearFilters' | 'showHistory' | 'advancedSearch';
+ action?: 'clearFilters' | 'showHistory' | 'filterByAuthor' | 'filterByDate' | 'filterByFile' | 'advancedSearch';
}
@@
private createActionItems(): SearchQuickPickItem[] {
const items: SearchQuickPickItem[] = [];
@@
- items.push({
- label: ' $(person) Filter by Author',
- description: 'Select one or more authors',
- type: 'action',
- action: 'clearFilters', // Will be changed to specific actions
- alwaysShow: true
- });
+ items.push({
+ label: ' $(person) Filter by Author',
+ description: 'Select one or more authors',
+ type: 'action',
+ action: 'filterByAuthor',
+ alwaysShow: true
+ });
@@
- items.push({
- label: ' $(calendar) Filter by Date Range',
- description: 'Select date range',
- type: 'action',
- action: 'clearFilters',
- alwaysShow: true
- });
+ items.push({
+ label: ' $(calendar) Filter by Date Range',
+ description: 'Select date range',
+ type: 'action',
+ action: 'filterByDate',
+ alwaysShow: true
+ });
@@
- items.push({
- label: ' $(file) Filter by File Pattern',
- description: 'Enter file path pattern',
- type: 'action',
- action: 'clearFilters',
- alwaysShow: true
- });
+ items.push({
+ label: ' $(file) Filter by File Pattern',
+ description: 'Enter file path pattern',
+ type: 'action',
+ action: 'filterByFile',
+ alwaysShow: true
+ });
@@
- private async handleAction(item: SearchQuickPickItem): Promise<void> {
- if (item.action === 'clearFilters') {
- await this.clearFilters();
- } else if (item.action === 'showHistory') {
- // Populate search input from history item
- if (this.quickPick) {
- this.quickPick.value = item.label.trim();
- }
- } else if (item.label.includes('Filter by Author')) {
- await this.showAuthorFilter();
- } else if (item.label.includes('Filter by Date')) {
- await this.showDateFilter();
- } else if (item.label.includes('Filter by File')) {
- await this.showFileFilter();
- }
- }
+ private async handleAction(item: SearchQuickPickItem): Promise<void> {
+ switch (item.action) {
+ case 'clearFilters':
+ await this.clearFilters();
+ break;
+ case 'showHistory':
+ if (this.quickPick) this.quickPick.value = item.label.trim();
+ break;
+ case 'filterByAuthor':
+ await this.showAuthorFilter();
+ break;
+ case 'filterByDate':
+ await this.showDateFilter();
+ break;
+ case 'filterByFile':
+ await this.showFileFilter();
+ break;
+ }
+ }Also applies to: 398-439, 484-499
🤖 Prompt for AI Agents
In src/searchUI.ts around lines 10-15 (and also inspect 398-439, 484-499):
filter QuickPick items are incorrectly assigned action: 'clearFilters' so
handleAction always clears filters; update the code that builds
createActionItems so filter items get explicit, correct actions (e.g.,
'clearFilters' only for the clear button/item, 'showHistory' for history items,
'advancedSearch' for items that should open dialogs, and omit action for plain
filters), then change handleAction to switch on the action value (handle
'clearFilters' by clearing, 'showHistory' by opening history dialog,
'advancedSearch' by opening advanced-search dialog, and no-op or treat undefined
as selecting a filter) so each action triggers the intended behavior.
| // Handle custom tag input | ||
| quickPick.onDidChangeValue((value) => { | ||
| // If user types something not in the list, add it as a custom tag option | ||
| if (value && !items.some((item) => item.label.includes(value))) { | ||
| const customTag = value.trim(); | ||
|
|
||
| // Validate the custom tag | ||
| const validation = TagManager.validateTag(customTag); | ||
|
|
||
| if (validation.isValid && validation.normalizedTag) { | ||
| // Check if this custom tag is already in the list | ||
| const existingCustom = items.find( | ||
| (item) => | ||
| item.label === `$(tag) ${validation.normalizedTag}` && | ||
| item.description === 'Custom tag (type to add)' | ||
| ); | ||
|
|
||
| if (!existingCustom) { | ||
| // Add custom tag option at the top (after categories) | ||
| const customTagItem: vscode.QuickPickItem = { | ||
| label: `$(tag) ${validation.normalizedTag}`, | ||
| description: 'Custom tag (type to add)', | ||
| alwaysShow: true, | ||
| }; | ||
|
|
||
| // Find where to insert (after predefined categories) | ||
| const categoryEndIndex = items.findIndex( | ||
| (item) => item.label === 'Recently Used' | ||
| ); | ||
|
|
||
| if (categoryEndIndex !== -1) { | ||
| items.splice(categoryEndIndex, 0, customTagItem); | ||
| } else { | ||
| items.push(customTagItem); | ||
| } | ||
|
|
||
| quickPick.items = items; | ||
| } | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent duplicate custom tags and honor predefined categories on type
Typing a tag already present (suggested/custom) can add a duplicate “type to add” item; typing a predefined category in lowercase adds it as a custom tag. Normalize, de‑dupe by exact label, and select the existing category/item instead of adding a duplicate.
Apply:
- quickPick.onDidChangeValue((value) => {
- // If user types something not in the list, add it as a custom tag option
- if (value && !items.some((item) => item.label.includes(value))) {
- const customTag = value.trim();
-
- // Validate the custom tag
- const validation = TagManager.validateTag(customTag);
-
- if (validation.isValid && validation.normalizedTag) {
- // Check if this custom tag is already in the list
- const existingCustom = items.find(
- (item) =>
- item.label === `$(tag) ${validation.normalizedTag}` &&
- item.description === 'Custom tag (type to add)'
- );
-
- if (!existingCustom) {
- // Add custom tag option at the top (after categories)
- const customTagItem: vscode.QuickPickItem = {
- label: `$(tag) ${validation.normalizedTag}`,
- description: 'Custom tag (type to add)',
- alwaysShow: true,
- };
-
- // Find where to insert (after predefined categories)
- const categoryEndIndex = items.findIndex(
- (item) => item.label === 'Recently Used'
- );
-
- if (categoryEndIndex !== -1) {
- items.splice(categoryEndIndex, 0, customTagItem);
- } else {
- items.push(customTagItem);
- }
-
- quickPick.items = items;
- }
- }
- }
- });
+ quickPick.onDidChangeValue((value) => {
+ const raw = value.trim();
+ if (!raw) return;
+ const normalized = TagManager.normalizeTag(raw);
+
+ // If it's a predefined category, focus the category item instead of adding a custom tag
+ if (TagManager.isPredefinedCategory(normalized)) {
+ const cat = categoryItems.find(ci => ci.label === `$(tag) ${normalized}`);
+ if (cat) quickPick.activeItems = [cat];
+ return;
+ }
+
+ // Validate custom tag
+ const validation = TagManager.validateTag(normalized);
+ if (!validation.isValid || !validation.normalizedTag) return;
+
+ const exists = items.some(it => it.label.replace('$(tag) ', '') === normalized);
+ if (exists) {
+ const existing = items.find(it => it.label.replace('$(tag) ', '') === normalized);
+ if (existing) quickPick.activeItems = [existing];
+ return;
+ }
+
+ // Add custom tag option (after categories, before "Recently Used" if present)
+ const customTagItem: vscode.QuickPickItem = {
+ label: `$(tag) ${validation.normalizedTag}`,
+ description: 'Custom tag (type to add)',
+ alwaysShow: true,
+ };
+ const recentlyUsedIdx = items.findIndex(it => it.label === 'Recently Used');
+ const insertIdx = recentlyUsedIdx !== -1 ? recentlyUsedIdx : items.length;
+ items.splice(insertIdx, 0, customTagItem);
+ quickPick.items = items;
+ });Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/tagInputUI.ts around lines 86 to 126, typing a tag can create duplicate
"type to add" entries or convert a predefined category typed in lowercase into a
custom tag; normalize the typed value (trim + normalize via TagManager), compute
the exact label string `$(tag) ${normalized}` and check for duplicates by exact
label match against items (case-insensitive for category labels too) before
creating a new customTagItem, and if an existing item/category matches, do not
push/splice a new item but instead set quickPick.activeItems (or
quickPick.selectedItems) to that existing item so it becomes selected; ensure
insertion respects predefined category boundaries by checking labels with
case-insensitive comparisons and only insert when no exact match is found.
| static filterNotesByTags(notes: Note[], params: TagFilterParams): Note[] { | ||
| return notes.filter(note => { | ||
| const noteTags = note.tags || []; | ||
|
|
||
| // Exclude notes with excluded tags | ||
| if (params.excludeTags && params.excludeTags.length > 0) { | ||
| const hasExcludedTag = params.excludeTags.some(tag => | ||
| noteTags.includes(tag) | ||
| ); | ||
| if (hasExcludedTag) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Include notes with included tags | ||
| if (params.includeTags && params.includeTags.length > 0) { | ||
| if (params.requireAllTags) { | ||
| // Note must have ALL included tags | ||
| return params.includeTags.every(tag => noteTags.includes(tag)); | ||
| } else { | ||
| // Note must have at least ONE included tag | ||
| return params.includeTags.some(tag => noteTags.includes(tag)); | ||
| } | ||
| } | ||
|
|
||
| // If no include/exclude filters, return all notes | ||
| return true; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Normalize both filter inputs and note tags inside filterNotesByTags
Current includes/excludes do raw string compares. Normalize to ensure consistent behavior across casing and predefined categories.
- static filterNotesByTags(notes: Note[], params: TagFilterParams): Note[] {
- return notes.filter(note => {
- const noteTags = note.tags || [];
+ static filterNotesByTags(notes: Note[], params: TagFilterParams): Note[] {
+ const includeTags = (params.includeTags ?? []).map(t => this.normalizeTag(t));
+ const excludeTags = (params.excludeTags ?? []).map(t => this.normalizeTag(t));
+ return notes.filter(note => {
+ const noteTags = (note.tags || []).map(t => this.normalizeTag(t));
@@
- if (params.excludeTags && params.excludeTags.length > 0) {
- const hasExcludedTag = params.excludeTags.some(tag =>
- noteTags.includes(tag)
- );
+ if (excludeTags.length > 0) {
+ const hasExcludedTag = excludeTags.some(tag => noteTags.includes(tag));
if (hasExcludedTag) {
return false;
}
}
@@
- if (params.includeTags && params.includeTags.length > 0) {
- if (params.requireAllTags) {
+ if (includeTags.length > 0) {
+ if (params.requireAllTags) {
// Note must have ALL included tags
- return params.includeTags.every(tag => noteTags.includes(tag));
+ return includeTags.every(tag => noteTags.includes(tag));
} else {
// Note must have at least ONE included tag
- return params.includeTags.some(tag => noteTags.includes(tag));
+ return includeTags.some(tag => noteTags.includes(tag));
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/tagManager.ts around lines 138 to 166, the filterNotesByTags function
performs raw string comparisons causing mismatches due to case/whitespace and
category variants; normalize both the incoming
params.includeTags/params.excludeTags and each note.tags before comparisons
(e.g., map to lowercased, trimmed strings and deduplicate) and use those
normalized arrays for all includes/excludes checks (including requireAllTags
logic) so comparisons are consistent regardless of casing/whitespace or
duplicate entries.
| suite('Search Caching', () => { | ||
| test('should cache search results on first query', async () => { | ||
| const notes = [ | ||
| createMockNote('note1', 'JavaScript content', 'Alice', '/workspace/file1.ts') | ||
| ]; | ||
| await searchManager.buildIndex(notes); | ||
|
|
||
| const query: SearchQuery = { text: 'javascript' }; | ||
| const results1 = await searchManager.search(query, notes); | ||
| const results2 = await searchManager.search(query, notes); | ||
|
|
||
| assert.strictEqual(results1.length, results2.length); | ||
| assert.strictEqual(results1[0].note.id, results2[0].note.id); | ||
| }); | ||
|
|
||
| test('should return cached results for duplicate queries', async () => { | ||
| const notes = [ | ||
| createMockNote('note1', 'JavaScript content', 'Alice', '/workspace/file1.ts') | ||
| ]; | ||
| await searchManager.buildIndex(notes); | ||
|
|
||
| const query: SearchQuery = { text: 'javascript' }; | ||
| const startTime1 = Date.now(); | ||
| await searchManager.search(query, notes); | ||
| const duration1 = Date.now() - startTime1; | ||
|
|
||
| const startTime2 = Date.now(); | ||
| const results2 = await searchManager.search(query, notes); | ||
| const duration2 = Date.now() - startTime2; | ||
|
|
||
| // Cached query should be faster | ||
| assert.ok(duration2 <= duration1); | ||
| assert.strictEqual(results2.length, 1); | ||
| }); | ||
|
|
||
| test('should not use cache for different queries', async () => { | ||
| const notes = [ | ||
| createMockNote('note1', 'JavaScript content', 'Alice', '/workspace/file1.ts'), | ||
| createMockNote('note2', 'Python content', 'Bob', '/workspace/file2.ts') | ||
| ]; | ||
| await searchManager.buildIndex(notes); | ||
|
|
||
| const query1: SearchQuery = { text: 'javascript' }; | ||
| const query2: SearchQuery = { text: 'python' }; | ||
|
|
||
| const results1 = await searchManager.search(query1, notes); | ||
| const results2 = await searchManager.search(query2, notes); | ||
|
|
||
| assert.strictEqual(results1[0].note.id, 'note1'); | ||
| assert.strictEqual(results2[0].note.id, 'note2'); | ||
| }); | ||
|
|
||
| test('should invalidate cache on index update', async () => { | ||
| const notes = [ | ||
| createMockNote('note1', 'JavaScript content', 'Alice', '/workspace/file1.ts') | ||
| ]; | ||
| await searchManager.buildIndex(notes); | ||
|
|
||
| const query: SearchQuery = { text: 'javascript' }; | ||
| await searchManager.search(query, notes); | ||
|
|
||
| // Update index | ||
| const newNote = createMockNote('note2', 'JavaScript content', 'Bob', '/workspace/file2.ts'); | ||
| await searchManager.updateIndex(newNote); | ||
|
|
||
| // Search again - should get new results | ||
| const allNotes = [...notes, newNote]; | ||
| const results = await searchManager.search(query, allNotes); | ||
| assert.strictEqual(results.length, 2); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Cache key collision for regex queries (JSON.stringify drops RegExp)
SearchManager keys the cache via JSON.stringify(query). RegExp serializes to {}, so different regex patterns can collide and return wrong cached results. Add a test that issues two different regex queries and expects distinct results; fix keying to encode regex as {source, flags}.
Proposed test addition sketch:
const q1: SearchQuery = { regex: /foo/i };
const q2: SearchQuery = { regex: /bar/i };
await searchManager.search(q1, notes);
const r2 = await searchManager.search(q2, notes);
assert.notDeepStrictEqual(r2, await searchManager.search(q1, notes));Suggested fix in SearchManager (conceptual):
- Replace JSON.stringify(query) with a stableKey(query) that stringifies regex as
/{source}/{flags}and sorts object keys.
I'll verify the cache collision issue by examining the SearchManager implementation and the SearchQuery type definition.
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# Find SearchManager implementation
fd -t f "SearchManager" -o "searchManager" | head -20Length of output: 145
🏁 Script executed:
#!/bin/bash
# Search for cache-related code in SearchManager
rg -n "cache|JSON.stringify" --type ts -g "!test" -g "!*.test.ts" | grep -i search | head -30Length of output: 970
🏁 Script executed:
#!/bin/bash
# Find SearchQuery type definition
rg -n "interface SearchQuery|type SearchQuery" --type tsLength of output: 51
🏁 Script executed:
#!/bin/bash
# Look for regex field in SearchQuery
rg -n "regex\s*:" --type ts -C 3 | head -50Length of output: 314
Let me examine the SearchQuery type definition and the cache key implementation more closely.
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# View SearchQuery interface definition
cat src/searchTypes.ts | head -100Length of output: 1792
🏁 Script executed:
#!/bin/bash
# View getCacheKey implementation and surrounding context
sed -n '740,760p' src/searchManager.tsLength of output: 467
🏁 Script executed:
#!/bin/bash
# View the full getCacheKey method and related cache functions
sed -n '742,800p' src/searchManager.tsLength of output: 1285
Now let me verify if there are any existing tests using regex queries and check if there's any custom serialization logic.
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# Search for regex usage in test files
rg -n "regex\s*:" src/test/suite/searchManager.test.ts -A 2 -B 2Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check if there's any custom serialization or special handling for regex in getCacheKey
rg -n "getCacheKey|stableKey|toJSON" src/searchManager.ts -A 5 -B 2Length of output: 540
🏁 Script executed:
#!/bin/bash
# Verify JSON.stringify behavior with RegExp by looking for any workarounds
rg -n "JSON\.|replacer|toJSON" src/searchManager.tsLength of output: 37
🏁 Script executed:
#!/bin/bash
# Check if regex queries are actually executed anywhere in the codebase
rg -n "query\.regex" src/searchManager.ts -A 2 -B 2Length of output: 1372
Fix cache key serialization to properly handle RegExp patterns
The review comment is accurate. getCacheKey() at line 745 uses JSON.stringify(query), which serializes RegExp objects to {}, causing cache key collisions. Since query.regex is actively used (e.g., line 164 calls searchRegex() when regex is present), different regex patterns will collide and return incorrect cached results.
Current tests only cover text-based queries; no regex query caching tests exist to detect this bug.
Required fixes:
- Replace
JSON.stringify(query)with a stable key function that serializes regex as{source, flags} - Add test case with distinct regex patterns to verify no collision occurs
| const startTime1 = Date.now(); | ||
| await searchManager.search(query, notes); | ||
| const duration1 = Date.now() - startTime1; | ||
|
|
||
| const startTime2 = Date.now(); | ||
| const results2 = await searchManager.search(query, notes); | ||
| const duration2 = Date.now() - startTime2; | ||
|
|
||
| // Cached query should be faster | ||
| assert.ok(duration2 <= duration1); | ||
| assert.strictEqual(results2.length, 1); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid timing-based assertions for cache behavior
Duration comparisons are flaky under CI noise. Prefer asserting semantic cache effects (same results, cache hit count) or drop the time delta check.
Example: expose a lightweight getCacheStats() in SearchManager for tests, or remove the duration assertion and keep result equality checks. Based on learnings.
🤖 Prompt for AI Agents
In src/test/suite/searchManager.test.ts around lines 491 to 502, the test uses a
timing-based assertion to verify cache behavior (duration2 <= duration1), which
is flaky; remove the timing assertion and instead either (A) assert semantic
cache effects by exposing and checking a lightweight getCacheStats()/hit counter
on SearchManager (call before/after and assert hit count increased) or (B) at
minimum remove the duration check and assert equality of results (and length)
for both calls; update the test to use one of these approaches so CI noise
cannot break the test.
…te' into 11-feature-tags-and-categories-for-notes
Summary
This PR implements Epic 3: Organization & Categorization with three major features that significantly enhance note organization and discoverability:
These features work together to provide a complete note management experience, making it easy to organize, find, and navigate notes across large codebases.
🎯 Feature 1: Workspace Sidebar
What's New
Key Capabilities
Technical Implementation
Files Added/Modified
src/notesSidebarProvider.ts- Main sidebar provider (261 lines)src/noteTreeItem.ts- Tree item implementations (158 lines)src/test/suite/notesSidebarProvider.test.ts- Tests (402 lines)src/extension.ts- Sidebar registration and commands🔍 Feature 2: Advanced Search & Filter
What's New
Key Capabilities
/pattern/flagsfor complex searchesSearch Examples
Technical Implementation
Files Added/Modified
src/searchManager.ts- Core search engine (917 lines)src/searchUI.ts- QuickPick UI implementation (697 lines)src/searchTypes.ts- Type definitions (165 lines)src/test/suite/searchManager.test.ts- Tests (860 lines)🏷️ Feature 3: Tags & Categories
What's New
Predefined Categories
Key Capabilities
Usage Examples
Technical Implementation
Files Added/Modified
src/tagManager.ts- Tag validation and operations (278 lines)src/tagInputUI.ts- Tag selection UI (232 lines)src/tagTypes.ts- Type definitions and constants (110 lines)src/test/suite/tagManager.test.ts- Tests (683 lines)src/types.ts- Added tags field to Note interface📊 Statistics
Code Changes
Test Summary
Documentation
🎨 User Experience
Before
After
Example Workflow
✅ Acceptance Criteria
Sidebar (User Story 3.2) - ✅ Complete
Search (User Story 3.3) - ✅ Complete
Tags (User Story 3.1) - ✅ Complete
🧪 Testing
All features are thoroughly tested with unit and integration tests:
Test Results: 100+ tests passing with comprehensive coverage of:
📚 Documentation
README Updates
User Stories
docs/sidebar-view-for-browsing-all-notes/USER_STORY.md(364 lines)docs/search-and-filter-notes/USER_STORY.md(482 lines)docs/tags-and-categories/USER_STORY.md(135 lines)Changelogs
docs/changelogs/v0.2.0.md- Sidebar featuredocs/changelogs/v0.3.0.md- Search featuredocs/changelogs/v0.4.0.md- Tags feature🔄 Migration & Compatibility
Breaking Changes
Data Migration
Version Support
🚀 Performance
Optimizations
Benchmarks
🎯 What's Next
This PR completes Epic 3: Organization & Categorization. Future enhancements could include:
Test Plan
Manual Testing Checklist
Sidebar:
Search:
Tags:
Automated Testing
All automated tests passing:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Configuration