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>
108 lines
3.1 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|