mirror of
https://github.com/trevware/obsidian-sidebar-highlights.git
synced 2026-07-22 05:51:12 +00:00
853 lines
No EOL
32 KiB
TypeScript
853 lines
No EOL
32 KiB
TypeScript
// main.ts
|
|
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, TAbstractFile, WorkspaceLeaf, ItemView, Menu, debounce, setIcon } from 'obsidian';
|
|
import { HighlightsSidebarView } from './src/views/sidebar-view';
|
|
|
|
export interface Highlight {
|
|
id: string;
|
|
text: string;
|
|
tags: string[];
|
|
line: number;
|
|
startOffset: number;
|
|
endOffset: number;
|
|
filePath: string;
|
|
footnoteCount?: number;
|
|
footnoteContents?: string[];
|
|
color?: string;
|
|
collectionIds?: string[]; // Add collection support
|
|
createdAt?: number; // Timestamp when highlight was created
|
|
}
|
|
|
|
export interface Collection {
|
|
id: string;
|
|
name: string;
|
|
description?: string; // Add optional description field
|
|
highlightIds: string[];
|
|
createdAt: number;
|
|
}
|
|
|
|
interface CommentPluginSettings {
|
|
highlightColor: string;
|
|
sidebarPosition: 'left' | 'right';
|
|
autoOpenSidebar: boolean;
|
|
highlights: { [filePath: string]: Highlight[] };
|
|
collections: { [id: string]: Collection }; // Add collections to settings
|
|
groupingMode: 'none' | 'color' | 'comments-asc' | 'comments-desc' | 'tag' | 'parent' | 'collection' | 'filename' | 'date-created-asc' | 'date-created-desc'; // Add grouping mode persistence
|
|
}
|
|
|
|
const DEFAULT_SETTINGS: CommentPluginSettings = {
|
|
highlightColor: '#ffd700',
|
|
sidebarPosition: 'right',
|
|
autoOpenSidebar: true,
|
|
highlights: {},
|
|
collections: {}, // Initialize empty collections
|
|
groupingMode: 'none' // Default grouping mode
|
|
}
|
|
|
|
const VIEW_TYPE_HIGHLIGHTS = 'highlights-sidebar';
|
|
|
|
export default class HighlightCommentsPlugin extends Plugin {
|
|
settings: CommentPluginSettings;
|
|
highlights: Map<string, Highlight[]> = new Map();
|
|
collections: Map<string, Collection> = new Map();
|
|
collectionsManager: CollectionsManager;
|
|
private sidebarView: HighlightsSidebarView | null = null;
|
|
private detectHighlightsTimeout: NodeJS.Timeout | null = null;
|
|
public selectedHighlightId: string | null = null;
|
|
private collectionCommands: Set<string> = new Set(); // Track registered collection commands
|
|
public deletedCollectionNames: Map<string, string> = new Map(); // Track names of deleted collections
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
|
|
this.highlights = new Map(Object.entries(this.settings.highlights || {}));
|
|
this.collections = new Map(Object.entries(this.settings.collections || {}));
|
|
this.collectionsManager = new CollectionsManager(this);
|
|
|
|
this.registerView(
|
|
VIEW_TYPE_HIGHLIGHTS,
|
|
(leaf) => {
|
|
this.sidebarView = new HighlightsSidebarView(leaf, this);
|
|
return this.sidebarView;
|
|
}
|
|
);
|
|
|
|
this.addRibbonIcon('highlighter', 'Open Highlights', () => {
|
|
this.activateView();
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'create-highlight',
|
|
name: 'Create highlight from selection',
|
|
editorCallback: (editor: Editor) => {
|
|
this.createHighlight(editor);
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'open-highlights-sidebar',
|
|
name: 'Toggle',
|
|
callback: () => {
|
|
this.toggleView();
|
|
}
|
|
});
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on('editor-menu', (menu, editor, view) => {
|
|
if (editor.getSelection()) {
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle('Create highlight')
|
|
.setIcon('highlighter')
|
|
.onClick(() => {
|
|
this.createHighlight(editor);
|
|
});
|
|
});
|
|
}
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on('file-open', (file) => {
|
|
if (file) {
|
|
this.loadHighlightsFromFile(file);
|
|
} else {
|
|
this.highlights.clear();
|
|
this.selectedHighlightId = null;
|
|
this.refreshSidebar();
|
|
}
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on('editor-change', (editor, view) => {
|
|
if (view instanceof MarkdownView) {
|
|
this.debounceDetectMarkdownHighlights(editor, view);
|
|
}
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.vault.on('create', (file) => {
|
|
if (file instanceof TFile && file.extension === 'md') {
|
|
this.handleFileCreate(file);
|
|
}
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.vault.on('rename', (file, oldPath) => {
|
|
if (file instanceof TFile && file.extension === 'md') {
|
|
this.handleFileRename(file, oldPath);
|
|
}
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.vault.on('delete', (file) => {
|
|
if (file instanceof TFile && file.extension === 'md') {
|
|
this.handleFileDelete(file);
|
|
}
|
|
})
|
|
);
|
|
|
|
this.addSettingTab(new HighlightSettingTab(this.app, this));
|
|
this.addStyles();
|
|
|
|
// Register collection commands
|
|
this.registerCollectionCommands();
|
|
|
|
// Scan all files for highlights after workspace is ready
|
|
this.app.workspace.onLayoutReady(() => {
|
|
this.scanAllFilesForHighlights();
|
|
|
|
if (this.settings.autoOpenSidebar) {
|
|
this.activateView();
|
|
}
|
|
});
|
|
}
|
|
|
|
onunload() {
|
|
// Cleanup is mostly automatic due to using registerEvent() and addCommand()
|
|
// But we can explicitly clean up the sidebar view if needed
|
|
|
|
// The sidebar view's onClose() method will handle its own cleanup
|
|
// Obsidian automatically handles:
|
|
// - Registered events (this.registerEvent)
|
|
// - Registered commands (this.addCommand)
|
|
// - Settings tab removal
|
|
// - View unregistration
|
|
|
|
// Only manual cleanup needed is for any direct DOM listeners or intervals
|
|
// which we don't currently have in the main plugin file
|
|
console.log('Highlight Comments plugin unloaded');
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
// Save highlights and collections to settings before saving
|
|
this.settings.highlights = Object.fromEntries(this.highlights);
|
|
this.settings.collections = Object.fromEntries(this.collections);
|
|
await this.saveData(this.settings);
|
|
this.updateStyles();
|
|
}
|
|
|
|
addStyles() {
|
|
const style = document.createElement('style');
|
|
style.id = 'highlight-comments-plugin-styles';
|
|
|
|
// Set CSS custom property for highlight color
|
|
document.documentElement.style.setProperty('--highlight-color', this.settings.highlightColor);
|
|
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
updateStyles() {
|
|
// Update CSS custom property when settings change
|
|
document.documentElement.style.setProperty('--highlight-color', this.settings.highlightColor);
|
|
}
|
|
|
|
removeStyles() {
|
|
const style = document.getElementById('highlight-comments-plugin-styles');
|
|
if (style) {
|
|
style.remove();
|
|
}
|
|
// Clean up CSS custom property
|
|
document.documentElement.style.removeProperty('--highlight-color');
|
|
}
|
|
|
|
async activateView() {
|
|
const { workspace } = this.app;
|
|
let leaf: WorkspaceLeaf | null = null;
|
|
const leaves = workspace.getLeavesOfType(VIEW_TYPE_HIGHLIGHTS);
|
|
|
|
if (leaves.length > 0) {
|
|
leaf = leaves[0];
|
|
} else {
|
|
leaf = this.settings.sidebarPosition === 'left'
|
|
? workspace.getLeftLeaf(false)
|
|
: workspace.getRightLeaf(false);
|
|
await leaf?.setViewState({ type: VIEW_TYPE_HIGHLIGHTS, active: true });
|
|
}
|
|
if (leaf) {
|
|
workspace.revealLeaf(leaf);
|
|
}
|
|
}
|
|
|
|
async toggleView() {
|
|
const { workspace } = this.app;
|
|
const leaves = workspace.getLeavesOfType(VIEW_TYPE_HIGHLIGHTS);
|
|
|
|
if (leaves.length > 0) {
|
|
// Sidebar is open, close it
|
|
leaves.forEach(leaf => leaf.detach());
|
|
} else {
|
|
// Sidebar is closed, open it
|
|
await this.activateView();
|
|
}
|
|
}
|
|
|
|
async createHighlight(editor: Editor) {
|
|
const selection = editor.getSelection();
|
|
if (!selection) {
|
|
new Notice('Please select some text first');
|
|
return;
|
|
}
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (!file) {
|
|
new Notice('No active file');
|
|
return;
|
|
}
|
|
|
|
const fromCursor = editor.getCursor('from');
|
|
const toCursor = editor.getCursor('to');
|
|
const fromOffset = editor.posToOffset(fromCursor);
|
|
const toOffset = editor.posToOffset(toCursor);
|
|
const highlightId = this.generateId();
|
|
|
|
const highlight: Highlight = {
|
|
id: highlightId,
|
|
text: selection,
|
|
tags: [],
|
|
line: fromCursor.line,
|
|
startOffset: fromOffset,
|
|
endOffset: toOffset,
|
|
filePath: file.path,
|
|
createdAt: Date.now(),
|
|
};
|
|
|
|
const fileHighlights = this.highlights.get(file.path) || [];
|
|
fileHighlights.push(highlight);
|
|
this.highlights.set(file.path, fileHighlights);
|
|
|
|
const highlightedText = `==${selection}==`;
|
|
editor.replaceSelection(highlightedText);
|
|
this.refreshSidebar();
|
|
new Notice('Highlight created');
|
|
|
|
if (this.settings.autoOpenSidebar) {
|
|
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_HIGHLIGHTS);
|
|
if (leaves.length === 0) { // Only activate if not already open/visible
|
|
this.activateView();
|
|
}
|
|
}
|
|
}
|
|
|
|
refreshSidebar() {
|
|
if (this.sidebarView) {
|
|
this.sidebarView.refresh();
|
|
}
|
|
}
|
|
|
|
// Register dynamic commands for collections
|
|
public registerCollectionCommands() {
|
|
// Register commands for all existing collections
|
|
for (const collection of this.collections.values()) {
|
|
const commandId = `go-to-collection-${collection.id}`;
|
|
|
|
// Always register/re-register to update the name if it changed
|
|
this.addCommand({
|
|
id: commandId,
|
|
name: `Go to ${collection.name}`,
|
|
callback: () => {
|
|
this.goToCollection(collection.id);
|
|
}
|
|
});
|
|
|
|
this.collectionCommands.add(commandId);
|
|
}
|
|
}
|
|
|
|
// Update commands for deleted collections to show "(deleted)" in the name
|
|
public updateDeletedCollectionCommands() {
|
|
// Get all currently existing collection IDs
|
|
const existingCollectionIds = new Set(Array.from(this.collections.keys()));
|
|
|
|
// Find commands for collections that no longer exist
|
|
const deletedCommands = Array.from(this.collectionCommands).filter(commandId => {
|
|
const collectionId = commandId.replace('go-to-collection-', '');
|
|
return !existingCollectionIds.has(collectionId);
|
|
});
|
|
|
|
// Re-register deleted collection commands with updated names
|
|
for (const commandId of deletedCommands) {
|
|
const collectionId = commandId.replace('go-to-collection-', '');
|
|
const deletedCollectionName = this.deletedCollectionNames.get(collectionId) || 'Collection';
|
|
|
|
this.addCommand({
|
|
id: commandId,
|
|
name: `Go to ${deletedCollectionName} (deleted)`,
|
|
callback: () => {
|
|
new Notice(`Collection "${deletedCollectionName}" has been deleted`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Unregister all collection commands
|
|
private unregisterCollectionCommands() {
|
|
// Note: Obsidian doesn't provide a direct way to unregister commands
|
|
// Commands are automatically cleaned up when the plugin is unloaded
|
|
// We'll track them and they'll be replaced when registerCollectionCommands is called
|
|
this.collectionCommands.clear();
|
|
}
|
|
|
|
// Navigate to a specific collection in the sidebar
|
|
private async goToCollection(collectionId: string) {
|
|
// First, make sure the sidebar is open
|
|
await this.activateView();
|
|
|
|
// Then navigate to the collection
|
|
if (this.sidebarView) {
|
|
this.sidebarView.navigateToCollection(collectionId);
|
|
}
|
|
}
|
|
|
|
updateHighlight(highlightId: string, updates: Partial<Highlight>, filePath?: string) {
|
|
let determinedFilePath = filePath;
|
|
let fileHighlightsList: Highlight[] | undefined;
|
|
|
|
if (determinedFilePath) {
|
|
fileHighlightsList = this.highlights.get(determinedFilePath);
|
|
} else {
|
|
// If no filePath is provided, attempt to find the highlight by ID across all files.
|
|
// This assumes highlight IDs are globally unique if filePath is omitted.
|
|
for (const [path, highlightsInFile] of this.highlights) {
|
|
if (highlightsInFile.some(h => h.id === highlightId)) {
|
|
determinedFilePath = path;
|
|
fileHighlightsList = highlightsInFile;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If still no path after searching, and filePath was not initially provided,
|
|
// fallback to active file to maintain some compatibility if a caller truly doesn't know the path.
|
|
if (!determinedFilePath && !filePath) {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile) {
|
|
determinedFilePath = activeFile.path;
|
|
fileHighlightsList = this.highlights.get(determinedFilePath);
|
|
}
|
|
}
|
|
|
|
if (!determinedFilePath || !fileHighlightsList) {
|
|
console.error(`Cannot update highlight ${highlightId}: File path "${determinedFilePath}" invalid or highlight list not found.`);
|
|
return;
|
|
}
|
|
|
|
const highlightIndex = fileHighlightsList.findIndex(h => h.id === highlightId);
|
|
|
|
if (highlightIndex !== -1) {
|
|
const updatedHighlight = { ...fileHighlightsList[highlightIndex], ...updates };
|
|
// Create a new array for the highlights of the specific file to ensure reactivity
|
|
const newFileHighlightsList = [...fileHighlightsList];
|
|
newFileHighlightsList[highlightIndex] = updatedHighlight;
|
|
|
|
this.highlights.set(determinedFilePath, newFileHighlightsList);
|
|
this.saveSettings();
|
|
this.refreshSidebar();
|
|
} else {
|
|
console.warn(`Highlight ${highlightId} not found in file ${determinedFilePath} for update.`);
|
|
}
|
|
}
|
|
|
|
async loadHighlightsFromFile(file: TFile) {
|
|
const content = await this.app.vault.read(file);
|
|
// Clear any selection from previous file
|
|
this.selectedHighlightId = null;
|
|
// detectAndStoreMarkdownHighlights will call refreshSidebar if changes are detected
|
|
this.detectAndStoreMarkdownHighlights(content, file);
|
|
// Always refresh sidebar when file changes, even if no highlights detected
|
|
this.refreshSidebar();
|
|
}
|
|
|
|
debounceDetectMarkdownHighlights(editor: Editor, view: MarkdownView) {
|
|
if (this.detectHighlightsTimeout) {
|
|
clearTimeout(this.detectHighlightsTimeout);
|
|
}
|
|
this.detectHighlightsTimeout = setTimeout(() => {
|
|
this.detectMarkdownHighlights(editor, view);
|
|
}, 1000);
|
|
}
|
|
|
|
async detectMarkdownHighlights(editor: Editor, view: MarkdownView) {
|
|
const file = view.file;
|
|
if (!file) return;
|
|
const content = editor.getValue();
|
|
this.detectAndStoreMarkdownHighlights(content, file);
|
|
}
|
|
|
|
private async scanAllFilesForHighlights() {
|
|
const markdownFiles = this.app.vault.getMarkdownFiles();
|
|
const existingFilePaths = new Set(markdownFiles.map(file => file.path));
|
|
let hasChanges = false;
|
|
|
|
// First, clean up highlights for files that no longer exist
|
|
for (const filePath of this.highlights.keys()) {
|
|
if (!existingFilePaths.has(filePath)) {
|
|
console.log(`Cleaning up highlights for deleted file: ${filePath}`);
|
|
this.highlights.delete(filePath);
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
|
|
// Clean up orphaned highlight IDs from collections
|
|
for (const collection of this.collections.values()) {
|
|
const originalLength = collection.highlightIds.length;
|
|
collection.highlightIds = collection.highlightIds.filter(highlightId => {
|
|
// Check if this highlight still exists in any file
|
|
for (const [filePath, fileHighlights] of this.highlights) {
|
|
if (fileHighlights.some(h => h.id === highlightId)) {
|
|
return true; // Keep this highlight ID
|
|
}
|
|
}
|
|
console.log(`Removing orphaned highlight ${highlightId} from collection ${collection.name}`);
|
|
return false; // Remove this highlight ID
|
|
});
|
|
|
|
if (collection.highlightIds.length !== originalLength) {
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
|
|
// Now scan existing files for highlights
|
|
for (const file of markdownFiles) {
|
|
try {
|
|
const content = await this.app.vault.read(file);
|
|
const oldHighlights = this.highlights.get(file.path) || [];
|
|
this.detectAndStoreMarkdownHighlights(content, file, false); // Don't refresh sidebar for each file
|
|
const newHighlights = this.highlights.get(file.path) || [];
|
|
|
|
// Check if any highlights were found or changed (more thorough than just count)
|
|
const oldHighlightsJSON = JSON.stringify(oldHighlights.map(h => ({id: h.id, text: h.text, start: h.startOffset, end: h.endOffset, footnotes: h.footnoteCount})));
|
|
const newHighlightsJSON = JSON.stringify(newHighlights.map(h => ({id: h.id, text: h.text, start: h.startOffset, end: h.endOffset, footnotes: h.footnoteCount})));
|
|
|
|
if (oldHighlightsJSON !== newHighlightsJSON) {
|
|
hasChanges = true;
|
|
if (newHighlights.length > oldHighlights.length) {
|
|
console.log(`Found ${newHighlights.length - oldHighlights.length} new highlights in ${file.path}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to scan file ${file.path} for highlights:`, error);
|
|
}
|
|
}
|
|
|
|
// Save settings and refresh sidebar only once after scanning all files
|
|
if (hasChanges) {
|
|
console.log('Highlights changed during scan, saving settings and refreshing sidebar');
|
|
await this.saveSettings();
|
|
this.refreshSidebar();
|
|
}
|
|
}
|
|
|
|
detectAndStoreMarkdownHighlights(content: string, file: TFile, shouldRefresh: boolean = true) {
|
|
const markdownHighlightRegex = /==(.*?)==/g;
|
|
const newHighlights: Highlight[] = [];
|
|
let match;
|
|
const existingHighlightsForFile = this.highlights.get(file.path) || [];
|
|
const existingByTextAndOrder = new Map<string, {highlights: Highlight[], count: number}>();
|
|
|
|
existingHighlightsForFile.forEach(h => {
|
|
if (!existingByTextAndOrder.has(h.text)) {
|
|
existingByTextAndOrder.set(h.text, {highlights: [], count: 0});
|
|
}
|
|
existingByTextAndOrder.get(h.text)!.highlights.push(h);
|
|
});
|
|
|
|
// Extract all footnotes from the content
|
|
const footnoteMap = this.extractFootnotes(content);
|
|
|
|
while ((match = markdownHighlightRegex.exec(content)) !== null) {
|
|
const [, highlightText] = match;
|
|
const entry = existingByTextAndOrder.get(highlightText);
|
|
let existingHighlight: Highlight | undefined = undefined;
|
|
|
|
if (entry && entry.count < entry.highlights.length) {
|
|
existingHighlight = entry.highlights[entry.count];
|
|
entry.count++;
|
|
}
|
|
|
|
// Calculate line number from offset
|
|
const lineNumber = content.substring(0, match.index).split('\n').length - 1;
|
|
|
|
// Count footnotes immediately after the highlight (no spaces between footnotes)
|
|
const afterHighlight = content.substring(match.index + match[0].length);
|
|
const footnoteMatches = afterHighlight.match(/^(\[\^\w+\])+/);
|
|
|
|
// Extract footnote contents and filter out empty ones
|
|
let footnoteContents: string[] = [];
|
|
if (footnoteMatches) {
|
|
const footnoteRefs = footnoteMatches[0].match(/\[\^(\w+)\]/g) || [];
|
|
footnoteRefs.forEach(ref => {
|
|
const key = ref.slice(2, -1); // Remove [^ and ]
|
|
if (footnoteMap.has(key)) {
|
|
const fnContent = footnoteMap.get(key)!.trim();
|
|
if (fnContent) { // Only add non-empty content
|
|
footnoteContents.push(fnContent);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
const footnoteCount = footnoteContents.length; // Count only non-empty footnotes
|
|
|
|
if (existingHighlight) {
|
|
newHighlights.push({
|
|
...existingHighlight,
|
|
line: lineNumber,
|
|
startOffset: match.index,
|
|
endOffset: match.index + match[0].length,
|
|
filePath: file.path, // ensure filePath is current
|
|
footnoteCount: footnoteCount,
|
|
footnoteContents: footnoteContents
|
|
});
|
|
} else {
|
|
newHighlights.push({
|
|
id: this.generateId(),
|
|
text: highlightText,
|
|
tags: [],
|
|
line: lineNumber,
|
|
startOffset: match.index,
|
|
endOffset: match.index + match[0].length,
|
|
filePath: file.path,
|
|
footnoteCount: footnoteCount,
|
|
footnoteContents: footnoteContents,
|
|
createdAt: Date.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for actual changes before updating and refreshing
|
|
const oldHighlightsJSON = JSON.stringify(existingHighlightsForFile.map(h => ({id: h.id, start: h.startOffset, end: h.endOffset, text: h.text, footnotes: h.footnoteCount, contents: h.footnoteContents?.filter(c => c.trim() !== ''), color: h.color})));
|
|
const newHighlightsJSON = JSON.stringify(newHighlights.map(h => ({id: h.id, start: h.startOffset, end: h.endOffset, text: h.text, footnotes: h.footnoteCount, contents: h.footnoteContents?.filter(c => c.trim() !== ''), color: h.color})));
|
|
|
|
if (oldHighlightsJSON !== newHighlightsJSON) {
|
|
this.highlights.set(file.path, newHighlights);
|
|
if (shouldRefresh) {
|
|
this.saveSettings(); // Save to disk after detecting changes
|
|
this.refreshSidebar();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private extractFootnotes(content: string): Map<string, string> {
|
|
const footnoteMap = new Map<string, string>();
|
|
const footnoteRegex = /^\[\^(\w+)\]:\s*(.+)$/gm;
|
|
let match;
|
|
|
|
while ((match = footnoteRegex.exec(content)) !== null) {
|
|
const [, key, content] = match;
|
|
footnoteMap.set(key, content.trim());
|
|
}
|
|
|
|
return footnoteMap;
|
|
}
|
|
|
|
private async handleFileCreate(file: TFile) {
|
|
console.log(`New file created: ${file.path}`);
|
|
|
|
try {
|
|
// Read the content of the newly created file
|
|
const content = await this.app.vault.read(file);
|
|
|
|
// Scan for existing highlights in the new file
|
|
this.detectAndStoreMarkdownHighlights(content, file, false); // Don't refresh sidebar for each scan
|
|
|
|
// Get the highlights found
|
|
const highlights = this.highlights.get(file.path);
|
|
if (highlights && highlights.length > 0) {
|
|
console.log(`Found ${highlights.length} existing highlights in newly created file: ${file.path}`);
|
|
|
|
// Save settings and refresh sidebar since we found highlights
|
|
await this.saveSettings();
|
|
this.refreshSidebar();
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to scan newly created file ${file.path} for highlights:`, error);
|
|
}
|
|
}
|
|
|
|
async handleFileRename(file: TFile, oldPath: string) {
|
|
const oldHighlights = this.highlights.get(oldPath);
|
|
if (oldHighlights && oldHighlights.length > 0) {
|
|
// Update file paths in highlights
|
|
const updatedHighlights = oldHighlights.map(highlight => ({
|
|
...highlight,
|
|
filePath: file.path
|
|
}));
|
|
|
|
// Remove old path and add new path
|
|
this.highlights.delete(oldPath);
|
|
this.highlights.set(file.path, updatedHighlights);
|
|
|
|
// Save settings and refresh sidebar
|
|
await this.saveSettings();
|
|
this.refreshSidebar();
|
|
}
|
|
}
|
|
|
|
private handleFileDelete(file: TFile) {
|
|
console.log(`File deleted: ${file.path}`);
|
|
|
|
// Remove highlights for the deleted file
|
|
if (this.highlights.has(file.path)) {
|
|
console.log(`Removing ${this.highlights.get(file.path)?.length || 0} highlights for deleted file: ${file.path}`);
|
|
|
|
// Get the highlight IDs that will be removed
|
|
const deletedHighlightIds = new Set(
|
|
(this.highlights.get(file.path) || []).map(h => h.id)
|
|
);
|
|
|
|
// Remove the file's highlights
|
|
this.highlights.delete(file.path);
|
|
|
|
// Clean up collection references to these specific highlights
|
|
let collectionsModified = false;
|
|
for (const collection of this.collections.values()) {
|
|
const originalLength = collection.highlightIds.length;
|
|
collection.highlightIds = collection.highlightIds.filter(
|
|
highlightId => !deletedHighlightIds.has(highlightId)
|
|
);
|
|
|
|
if (collection.highlightIds.length !== originalLength) {
|
|
console.log(`Removed ${originalLength - collection.highlightIds.length} highlights from collection: ${collection.name}`);
|
|
collectionsModified = true;
|
|
}
|
|
}
|
|
|
|
this.saveSettings();
|
|
this.refreshSidebar();
|
|
|
|
if (collectionsModified) {
|
|
console.log('Collections were modified due to file deletion');
|
|
}
|
|
} else {
|
|
console.log(`No highlights found for deleted file: ${file.path}`);
|
|
}
|
|
}
|
|
|
|
getCurrentFileHighlights(): Highlight[] {
|
|
const file = this.app.workspace.getActiveFile();
|
|
return file ? this.highlights.get(file.path) || [] : [];
|
|
}
|
|
|
|
generateId(): string {
|
|
return Math.random().toString(36).substr(2, 9);
|
|
}
|
|
}
|
|
|
|
class HighlightSettingTab extends PluginSettingTab {
|
|
plugin: HighlightCommentsPlugin;
|
|
|
|
constructor(app: App, plugin: HighlightCommentsPlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.createEl('h2', { text: 'Highlights Sidebar' });
|
|
|
|
containerEl.createEl('p', {
|
|
text: 'This plugin has no options to configure.',
|
|
attr: { style: 'color: var(--text-muted); margin-top: 16px;' }
|
|
});
|
|
}
|
|
}
|
|
|
|
class CollectionsManager {
|
|
private plugin: HighlightCommentsPlugin;
|
|
|
|
constructor(plugin: HighlightCommentsPlugin) {
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
createCollection(name: string, description?: string): Collection {
|
|
const collection: Collection = {
|
|
id: this.plugin.generateId(),
|
|
name,
|
|
description,
|
|
highlightIds: [],
|
|
createdAt: Date.now()
|
|
};
|
|
|
|
this.plugin.collections.set(collection.id, collection);
|
|
this.plugin.saveSettings();
|
|
|
|
// Update dynamic commands after creating collection
|
|
this.plugin.registerCollectionCommands();
|
|
|
|
return collection;
|
|
}
|
|
|
|
deleteCollection(collectionId: string) {
|
|
// Store the collection name before deleting it
|
|
const collection = this.plugin.collections.get(collectionId);
|
|
if (collection) {
|
|
this.plugin.deletedCollectionNames.set(collectionId, collection.name);
|
|
}
|
|
|
|
this.plugin.collections.delete(collectionId);
|
|
this.plugin.saveSettings();
|
|
|
|
// Update dynamic commands to mark deleted collections
|
|
this.plugin.updateDeletedCollectionCommands();
|
|
}
|
|
|
|
async deleteCollectionWithConfirmation(collectionId: string): Promise<boolean> {
|
|
const collection = this.plugin.collections.get(collectionId);
|
|
if (!collection) return false;
|
|
|
|
const confirmed = confirm(`Are you sure you want to delete "${collection.name}"?`);
|
|
if (confirmed) {
|
|
this.deleteCollection(collectionId);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
addHighlightToCollection(collectionId: string, highlightId: string) {
|
|
const collection = this.plugin.collections.get(collectionId);
|
|
if (collection && !collection.highlightIds.includes(highlightId)) {
|
|
collection.highlightIds.push(highlightId);
|
|
this.plugin.saveSettings();
|
|
}
|
|
}
|
|
|
|
removeHighlightFromCollection(collectionId: string, highlightId: string) {
|
|
const collection = this.plugin.collections.get(collectionId);
|
|
if (collection) {
|
|
collection.highlightIds = collection.highlightIds.filter(id => id !== highlightId);
|
|
this.plugin.saveSettings();
|
|
}
|
|
}
|
|
|
|
getAllCollections(): Collection[] {
|
|
return Array.from(this.plugin.collections.values());
|
|
}
|
|
|
|
getCollection(collectionId: string): Collection | undefined {
|
|
return this.plugin.collections.get(collectionId);
|
|
}
|
|
|
|
getCollectionStats(collectionId: string): { highlightCount: number; fileCount: number } {
|
|
const collection = this.plugin.collections.get(collectionId);
|
|
if (!collection) return { highlightCount: 0, fileCount: 0 };
|
|
|
|
const uniqueFiles = new Set<string>();
|
|
let highlightCount = 0;
|
|
|
|
for (const highlightId of collection.highlightIds) {
|
|
// Find the highlight across all files
|
|
for (const [filePath, highlights] of this.plugin.highlights) {
|
|
const highlight = highlights.find(h => h.id === highlightId);
|
|
if (highlight) {
|
|
uniqueFiles.add(filePath);
|
|
highlightCount++;
|
|
break; // Found the highlight, no need to check other files
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
highlightCount,
|
|
fileCount: uniqueFiles.size
|
|
};
|
|
}
|
|
|
|
getHighlightsInCollection(collectionId: string): Highlight[] {
|
|
const collection = this.plugin.collections.get(collectionId);
|
|
if (!collection) return [];
|
|
|
|
const highlights: Highlight[] = [];
|
|
for (const highlightId of collection.highlightIds) {
|
|
// Find the highlight across all files
|
|
for (const [filePath, fileHighlights] of this.plugin.highlights) {
|
|
const highlight = fileHighlights.find(h => h.id === highlightId);
|
|
if (highlight) {
|
|
highlights.push(highlight);
|
|
break; // Found the highlight, no need to check other files
|
|
}
|
|
}
|
|
}
|
|
|
|
return highlights;
|
|
}
|
|
|
|
getHighlightCollectionCount(highlightId: string): number {
|
|
let collectionCount = 0;
|
|
for (const collection of this.plugin.collections.values()) {
|
|
if (collection.highlightIds.includes(highlightId)) {
|
|
collectionCount++;
|
|
}
|
|
}
|
|
return collectionCount;
|
|
}
|
|
}
|
|
|
|
export { CollectionsManager }; |