feat: Add Sidebar, Search, and Tags - Epic 3 Organization Features#14
feat: Add Sidebar, Search, and Tags - Epic 3 Organization Features#14
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.
|
📝 WalkthroughWalkthroughAdds workspace-wide sidebar, tag/category management, and an advanced search subsystem (indexing, filtering, UI). Implements tag-aware storage, CodeLens/tag displays, tag input UI, sidebar tree provider, search manager/UI, configuration updates, and extensive docs and tests. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CommentCtrl as CommentController
participant TagInputUI
participant TagManager
participant NoteManager
participant StorageManager
User->>CommentCtrl: Create note (save)
CommentCtrl->>TagInputUI: dynamically import & show tag picker
TagInputUI->>TagManager: request categories/suggestions
TagManager-->>TagInputUI: categories & suggestions
User->>TagInputUI: select/input tags
TagInputUI->>TagManager: validate & normalize
TagManager-->>TagInputUI: normalized tags
TagInputUI-->>CommentCtrl: return tags[]
CommentCtrl->>NoteManager: create note with tags
NoteManager->>StorageManager: save note (includes tags)
StorageManager-->>NoteManager: saved note
NoteManager-->>CommentCtrl: created note (with tags)
sequenceDiagram
participant User
participant SearchUI
participant SearchManager
participant NoteManager
participant StorageManager
Note over SearchManager: Background indexing on activation
StorageManager->>SearchManager: load notes
SearchManager->>SearchManager: build inverted + tag index
User->>SearchUI: open search (cmd)
SearchUI->>SearchManager: query (text + tags + filters)
SearchManager->>SearchManager: cache lookup
alt cache hit
SearchManager-->>SearchUI: cached results
else
SearchManager->>SearchManager: query index, apply tag filter (any/all), rank
SearchManager-->>SearchUI: results
end
SearchUI->>NoteManager: open selected note
NoteManager-->>User: navigate to note location
sequenceDiagram
participant User
participant Sidebar as NotesSidebarProvider
participant TagManager
participant NoteManager
NoteManager->>Sidebar: initialize provider (notes list)
Sidebar->>Sidebar: build tree (files → notes)
Sidebar-->>User: render tree
User->>Sidebar: invoke filter-by-tags
Sidebar->>TagManager: get tag suggestions/stats
TagManager-->>Sidebar: tags & counts
User->>Sidebar: select tags + mode (any/all)
Sidebar->>Sidebar: setTagFilters(tags, mode) and debounce refresh
Sidebar->>Sidebar: filter nodes, emit onDidChangeTreeData
Sidebar-->>User: updated filtered tree
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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.
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.
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.
🧩 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.
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.
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.
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.
🛠️ 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.
🧩 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.
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
…idebar Co-authored-by: jnahian <11865446+jnahian@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements Epic 3: Organization & Categorization features, adding comprehensive note organization capabilities through three major features: Tags & Categories, Advanced Search & Filter, and Workspace Sidebar. The implementation adds significant functionality to help developers organize and find notes across large codebases.
Changes:
- Added Tags & Categories system with 7 predefined categories (TODO, FIXME, BUG, QUESTION, NOTE, IMPROVEMENT, REVIEW) and custom tag support with validation, autocomplete, and visual display in CodeLens and sidebar
- Implemented Advanced Search with full-text search engine using inverted index, regex support, multiple filters (author, date, file, tags), search history, and relevance scoring
- Enhanced Workspace Sidebar with tag filtering capabilities using AND/OR logic, real-time updates, and configurable sorting
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types.ts | Added tags field to Note, NoteMetadata, CreateNoteParams, and UpdateNoteParams interfaces |
| src/tagTypes.ts | New file defining tag types, predefined categories with styles, and tag-related interfaces |
| src/tagManager.ts | Core tag logic with validation, normalization, filtering, and statistics (278 lines) |
| src/tagInputUI.ts | Tag selection UI with autocomplete and multi-select QuickPick (232 lines) |
| src/searchTypes.ts | Added tags and tagFilterMode fields to SearchQuery interface |
| src/searchManager.ts | Search engine with inverted index, tag indexing, stop word filtering, caching, and history (917 lines) |
| src/searchUI.ts | QuickPick-based search interface with live results, filters, and history (697 lines) |
| src/storageManager.ts | Tag serialization/deserialization in markdown format |
| src/notesSidebarProvider.ts | Added tag filtering with matchesTagFilter method and filter management |
| src/extension.ts | Search index initialization, command registration for search and tag filtering |
| src/noteManager.ts | Tag support in createNote and updateNote operations |
| src/commentController.ts | Tag input integration during note creation |
| src/codeLensProvider.ts | Tag display in CodeLens with truncation logic |
| src/test/suite/tagManager.test.ts | Comprehensive tag tests (683 lines, 100+ test cases) |
| src/test/suite/searchManager.test.ts | Search engine tests (860 lines, 35+ test suites) |
| src/test/suite/storageManager.test.ts | Tag serialization tests added to existing suite |
| src/test/runUnitTests.ts | Updated test runner with comment about search tests |
| package.json | New commands, keybindings (Ctrl+Shift+F), and configuration options for search |
| docs/tags-and-categories/USER_STORY.md | Complete user story with implementation details (135 lines) |
| docs/search-and-filter-notes/USER_STORY.md | Updated progress tracking showing 83% completion |
| docs/changelogs/v0.3.0.md | Search feature changelog with testing details |
| docs/changelogs/v0.4.0.md | Tags feature changelog with usage examples (130 lines) |
| docs/architecture/ARCHITECTURE.md | Updated architecture docs with SearchManager and SearchUI sections |
| README.md | Comprehensive documentation of all three features with examples (459 new lines) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Create file nodes, applying tag filters | ||
| for (const [filePath, notes] of notesByFile.entries()) { | ||
| // Filter notes by tags if filters are active | ||
| const filteredNotes = | ||
| this.activeTagFilters.length > 0 | ||
| ? notes.filter((note) => this.matchesTagFilter(note)) | ||
| : notes; | ||
|
|
||
| // Only create file node if it has notes after filtering | ||
| if (filteredNotes.length > 0) { | ||
| fileNodes.push( | ||
| new FileTreeItem(filePath, filteredNotes, this.workspaceRoot) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Tag filtering in the sidebar uses a synchronous filter operation on the notes array for each file. For workspaces with many notes (1000+), this could cause UI lag. Consider implementing the tag filtering at the getNotesByFile level in NoteManager to avoid filtering notes multiple times (once per file). Alternatively, document the expected performance characteristics for large workspaces.
| // Index tags | ||
| if (note.tags && note.tags.length > 0) { | ||
| for (const tag of note.tags) { | ||
| if (!this.tagIndex.has(tag)) { | ||
| this.tagIndex.set(tag, new Set()); | ||
| } | ||
| this.tagIndex.get(tag)!.add(note.id); | ||
| } | ||
| } |
There was a problem hiding this comment.
The tag index is cleared and rebuilt on every index update, even when updating a single note. This could be inefficient for incremental updates. Consider implementing incremental tag index updates similar to how the content index is updated - remove old tags for the note being updated, then add new tags. This would improve performance when notes are frequently edited.
| const maxPreviewLength = 50 - tagsDisplay.length; | ||
| const preview = firstLine.length > maxPreviewLength | ||
| ? firstLine.substring(0, maxPreviewLength - 3) + '...' | ||
| : firstLine; |
There was a problem hiding this comment.
The CodeLens preview length calculation subtracts tag display length from a fixed 50/35 character limit. However, if a note has many tags, this could result in a very short or even negative preview length. Consider enforcing a minimum preview length (e.g., 20 characters) and truncating tags instead if needed, or adjusting the overall CodeLens width budget dynamically. This ensures users always see meaningful note content even for heavily tagged notes.
| suite('Tag Serialization and Deserialization', () => { | ||
| test('should save and load note with tags', async () => { | ||
| const noteWithTags: Note = { | ||
| ...testNote, | ||
| tags: ['TODO', 'BUG', 'authentication'] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithTags); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithTags.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.ok(loadedNote!.tags); | ||
| assert.strictEqual(loadedNote!.tags!.length, 3); | ||
| assert.ok(loadedNote!.tags!.includes('TODO')); | ||
| assert.ok(loadedNote!.tags!.includes('BUG')); | ||
| assert.ok(loadedNote!.tags!.includes('authentication')); | ||
| }); | ||
|
|
||
| test('should preserve tag order', async () => { | ||
| const noteWithTags: Note = { | ||
| ...testNote, | ||
| tags: ['zebra', 'apple', 'middle'] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithTags); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithTags.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.deepStrictEqual(loadedNote!.tags, ['zebra', 'apple', 'middle']); | ||
| }); | ||
|
|
||
| test('should handle note with single tag', async () => { | ||
| const noteWithTag: Note = { | ||
| ...testNote, | ||
| tags: ['TODO'] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithTag); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithTag.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.strictEqual(loadedNote!.tags!.length, 1); | ||
| assert.strictEqual(loadedNote!.tags![0], 'TODO'); | ||
| }); | ||
|
|
||
| test('should handle note with empty tags array', async () => { | ||
| const noteWithNoTags: Note = { | ||
| ...testNote, | ||
| tags: [] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithNoTags); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithNoTags.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.ok(Array.isArray(loadedNote!.tags)); | ||
| assert.strictEqual(loadedNote!.tags!.length, 0); | ||
| }); | ||
|
|
||
| test('should handle note with undefined tags', async () => { | ||
| const noteWithUndefinedTags: Note = { | ||
| ...testNote, | ||
| tags: undefined | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithUndefinedTags); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithUndefinedTags.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| // Should be undefined or empty array after loading | ||
| assert.ok(!loadedNote!.tags || loadedNote!.tags.length === 0); | ||
| }); | ||
|
|
||
| test('should handle tags with special characters', async () => { | ||
| const noteWithSpecialTags: Note = { | ||
| ...testNote, | ||
| tags: ['tag-with-dash', 'tag_with_underscore', 'tag.with.dot', 'tag#123'] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithSpecialTags); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithSpecialTags.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.strictEqual(loadedNote!.tags!.length, 4); | ||
| assert.ok(loadedNote!.tags!.includes('tag-with-dash')); | ||
| assert.ok(loadedNote!.tags!.includes('tag_with_underscore')); | ||
| assert.ok(loadedNote!.tags!.includes('tag.with.dot')); | ||
| assert.ok(loadedNote!.tags!.includes('tag#123')); | ||
| }); | ||
|
|
||
| test('should handle tags with spaces (trimmed)', async () => { | ||
| const noteWithSpacedTags: Note = { | ||
| ...testNote, | ||
| tags: ['tag with spaces', 'another tag'] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithSpacedTags); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithSpacedTags.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.strictEqual(loadedNote!.tags!.length, 2); | ||
| assert.ok(loadedNote!.tags!.includes('tag with spaces')); | ||
| assert.ok(loadedNote!.tags!.includes('another tag')); | ||
| }); | ||
|
|
||
| test('should format tags in markdown correctly', async () => { | ||
| const noteWithTags: Note = { | ||
| ...testNote, | ||
| tags: ['TODO', 'BUG', 'custom'] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithTags); | ||
| const filePath = storageManager.getNoteFilePath(noteWithTags.id); | ||
| const content = await fs.readFile(filePath, 'utf-8'); | ||
|
|
||
| assert.ok(content.includes('**Tags:** TODO, BUG, custom')); | ||
| }); | ||
|
|
||
| test('should not include Tags line when no tags', async () => { | ||
| const noteWithoutTags: Note = { | ||
| ...testNote, | ||
| tags: [] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithoutTags); | ||
| const filePath = storageManager.getNoteFilePath(noteWithoutTags.id); | ||
| const content = await fs.readFile(filePath, 'utf-8'); | ||
|
|
||
| // Should not have a Tags line | ||
| const lines = content.split('\n'); | ||
| const tagsLine = lines.find(line => line.startsWith('**Tags:**')); | ||
| assert.strictEqual(tagsLine, undefined); | ||
| }); | ||
|
|
||
| test('should handle many tags', async () => { | ||
| const manyTags = Array.from({ length: 20 }, (_, i) => `tag${i}`); | ||
| const noteWithManyTags: Note = { | ||
| ...testNote, | ||
| tags: manyTags | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithManyTags); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithManyTags.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.strictEqual(loadedNote!.tags!.length, 20); | ||
| for (let i = 0; i < 20; i++) { | ||
| assert.ok(loadedNote!.tags!.includes(`tag${i}`)); | ||
| } | ||
| }); | ||
|
|
||
| test('should handle tags with maximum length', async () => { | ||
| const longTag = 'a'.repeat(50); | ||
| const noteWithLongTag: Note = { | ||
| ...testNote, | ||
| tags: [longTag, 'short'] | ||
| }; | ||
|
|
||
| await storageManager.saveNote(noteWithLongTag); | ||
| const loadedNote = await storageManager.loadNoteById(noteWithLongTag.id); | ||
|
|
||
| assert.ok(loadedNote); | ||
| assert.strictEqual(loadedNote!.tags!.length, 2); | ||
| assert.ok(loadedNote!.tags!.includes(longTag)); | ||
| assert.ok(loadedNote!.tags!.includes('short')); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The storage tests verify that tags are persisted and loaded correctly, but they don't test edge cases like empty tags array vs undefined tags field, or tags with special characters that might need escaping in the markdown format (e.g., tags containing commas in the display, even though validation prevents this). Consider adding tests for these edge cases to ensure robust serialization/deserialization.
| * Show the search UI | ||
| */ | ||
| async show(): Promise<void> { | ||
| // Load all notes for searching |
There was a problem hiding this comment.
The SearchUI class stores allNotes as a class property and loads them once when show() is called. If notes are created, updated, or deleted while the search UI is open, the search results could become stale and show outdated information. Consider either refreshing the notes list periodically, listening for note change events from NoteManager, or documenting this limitation in the code comments.
| // Load all notes for searching | |
| // Load all notes for searching. | |
| // NOTE: This loads a snapshot of all notes at the time the search UI is opened. | |
| // If notes are created, updated, or deleted while the QuickPick is open, those | |
| // changes will not be reflected in the current search results. Reopen the search | |
| // UI to refresh, or refactor this logic to refresh notes or listen to NoteManager | |
| // change events if live-updating results are required. |
| // If user cancelled tag input, cancel note creation | ||
| if (tags === undefined) { | ||
| thread.dispose(); | ||
| if (tempId) { | ||
| this.commentThreads.delete(tempId); | ||
| } | ||
| this.currentlyCreatingThreadId = null; |
There was a problem hiding this comment.
The tag input cancellation logic clears the thread and returns early if the user cancels tag selection. However, the user has already entered note content at this point. Consider preserving the partially created note or warning the user that canceling tag selection will discard their note content. This could lead to frustration if users accidentally cancel and lose their work.
| // If user cancelled tag input, cancel note creation | |
| if (tags === undefined) { | |
| thread.dispose(); | |
| if (tempId) { | |
| this.commentThreads.delete(tempId); | |
| } | |
| this.currentlyCreatingThreadId = null; | |
| // If user cancelled tag input, confirm before discarding the note content | |
| if (tags === undefined) { | |
| const choice = await vscode.window.showWarningMessage( | |
| "Cancelling tag selection will discard your note content. Do you want to discard this note?", | |
| { modal: true }, | |
| "Discard note", | |
| "Continue editing" | |
| ); | |
| // Only discard if the user explicitly confirms | |
| if (choice === "Discard note") { | |
| thread.dispose(); | |
| if (tempId) { | |
| this.commentThreads.delete(tempId); | |
| } | |
| this.currentlyCreatingThreadId = null; | |
| } | |
| // If the user chooses "Continue editing" or closes the dialog, | |
| // keep the thread and current state so they can continue editing. |
| ], | ||
| "default": "file", | ||
| "description": "Sort notes by file path" | ||
| "description": "Sort notes by: file path, date, or author (file path only in v0.2.0)" |
There was a problem hiding this comment.
The configuration setting description for sidebar.sortBy mentions "file path only in v0.2.0" but this PR appears to be implementing v0.2.0-v0.4.0 features together. The sort functionality by date and author appears to already be implemented in notesSidebarProvider.ts lines 161-183. Consider updating this description to accurately reflect the current capabilities, or remove the version-specific caveat.
| private readonly STOP_WORDS = new Set([ | ||
| 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', | ||
| 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been', | ||
| 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', | ||
| 'could', 'may', 'might', 'can', 'this', 'that', 'these', 'those', 'it', | ||
| 'its', 'we', 'you', 'they', 'them', 'their', 'our', 'your', 'my', 'me' | ||
| ]); |
There was a problem hiding this comment.
The stop words set is defined as a class property using a Set literal, but it's marked as readonly. Since this is a relatively large set (48 words) that's instantiated for every SearchManager instance, consider making it a static constant outside the class to avoid duplicating it in memory for each instance. This would be more memory-efficient, especially if multiple SearchManager instances are created during testing.
| // Only include tests that don't require vscode | ||
| // searchManager.test.js runs as an integration test since it needs vscode API | ||
| return basename === 'storageManager.test.js' || basename === 'gitIntegration.test.js'; |
There was a problem hiding this comment.
The comment indicates that searchManager.test.js runs as an integration test, but the test file at src/test/suite/searchManager.test.ts creates a mock VSCode context rather than using the real VSCode API. This suggests it should be able to run as a unit test. Consider adding it to the unit test suite, or if it truly requires VSCode API, update the comment to be more specific about which VSCode APIs are required.
| // Only include tests that don't require vscode | |
| // searchManager.test.js runs as an integration test since it needs vscode API | |
| return basename === 'storageManager.test.js' || basename === 'gitIntegration.test.js'; | |
| // Only include tests that don't require vscode or that use a mocked vscode context | |
| // searchManager.test.js uses a mocked vscode context and can run as a unit test | |
| return basename === 'storageManager.test.js' | |
| || basename === 'gitIntegration.test.js' | |
| || basename === 'searchManager.test.js'; |
| // Combine suggestions | ||
| const allSuggestions = [ | ||
| ...predefinedCategories, | ||
| ...suggestedTags.filter((tag) => !TagManager.isPredefinedCategory(tag)), | ||
| ]; | ||
|
|
There was a problem hiding this comment.
Unused variable allSuggestions.
| // Combine suggestions | |
| const allSuggestions = [ | |
| ...predefinedCategories, | |
| ...suggestedTags.filter((tag) => !TagManager.isPredefinedCategory(tag)), | |
| ]; |
Co-authored-by: jnahian <11865446+jnahian@users.noreply.github.com>
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
Documentation
Tests