obsidian-logfire/src/collectors/editor-collector.ts
tolvitty e0d9f301d6 Alle Event-Collectors implementiert
FileCollector: create, delete, rename, move, modify Events.
ContentCollector: Semantische Analyse bei file:modify
(Wörter, Links, Tags, Headings, Embeds, Frontmatter).
NavCollector: file-open/close mit Dauer, active-leaf-change.
EditorCollector: CM6 ViewPlugin mit Debouncing.
SystemCollector: Command-Patching für Kommando-Tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 10:50:41 +01:00

108 lines
3.1 KiB
TypeScript

import { ViewPlugin, ViewUpdate } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { categoryForType, LogfireSettings } from '../types';
import { EventBus } from '../core/event-bus';
import { SessionManager } from '../core/session-manager';
interface PendingChange {
inserted: number;
deleted: number;
from: number;
to: number;
}
export class EditorCollector {
constructor(
private eventBus: EventBus,
private sessionManager: SessionManager,
private settings: LogfireSettings,
private shouldTrack: (path: string) => boolean,
private getActiveFilePath: () => string | null,
) {}
createExtension(): Extension {
const collector = this;
return ViewPlugin.fromClass(
class {
private pendingChanges: PendingChange[] = [];
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
private batchStartTime = 0;
update(update: ViewUpdate): void {
if (!update.docChanged) return;
if (!collector.settings.tracking.editorChanges) return;
const filePath = collector.getActiveFilePath();
if (!filePath || !collector.shouldTrack(filePath)) return;
if (this.pendingChanges.length === 0) {
this.batchStartTime = Date.now();
}
update.changes.iterChanges((fromA, toA, _fromB, toB, inserted) => {
this.pendingChanges.push({
inserted: inserted.length,
deleted: toA - fromA,
from: fromA,
to: toB,
});
});
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(
() => this.flush(filePath),
collector.settings.advanced.debounceMs,
);
}
private flush(filePath: string): void {
if (this.pendingChanges.length === 0) return;
const changes = this.pendingChanges.splice(0);
const duration = Date.now() - this.batchStartTime;
let insertedChars = 0;
let deletedChars = 0;
let rangeStart = Infinity;
let rangeEnd = 0;
for (const c of changes) {
insertedChars += c.inserted;
deletedChars += c.deleted;
if (c.from < rangeStart) rangeStart = c.from;
if (c.to > rangeEnd) rangeEnd = c.to;
}
collector.emitChange(filePath, {
insertedChars,
deletedChars,
rangeStart: rangeStart === Infinity ? 0 : rangeStart,
rangeEnd,
duration,
});
}
destroy(): void {
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
}
}
},
);
}
private emitChange(source: string, payload: Record<string, unknown>): void {
this.eventBus.emit({
id: crypto.randomUUID(),
timestamp: Date.now(),
type: 'editor:change',
category: categoryForType('editor:change'),
source,
payload,
session: this.sessionManager.currentSessionId,
});
}
}