Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,24 @@
"title": "View History",
"icon": "$(history)",
"category": "Code Notes"
},
{
"command": "codeContextNotes.searchNotes",
"title": "Search Notes",
"icon": "$(search)",
"category": "Code Notes"
},
{
"command": "codeContextNotes.filterByTags",
"title": "Filter by Tags",
"icon": "$(tag)",
"category": "Code Notes"
},
{
"command": "codeContextNotes.clearTagFilters",
"title": "Clear Tag Filters",
"icon": "$(clear-all)",
"category": "Code Notes"
}
],
"keybindings": [
Expand Down Expand Up @@ -223,6 +241,12 @@
"mac": "cmd+alt+r",
"when": "editorTextFocus"
},
{
"command": "codeContextNotes.searchNotes",
"key": "ctrl+alt+shift+f",
"mac": "cmd+alt+shift+f",
"when": "editorTextFocus && !searchViewletFocus && !replaceInputBoxFocus"
},
{
"command": "codeContextNotes.insertBold",
"key": "ctrl+b",
Expand Down Expand Up @@ -268,10 +292,20 @@
"when": "view == codeContextNotes.sidebarView",
"group": "navigation@1"
},
{
"command": "codeContextNotes.searchNotes",
"when": "view == codeContextNotes.sidebarView",
"group": "navigation@2"
},
{
"command": "codeContextNotes.refreshSidebar",
"when": "view == codeContextNotes.sidebarView",
"group": "navigation@3"
},
{
"command": "codeContextNotes.filterByTags",
"when": "view == codeContextNotes.sidebarView",
"group": "navigation@4"
}
],
"view/item/context": [
Expand Down
53 changes: 45 additions & 8 deletions src/codeLensProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import * as vscode from 'vscode';
import { Note } from './types.js';
import { NoteManager } from './noteManager.js';

// Preview length constants
const MIN_PREVIEW_LENGTH = 10; // Minimum characters for note preview
const MAX_PREVIEW_LENGTH_SINGLE_NOTE = 50; // Max preview length for single note
const MAX_PREVIEW_LENGTH_MULTI_NOTE = 35; // Max preview length for multiple notes
const MAX_TAGS_TO_DISPLAY = 2; // Max tags to show before truncating

/**
* CodeLensProvider displays indicators above lines with notes
*/
Expand Down Expand Up @@ -154,31 +160,62 @@ export class CodeNotesLensProvider implements vscode.CodeLensProvider {
private formatCodeLensTitle(notes: Note[]): string {
if (notes.length === 1) {
const note = notes[0];

// 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) with minimum guard
const maxPreviewLength = Math.max(MIN_PREVIEW_LENGTH, MAX_PREVIEW_LENGTH_SINGLE_NOTE - 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})`;
} else {
// Multiple notes - show count and authors
const uniqueAuthors = [...new Set(notes.map(n => n.author))];
const authorsDisplay = uniqueAuthors.length > 2
? `${uniqueAuthors.slice(0, 2).join(', ')} +${uniqueAuthors.length - 2} more`
: uniqueAuthors.join(', ');

// Collect all unique tags from all notes
const allTags = new Set<string>();
notes.forEach(note => {
if (note.tags) {
note.tags.forEach(tag => allTags.add(tag));
}
});

// Format tags for display (limit to MAX_TAGS_TO_DISPLAY if many)
let tagsDisplay = '';
if (allTags.size > 0) {
const tagArray = Array.from(allTags);
const displayTags = tagArray.slice(0, MAX_TAGS_TO_DISPLAY);
tagsDisplay = displayTags.map(tag => `[${tag}]`).join(' ');
if (tagArray.length > MAX_TAGS_TO_DISPLAY) {
tagsDisplay += ` +${tagArray.length - MAX_TAGS_TO_DISPLAY}`;
}
tagsDisplay += ' ';
}

// Get preview from first note
const plainText = this.stripMarkdown(notes[0].content);
const firstLine = plainText.split('\n')[0];
const preview = firstLine.length > 35
? firstLine.substring(0, 32) + '...'
const maxPreviewLength = Math.max(MIN_PREVIEW_LENGTH, MAX_PREVIEW_LENGTH_MULTI_NOTE - tagsDisplay.length);
const preview = firstLine.length > maxPreviewLength
? firstLine.substring(0, maxPreviewLength - 3) + '...'
: firstLine;

// Format: "📝 Notes (3): preview... (by author1, author2)"
return `📝 Notes (${notes.length}): ${preview} (${authorsDisplay})`;
// Format: "📝 [TODO] [bug] Notes (3): preview... (by author1, author2)"
return `📝 ${tagsDisplay}Notes (${notes.length}): ${preview} (${authorsDisplay})`;
}
}

Expand Down
27 changes: 26 additions & 1 deletion src/commentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,12 +525,35 @@ export class CommentController {
end: thread.range.end.line,
};

// 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
console.error('Failed to load tag input UI:', e);
tags = [];
}

// If user cancelled tag input (explicit dismissal), cancel note creation
if (tags === undefined) {
thread.dispose();
if (tempId) {
this.commentThreads.delete(tempId);
}
this.currentlyCreatingThreadId = null;
return;
}

// Create the actual note
const note = await this.noteManager.createNote(
{
filePath: document.uri.fsPath,
lineRange,
content,
tags,
},
document
);
Expand All @@ -554,7 +577,8 @@ export class CommentController {
async handleCreateNote(
document: vscode.TextDocument,
range: vscode.Range,
content: string
content: string,
tags?: string[]
): Promise<Note> {
const lineRange: LineRange = {
start: range.start.line,
Expand All @@ -566,6 +590,7 @@ export class CommentController {
filePath: document.uri.fsPath,
lineRange,
content,
tags,
},
document
);
Expand Down
104 changes: 95 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ import { CommentController } from './commentController.js';
import { CodeNotesLensProvider } from './codeLensProvider.js';
import { NotesSidebarProvider } from './notesSidebarProvider.js';
import { SearchManager } from './searchManager.js';
import { SearchUI } from './searchUI.js';

let noteManager: NoteManager;
let searchManager: SearchManager;
let commentController: CommentController;
let codeLensProvider: CodeNotesLensProvider;
let sidebarProvider: NotesSidebarProvider;
let searchManager: SearchManager;

// Debounce timers for performance optimization
const documentChangeTimers: Map<string, NodeJS.Timeout> = new Map();
const DEBOUNCE_DELAY = 500; // ms
const LARGE_INDEX_THRESHOLD = 100; // Show completion message for large indexes
const INDEX_BUILD_DELAY = 1000; // Delay to not block activation (ms)

/**
* Extension activation
Expand Down Expand Up @@ -80,15 +83,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Initialize search manager
searchManager = new SearchManager(context);

// Connect search manager to note manager
// Link search manager to note manager (avoids circular dependency)
noteManager.setSearchManager(searchManager);

// Build initial search index with all existing notes
console.log('Code Context Notes: Building initial search index...');
const allNotes = await noteManager.getAllNotes();
await searchManager.buildIndex(allNotes);
console.log(`Code Context Notes: Search index built with ${allNotes.length} notes`);

// Initialize comment controller
commentController = new CommentController(noteManager, context);

Expand All @@ -112,6 +109,37 @@ export async function activate(context: vscode.ExtensionContext) {
});
context.subscriptions.push(treeView);

// Build search index in background with progress notification
console.log('Code Context Notes: Building search index...');
setTimeout(async () => {
try {
// Show progress for large workspaces
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Code Context Notes",
cancellable: false
}, async (progress) => {
progress.report({ message: "Building search index..." });

const allNotes = await noteManager.getAllNotes();
await searchManager.buildIndex(allNotes);

progress.report({ message: `Search index ready (${allNotes.length} notes)` });
console.log(`Code Context Notes: Search index built with ${allNotes.length} notes`);

// Show completion message for large indexes
if (allNotes.length > LARGE_INDEX_THRESHOLD) {
setTimeout(() => {
vscode.window.showInformationMessage(`Code Context Notes: Search index ready with ${allNotes.length} notes`);
}, 500);
}
});
} catch (error) {
console.error('Code Context Notes: Failed to build search index:', error);
vscode.window.showErrorMessage(`Code Context Notes: Failed to build search index: ${error}`);
}
}, INDEX_BUILD_DELAY);

// Set up event listeners
setupEventListeners(context);

Expand Down Expand Up @@ -832,6 +860,25 @@ function registerAllCommands(context: vscode.ExtensionContext) {
}
);

// Search Notes
const searchNotesCommand = vscode.commands.registerCommand(
'codeContextNotes.searchNotes',
async () => {
if (!noteManager || !searchManager) {
vscode.window.showErrorMessage('Code Context Notes requires a workspace folder to be opened.');
return;
}

try {
const searchUI = new SearchUI(searchManager, noteManager);
await searchUI.show();
} catch (error) {
console.error('Search failed:', error);
vscode.window.showErrorMessage(`Search failed: ${error}`);
}
}
);

// Collapse All in Sidebar
const collapseAllCommand = vscode.commands.registerCommand(
'codeContextNotes.collapseAll',
Expand Down Expand Up @@ -932,6 +979,42 @@ function registerAllCommands(context: vscode.ExtensionContext) {
}
);

// Filter Notes by Tags
const filterByTagsCommand = vscode.commands.registerCommand(
'codeContextNotes.filterByTags',
async () => {
if (!noteManager || !sidebarProvider) {
vscode.window.showErrorMessage('Code Context Notes requires a workspace folder to be opened.');
return;
}

try {
const { TagInputUI } = await import('./tagInputUI.js');
const allNotes = await noteManager.getAllNotes();
const selectedTags = await TagInputUI.showTagFilter(allNotes);

if (selectedTags && selectedTags.length > 0) {
sidebarProvider.setTagFilters(selectedTags, 'any');
vscode.window.showInformationMessage(`Filtering by tags: ${selectedTags.join(', ')}`);
}
} catch (error) {
vscode.window.showErrorMessage(`Failed to filter by tags: ${error}`);
}
}
);

// Clear Tag Filters
const clearTagFiltersCommand = vscode.commands.registerCommand(
'codeContextNotes.clearTagFilters',
() => {
if (!sidebarProvider) {
return;
}
sidebarProvider.clearTagFilters();
vscode.window.showInformationMessage('Tag filters cleared');
}
);

// Register all commands
context.subscriptions.push(
addNoteCommand,
Expand Down Expand Up @@ -959,11 +1042,14 @@ function registerAllCommands(context: vscode.ExtensionContext) {
addNoteToLineCommand,
openNoteFromSidebarCommand,
refreshSidebarCommand,
searchNotesCommand,
collapseAllCommand,
editNoteFromSidebarCommand,
deleteNoteFromSidebarCommand,
viewNoteHistoryFromSidebarCommand,
openFileFromSidebarCommand
openFileFromSidebarCommand,
filterByTagsCommand,
clearTagFiltersCommand
);
}

Expand Down
8 changes: 7 additions & 1 deletion src/noteManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export class NoteManager extends EventEmitter {
action: 'created'
}
],
isDeleted: false
isDeleted: false,
tags: params.tags || []
};

// Save to storage
Expand Down Expand Up @@ -140,6 +141,11 @@ export class NoteManager extends EventEmitter {
note.author = author;
note.updatedAt = now;

// Update tags if provided
if (params.tags !== undefined) {
note.tags = params.tags;
}

// Add history entry
note.history.push({
content: params.content.trim(),
Expand Down
6 changes: 3 additions & 3 deletions src/noteTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ export class NoteTreeItem extends BaseTreeItem {
*/
private createTooltip(): vscode.MarkdownString {
const tooltip = new vscode.MarkdownString();
tooltip.isTrusted = true;
tooltip.supportHtml = true;
tooltip.isTrusted = false;

const lineRange = `Lines ${this.note.lineRange.start + 1}-${this.note.lineRange.end + 1}`;
const created = new Date(this.note.createdAt).toLocaleString();
Expand All @@ -108,7 +107,8 @@ export class NoteTreeItem extends BaseTreeItem {
tooltip.appendMarkdown(`**Created:** ${created}\n\n`);
tooltip.appendMarkdown(`**Updated:** ${updated}\n\n`);
tooltip.appendMarkdown(`---\n\n`);
tooltip.appendMarkdown(this.note.content);
// Use appendText for user content to prevent injection
tooltip.appendText(this.note.content);

return tooltip;
}
Expand Down
Loading