/* ObsiGate — Autocomplete intelligent * Partagé entre éditeur Forge (textarea) et CodeMirror. * Détecte le contexte autour du curseur et suggère des complétions. * * Contexte supporté : mermaid, code fence, frontmatter, markdown, wikilink, table, texte libre. * * API : * suggest(text, cursorPos) → { context, items: [{label, detail, insert, from, to}] } * CompletionSource pour CodeMirror : codeMirrorCompletionSource(context) */ // ── Suggestion Data ────────────────────────────────────────────────── var MERMAID_TYPES = [ { label: 'flowchart TD', detail: 'Diagramme de flux (top-down)', insert: 'flowchart TD\n ' }, { label: 'flowchart LR', detail: 'Diagramme de flux (gauche-droite)', insert: 'flowchart LR\n ' }, { label: 'sequenceDiagram', detail: 'Sequence (acteurs + messages)', insert: 'sequenceDiagram\n ' }, { label: 'classDiagram', detail: 'Diagramme de classes UML', insert: 'classDiagram\n ' }, { label: 'stateDiagram-v2', detail: 'Diagramme d\'etats', insert: 'stateDiagram-v2\n ' }, { label: 'erDiagram', detail: 'Diagramme Entite-Relation', insert: 'erDiagram\n ' }, { label: 'journey', detail: 'Parcours utilisateur', insert: 'journey\n ' }, { label: 'gantt', detail: 'Diagramme de Gantt', insert: 'gantt\n ' }, { label: 'pie', detail: 'Diagramme circulaire', insert: 'pie\n ' }, { label: 'gitGraph', detail: 'Historique Git', insert: 'gitGraph\n ' }, { label: 'mindmap', detail: 'Carte mentale', insert: 'mindmap\n ' }, { label: 'timeline', detail: 'Ligne du temps', insert: 'timeline\n ' }, { label: 'quadrantChart', detail: 'Graphique quadrants', insert: 'quadrantChart\n ' }, { label: 'xychart-beta', detail: 'Graphique XY', insert: 'xychart-beta\n ' }, { label: 'block-beta', detail: 'Diagramme de blocs', insert: 'block-beta\n ' } ]; var MERMAID_SYNTAX = { 'flowchart': [ { label: 'A-->B', detail: 'Fleche', insert: '-->' }, { label: 'A--text-->B', detail: 'Fleche avec texte', insert: '-- texte -->' }, { label: 'A-.->B', detail: 'Fleche pointillee', insert: '-.->' }, { label: 'A==>B', detail: 'Fleche epaisse', insert: '==>' }, { label: 'subgraph', detail: 'Sous-graphe', insert: 'subgraph titre\n \nend' }, { label: 'style A fill:#xxx', detail: 'Style de noeud', insert: 'style A fill:#333,stroke:#58a6ff' }, { label: 'classDef', detail: 'Classe CSS', insert: 'classDef default fill:#333,stroke:#58a6ff' } ], 'sequenceDiagram': [ { label: 'participant X', detail: 'Declarer acteur', insert: 'participant Nom' }, { label: 'A->>B: msg', detail: 'Message', insert: '->> : ' }, { label: 'A-->>B: msg', detail: 'Message asynchrone', insert: '-->> : ' }, { label: 'Note over A,B', detail: 'Note', insert: 'Note over A,B: ' }, { label: 'loop text', detail: 'Boucle', insert: 'loop description\n \nend' }, { label: 'alt/else', detail: 'Alternative', insert: 'alt condition\n \nelse\n \nend' }, { label: 'activate A', detail: 'Activer ligne de vie', insert: 'activate A' } ], 'classDiagram': [ { label: 'class Nom {', detail: 'Declarer classe', insert: 'class Nom {\n \n}' }, { label: '+methode()', detail: 'Methode publique', insert: '+nom() Type' }, { label: '-attribut', detail: 'Attribut prive', insert: '-nom Type' }, { label: 'Heritage', detail: 'Relation heritage', insert: ' <|-- ' }, { label: 'Composition', detail: 'Relation composition', insert: ' *-- ' } ], 'gantt': [ { label: 'dateFormat', detail: 'Format de date', insert: 'dateFormat YYYY-MM-DD' }, { label: 'section', detail: 'Section Gantt', insert: 'section Nom' }, { label: 'tache : apres', detail: 'Tache avec dependance', insert: ' : after tache1, 3d' } ], 'pie': [ { label: 'title', detail: 'Titre du pie', insert: 'title Titre du graphique' }, { label: '"Label" : val', detail: 'Valeur', insert: '"Label" : 50' } ], 'mindmap': [ { label: 'root', detail: 'Racine', insert: 'root((Sujet))' }, { label: 'branche', detail: 'Sous-sujet', insert: '\n Sous-sujet' }, { label: 'leaf', detail: 'Feuille', insert: '\n Detail' } ] }; var CODE_LANGUAGES = [ { label: 'python', detail: 'Python', insert: 'python' }, { label: 'javascript', detail: 'JavaScript', insert: 'javascript' }, { label: 'typescript', detail: 'TypeScript', insert: 'typescript' }, { label: 'bash', detail: 'Bash / Shell', insert: 'bash' }, { label: 'sql', detail: 'SQL', insert: 'sql' }, { label: 'json', detail: 'JSON', insert: 'json' }, { label: 'yaml', detail: 'YAML', insert: 'yaml' }, { label: 'html', detail: 'HTML', insert: 'html' }, { label: 'css', detail: 'CSS', insert: 'css' }, { label: 'dockerfile', detail: 'Dockerfile', insert: 'dockerfile' }, { label: 'mermaid', detail: 'Mermaid (diagrammes)', insert: 'mermaid' }, { label: 'markdown', detail: 'Markdown', insert: 'markdown' }, { label: 'rust', detail: 'Rust', insert: 'rust' }, { label: 'go', detail: 'Go', insert: 'go' }, { label: 'java', detail: 'Java', insert: 'java' } ]; var CODE_SNIPPETS = { python: [ { label: 'def', detail: 'Fonction', insert: 'def nom(params):\n ' }, { label: 'class', detail: 'Classe', insert: 'class Nom:\n def __init__(self):\n ' }, { label: 'if/else', detail: 'Condition', insert: 'if condition:\n \nelse:\n ' }, { label: 'for', detail: 'Boucle for', insert: 'for item in iterable:\n ' }, { label: 'try/except', detail: 'Try-except', insert: 'try:\n \nexcept Exception as e:\n ' }, { label: 'import', detail: 'Import', insert: 'import ' }, { label: 'print', detail: 'Print', insert: 'print()' }, { label: 'lambda', detail: 'Lambda', insert: 'lambda x: ' } ], javascript: [ { label: 'function', detail: 'Fonction', insert: 'function nom(params) {\n \n}' }, { label: 'const', detail: 'Constante', insert: 'const nom = ' }, { label: '=>', detail: 'Fleche', insert: '(params) => {\n \n}' }, { label: 'if/else', detail: 'Condition', insert: 'if (cond) {\n \n} else {\n \n}' }, { label: 'for', detail: 'Boucle for', insert: 'for (let i = 0; i < n; i++) {\n \n}' }, { label: 'try/catch', detail: 'Try-catch', insert: 'try {\n \n} catch (e) {\n \n}' }, { label: 'console.log', detail: 'Console log', insert: 'console.log()' }, { label: 'async/await', detail: 'Async function', insert: 'async function nom() {\n await \n}' } ], bash: [ { label: '#!/bin/bash', detail: 'Shebang', insert: '#!/bin/bash' }, { label: 'if/fi', detail: 'Condition', insert: 'if [ condition ]; then\n \nfi' }, { label: 'for', detail: 'Boucle for', insert: 'for item in list; do\n \ndone' }, { label: 'case/esac', detail: 'Case', insert: 'case var in\n pattern)\n ;;\nesac' }, { label: 'function', detail: 'Fonction', insert: 'nom() {\n \n}' } ], sql: [ { label: 'SELECT', detail: 'Select', insert: 'SELECT colonnes\nFROM table' }, { label: 'JOIN', detail: 'Join', insert: 'JOIN table2 ON table1.id = table2.id' }, { label: 'WHERE', detail: 'Where', insert: 'WHERE condition' }, { label: 'GROUP BY', detail: 'Group by', insert: 'GROUP BY colonne' }, { label: 'ORDER BY', detail: 'Order by', insert: 'ORDER BY colonne DESC' }, { label: 'INSERT INTO', detail: 'Insert', insert: 'INSERT INTO table (cols) VALUES (vals)' }, { label: 'CREATE TABLE', detail: 'Create table', insert: 'CREATE TABLE nom (\n id INTEGER PRIMARY KEY,\n \n)' } ] }; var FRONTMATTER_FIELDS = [ { label: 'title:', detail: 'Titre du document', insert: 'title: ' }, { label: 'tags:', detail: 'Tags (liste)', insert: 'tags:\n - ' }, { label: 'date:', detail: 'Date', insert: 'date: ' + new Date().toISOString().slice(0,10) }, { label: 'status:', detail: 'Statut', insert: 'status: en cours' }, { label: 'category:', detail: 'Categorie', insert: 'category: ' }, { label: 'publish:', detail: 'Publier (true/false)', insert: 'publish: true' }, { label: 'favoris:', detail: 'Favoris (true/false)', insert: 'favoris: true' }, { label: 'author:', detail: 'Auteur', insert: 'author: ' }, { label: 'description:', detail: 'Description', insert: 'description: ' } ]; var MARKDOWN_FORMAT = [ { label: '# H1', detail: 'Titre niveau 1', insert: '# ' }, { label: '## H2', detail: 'Titre niveau 2', insert: '## ' }, { label: '### H3', detail: 'Titre niveau 3', insert: '### ' }, { label: '**gras**', detail: 'Texte en gras', insert: '****' }, { label: '*italique*', detail: 'Texte en italique', insert: '**' }, { label: '`code`', detail: 'Code inline', insert: '``' }, { label: '[lien](url)', detail: 'Lien', insert: '[]()' }, { label: '![image](url)', detail: 'Image', insert: '![]()' }, { label: '- liste', detail: 'Liste a puces', insert: '- ' }, { label: '1. liste', detail: 'Liste numerotee', insert: '1. ' }, { label: '> citation', detail: 'Citation', insert: '> ' }, { label: '---', detail: 'Separateur horizontal', insert: '\n---\n' }, { label: '> [!note]', detail: 'Callout Note', insert: '> [!note] Titre\n> Contenu' }, { label: '> [!warning]', detail: 'Callout Warning', insert: '> [!warning] Titre\n> Contenu' }, { label: '> [!tip]', detail: 'Callout Tip', insert: '> [!tip] Titre\n> Contenu' }, { label: '> [!danger]', detail: 'Callout Danger', insert: '> [!danger] Titre\n> Contenu' } ]; // ── Context Detection ───────────────────────────────────────────────── function detectContext(text, cursorPos) { var before = text.slice(0, cursorPos); var after = text.slice(cursorPos); var lines = before.split('\n'); var currentLine = lines[lines.length - 1] || ''; var lineNum = lines.length; // Check for code fence context (look backwards for unclosed ```) var inFence = null; var fenceLang = ''; var fenceStart = 0; for (var i = lines.length - 1; i >= 0; i--) { var l = lines[i].trim(); var m = l.match(/^```(\w*)/); if (m) { if (inFence === null) { inFence = true; fenceLang = m[1] || ''; fenceStart = i + 1; } else { inFence = false; break; } } } if (inFence) { if (fenceLang === 'mermaid') return { type: 'mermaid', lang: '', line: currentLine }; return { type: 'code', lang: fenceLang, line: currentLine }; } // Check for frontmatter context (between first and second ---) if (lineNum >= 2) { var firstSep = -1, secondSep = -1; for (var j = 0; j < lines.length; j++) { if (lines[j].trim() === '---') { if (firstSep === -1) firstSep = j; else if (secondSep === -1) { secondSep = j; break; } } } if (firstSep === 0 && secondSep === -1) { return { type: 'frontmatter', line: currentLine }; } } // Check for wikilink if (/\[\[$/.test(currentLine) || /\[\[[^\]]*$/.test(currentLine)) { var partial = (currentLine.match(/\[\[([^\]]*)$/) || ['',''])[1]; return { type: 'wikilink', partial: partial, line: currentLine }; } // Check for markdown link if (/\[.*\]\($/.test(currentLine)) { return { type: 'link', line: currentLine }; } // Check for triple backtick (about to open fence) if (/```$/.test(before) || /^```\s*$/.test(currentLine)) { return { type: 'fence_open', line: currentLine }; } // Check if inside a markdown table if (/^\|.*\|$/.test(currentLine.trim())) { return { type: 'table', line: currentLine }; } // Plain text / markdown return { type: 'markdown', line: currentLine }; } // ── Suggestion Engine ───────────────────────────────────────────────── function suggest(text, cursorPos) { var ctx = detectContext(text, cursorPos); var items = []; var prefix = ''; var currentLine = ctx.line; switch (ctx.type) { case 'mermaid': // Try to match mermaid type already declared var mermaidMatch = text.match(/```mermaid\s*\n(\w+)/); if (mermaidMatch) { var mmType = mermaidMatch[1]; items = MERMAID_SYNTAX[mmType] || []; } if (items.length === 0) { items = MERMAID_TYPES; } break; case 'code': items = CODE_SNIPPETS[ctx.lang] || []; break; case 'fence_open': items = CODE_LANGUAGES; break; case 'frontmatter': prefix = currentLine.trim(); items = FRONTMATTER_FIELDS.filter(function(f) { return f.label.startsWith(prefix) || prefix === ''; }).map(function(f) { return { label: f.label, detail: f.detail, insert: prefix ? f.insert : f.insert }; }); return { context: ctx.type, items: items.length > 0 ? items : FRONTMATTER_FIELDS.map(function(f) { return { label: f.label, detail: f.detail, insert: f.insert }; }) }; case 'wikilink': return { context: 'wikilink', items: [], partial: ctx.partial, async: true // requires file index lookup }; case 'markdown': default: // At start of line: markdown formatting if (currentLine.trim().length < 3) { items = MARKDOWN_FORMAT; } else { items = []; } break; } var filtered = items.filter(function(item) { return !prefix || item.label.toLowerCase().indexOf(prefix.toLowerCase()) !== -1; }); return { context: ctx.type, items: filtered.length > 0 ? filtered : items }; } // ── Wikilink async completion (fetches file list from server) ───────── var _fileCache = null; var _fileCacheTime = 0; function searchWikilinks(partial, callback) { var now = Date.now(); // Use cached file list if fresh (< 30s) if (_fileCache && (now - _fileCacheTime) < 30000) { callback(filterFiles(partial)); return; } try { fetch('/api/search/advanced?q=**&vault=all&limit=500') .then(function(r) { return r.json(); }) .then(function(data) { _fileCache = (data.results || []).map(function(r) { return { path: r.path, title: r.title || r.path.replace(/\.md$/i, ''), vault: r.vault, name: r.path.split('/').pop().replace(/\.md$/i, '') }; }); _fileCacheTime = now; callback(filterFiles(partial)); }) .catch(function() { callback([]); }); } catch(e) { callback([]); } } function filterFiles(partial) { if (!_fileCache) return []; var q = (partial || '').toLowerCase(); return _fileCache.filter(function(f) { return f.name.toLowerCase().indexOf(q) !== -1 || f.path.toLowerCase().indexOf(q) !== -1; }).slice(0, 20).map(function(f) { return { label: f.name, detail: f.path, insert: f.path + ']]', vault: f.vault }; }); } export { suggest, searchWikilinks, detectContext, MERMAID_TYPES, CODE_LANGUAGES, FRONTMATTER_FIELDS, MARKDOWN_FORMAT };