CI / lint (push) Successful in 25s
CI / security (push) Successful in 19s
CI / test (push) Successful in 29s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 21s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
351 lines
15 KiB
JavaScript
351 lines
15 KiB
JavaScript
/* 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: '', 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 };
|