/** * codeFile — Live-Spiegel der Code-Dateien, die ARIA in einem Code-Projekt * schreibt, plus Ruckkanal fuer eigene Edits. * * Fluss: ARIAs Write/Edit (proxy) → Bridge → RVS `code_file` → hier gepuffert → * CodeEditorTile zeigt es in CodeMirror. Tippt Stefan im Editor, geht das als * `code_file_edit` ueber RVS zurueck an die Bridge (schreibt die Datei). * * Der Service haelt pro (projectId,path) den aktuellen Volltext, damit eine * spaet gemountete Editor-Kachel sofort den Ist-Stand bekommt (setContent) und * danach nur noch Deltas anwendet. Muster wie services/rvs.ts (Singleton). */ import rvs, { RVSMessage } from './rvs'; export interface CodePatch { from: number; to: number; insert: string; } /** Persistenter Zustand einer Datei im Spiegel. */ export interface CodeFileState { projectId: string; path: string; language: string; content: string; version: number; } /** Ein eingehendes Update — entweder Voll-Ersatz (content) oder Delta (patch). */ export interface CodeFileUpdate { projectId: string; path: string; language: string; version: number; content?: string; patch?: CodePatch; } type UpdateSub = (u: CodeFileUpdate) => void; const SEP = ''; const keyOf = (projectId: string, path: string) => `${projectId || ''}${SEP}${path}`; function applyPatch(content: string, p: CodePatch): string { const from = Math.max(0, Math.min(p.from, content.length)); const to = Math.max(from, Math.min(p.to, content.length)); return content.slice(0, from) + (p.insert || '') + content.slice(to); } function guessLanguage(path: string): string { const ext = (path.split('.').pop() || '').toLowerCase(); const map: Record = { js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python', rb: 'ruby', go: 'go', rs: 'rust', c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp', java: 'java', kt: 'kotlin', cs: 'csharp', php: 'php', swift: 'swift', sh: 'shell', bash: 'shell', zsh: 'shell', html: 'html', htm: 'html', xml: 'xml', css: 'css', scss: 'css', json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml', md: 'markdown', sql: 'sql', bat: 'shell', asm: 'asm', s: 'asm', }; return map[ext] || 'text'; } class CodeFileService { private files = new Map(); private subs: UpdateSub[] = []; constructor() { rvs.onMessage((m) => this.onMessage(m)); } private onMessage(m: RVSMessage): void { if (m.type !== 'code_file') return; const p = (m.payload || {}) as any; const projectId: string = p.projectId || ''; const path: string = p.path || ''; if (!path) return; const language: string = p.language || guessLanguage(path); const version: number = typeof p.version === 'number' ? p.version : 0; const k = keyOf(projectId, path); const prev = this.files.get(k); let content: string; let patch: CodePatch | undefined; if (typeof p.content === 'string') { content = p.content; } else if (p.patch && typeof p.patch.from === 'number') { patch = { from: p.patch.from, to: p.patch.to, insert: p.patch.insert || '' }; content = applyPatch(prev?.content || '', patch); } else { content = prev?.content || ''; } this.files.set(k, { projectId, path, language, content, version }); const update: CodeFileUpdate = { projectId, path, language, version, content: p.content, patch }; this.subs.forEach((cb) => cb(update)); } /** Alle bekannten Dateien eines Projekts (fuer die Datei-Liste in der Kachel). */ getFiles(projectId: string): CodeFileState[] { const out: CodeFileState[] = []; for (const f of this.files.values()) { if ((f.projectId || '') === (projectId || '')) out.push(f); } return out.sort((a, b) => a.path.localeCompare(b.path)); } getFile(projectId: string, path: string): CodeFileState | undefined { return this.files.get(keyOf(projectId, path)); } /** Registriert einen Listener fuer eingehende Updates. */ subscribe(cb: UpdateSub): () => void { this.subs.push(cb); return () => { this.subs = this.subs.filter((s) => s !== cb); }; } /** Nutzer-Edit aus dem Editor zurueck an ARIA/Bridge. */ sendEdit(projectId: string, path: string, patch: CodePatch, fullText: string, version: number): void { // Optimistisch den lokalen Spiegel nachziehen, damit ein direkt folgendes // ARIA-Update nicht auf veraltetem Text patcht. const k = keyOf(projectId, path); const prev = this.files.get(k); if (prev) this.files.set(k, { ...prev, content: fullText, version }); rvs.send('code_file_edit', { projectId, path, from: patch.from, to: patch.to, insert: patch.insert, fullText, version }); } } const codeFile = new CodeFileService(); export default codeFile;