feat: autocomplete intelligent partage (Forge + CodeMirror)
- autocomplete.js: detection de contexte (mermaid, code, frontmatter, wikilink, markdown) - Suggestions Mermaid: 15 types + syntaxe par type (flowchart, sequence, class, etc.) - Suggestions Code: snippets Python/JS/Bash/SQL + 15 langages - Suggestions Frontmatter: 9 champs YAML - Suggestions Markdown: titres, formatting, callouts, tableaux - Wikilinks: [[ cherche dans l'index via API /search/advanced - Integration Forge: Ctrl+Space, auto-trigger [[ et ```, Tab/Enter pour accepter - Dropdown flottant positionne au curseur, navigation fleches
This commit is contained in:
@@ -215,10 +215,32 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
/* Resize handle between panes */
|
||||
.resize-handle { width: 4px; cursor: col-resize; background: var(--border); flex-shrink: 0; transition: background 150ms; }
|
||||
.resize-handle:hover { background: var(--accent); }
|
||||
|
||||
/* Autocomplete dropdown */
|
||||
.ac-dropdown {
|
||||
position: absolute; z-index: 200;
|
||||
background: var(--bg2); border: 1px solid var(--border-md);
|
||||
border-radius: 8px; box-shadow: 0 8px 30px rgba(0,0,0,0.5);
|
||||
min-width: 220px; max-width: 380px; max-height: 260px;
|
||||
overflow-y: auto; display: none; padding: 4px;
|
||||
}
|
||||
.ac-dropdown.active { display: block; }
|
||||
.ac-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 5px 10px; border-radius: 4px; cursor: pointer;
|
||||
font-size: 12px; color: var(--text);
|
||||
}
|
||||
.ac-item:hover, .ac-item.active { background: var(--bg-hover); }
|
||||
.ac-item-label { font-family: var(--mono); font-weight: 500; white-space: nowrap; }
|
||||
.ac-item-detail { font-size: 10.5px; color: var(--text3); margin-left: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px; }
|
||||
.ac-item-context { font-size: 9px; color: var(--accent); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; padding: 2px 10px 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Autocomplete dropdown -->
|
||||
<div class="ac-dropdown" id="ac-dropdown"></div>
|
||||
|
||||
<!-- Bar -->
|
||||
<div class="bar">
|
||||
<div class="bar-left">
|
||||
@@ -1619,6 +1641,169 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// ---- Autocomplete (Ctrl+Space or smart trigger) ----
|
||||
var acDropdown = document.getElementById('ac-dropdown');
|
||||
var acItems = [];
|
||||
var acIdx = -1;
|
||||
|
||||
function showAutocomplete(items, context) {
|
||||
acItems = items;
|
||||
acIdx = -1;
|
||||
if (!items.length) { hideAutocomplete(); return; }
|
||||
// Position near cursor
|
||||
var pos = getCaretCoordinates(ta);
|
||||
var rect = ta.getBoundingClientRect();
|
||||
acDropdown.style.left = (rect.left + pos.left - ta.scrollLeft + 20) + 'px';
|
||||
acDropdown.style.top = (rect.top + pos.top - ta.scrollTop + 24) + 'px';
|
||||
|
||||
var html = context ? '<div class="ac-item-context">' + context + '</div>' : '';
|
||||
items.forEach(function(item, i) {
|
||||
html += '<div class="ac-item" data-idx="' + i + '">' +
|
||||
'<span class="ac-item-label">' + escHtml(item.label) + '</span>' +
|
||||
(item.detail ? '<span class="ac-item-detail">' + escHtml(item.detail) + '</span>' : '') +
|
||||
'</div>';
|
||||
});
|
||||
acDropdown.innerHTML = html;
|
||||
acDropdown.classList.add('active');
|
||||
acIdx = 0;
|
||||
highlightAcItem();
|
||||
// Wire clicks
|
||||
acDropdown.querySelectorAll('.ac-item').forEach(function(el) {
|
||||
el.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
var idx = parseInt(el.dataset.idx);
|
||||
if (!isNaN(idx) && acItems[idx]) applyAutocomplete(acItems[idx]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function hideAutocomplete() {
|
||||
acDropdown.classList.remove('active');
|
||||
acItems = [];
|
||||
acIdx = -1;
|
||||
}
|
||||
|
||||
function highlightAcItem() {
|
||||
acDropdown.querySelectorAll('.ac-item').forEach(function(el, i) {
|
||||
el.classList.toggle('active', i === acIdx);
|
||||
});
|
||||
if (acIdx >= 0) {
|
||||
var active = acDropdown.querySelector('.ac-item.active');
|
||||
if (active) active.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
function applyAutocomplete(item) {
|
||||
if (!item || !item.insert) return;
|
||||
var start = ta.selectionStart;
|
||||
var end = ta.selectionEnd;
|
||||
var before = ta.value.slice(0, start);
|
||||
var after = ta.value.slice(end);
|
||||
|
||||
// Smart insertion: if we matched a prefix, replace it
|
||||
var lineStart = before.lastIndexOf('\n') + 1;
|
||||
var lineBefore = before.slice(lineStart);
|
||||
var ctx = detectContext(ta.value, start);
|
||||
|
||||
if (ctx.type === 'wikilink') {
|
||||
// Replace [[partial with [[filename]]
|
||||
var bracketPos = before.lastIndexOf('[[');
|
||||
if (bracketPos >= 0) {
|
||||
before = before.slice(0, bracketPos) + '[[' + item.insert;
|
||||
ta.value = before + after;
|
||||
var newPos = before.length;
|
||||
ta.setSelectionRange(newPos, newPos);
|
||||
}
|
||||
} else if (ctx.type === 'frontmatter') {
|
||||
// Replace current line with the field
|
||||
before = before.slice(0, lineStart) + item.insert;
|
||||
ta.value = before + after;
|
||||
ta.setSelectionRange(before.length, before.length);
|
||||
} else {
|
||||
// Standard insert at cursor
|
||||
ta.value = before + item.insert + after;
|
||||
var newPos = start + item.insert.length;
|
||||
ta.setSelectionRange(newPos, newPos);
|
||||
}
|
||||
|
||||
hideAutocomplete();
|
||||
ta.focus();
|
||||
markDirty();
|
||||
autoHeight();
|
||||
updateLineNumbers();
|
||||
}
|
||||
|
||||
function triggerAutocomplete() {
|
||||
var result = suggest(ta.value, ta.selectionStart);
|
||||
if (result.async) {
|
||||
// Wikilink: fetch file list
|
||||
import('/static/js/autocomplete.js').then(function(m) {
|
||||
m.searchWikilinks(result.partial || '', function(items) {
|
||||
showAutocomplete(items, 'Wikilinks');
|
||||
});
|
||||
});
|
||||
} else if (result.items.length > 0) {
|
||||
var ctxLabels = {
|
||||
mermaid: 'Mermaid', code: 'Code', fence_open: 'Langage',
|
||||
frontmatter: 'Frontmatter', markdown: 'Markdown'
|
||||
};
|
||||
showAutocomplete(result.items, ctxLabels[result.context] || result.context);
|
||||
}
|
||||
}
|
||||
|
||||
function getCaretCoordinates(textarea) {
|
||||
var div = document.createElement('div');
|
||||
var style = div.style;
|
||||
var computed = window.getComputedStyle(textarea);
|
||||
for (var key of computed) {
|
||||
try { style[key] = computed[key]; } catch(e) {}
|
||||
}
|
||||
style.position = 'absolute';
|
||||
style.visibility = 'hidden';
|
||||
style.whiteSpace = 'pre-wrap';
|
||||
style.overflow = 'hidden';
|
||||
style.width = textarea.clientWidth + 'px';
|
||||
style.height = 'auto';
|
||||
var pos = textarea.selectionStart;
|
||||
div.textContent = textarea.value.slice(0, pos) + '.';
|
||||
document.body.appendChild(div);
|
||||
var rect = div.getBoundingClientRect();
|
||||
document.body.removeChild(div);
|
||||
return { left: rect.width, top: rect.height };
|
||||
}
|
||||
|
||||
ta.addEventListener('keydown', function(e) {
|
||||
if (acDropdown.classList.contains('active')) {
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); acIdx = Math.min(acIdx + 1, acItems.length - 1); highlightAcItem(); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); acIdx = Math.max(acIdx - 1, 0); highlightAcItem(); }
|
||||
else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
if (acIdx >= 0 && acItems[acIdx]) applyAutocomplete(acItems[acIdx]);
|
||||
}
|
||||
else if (e.key === 'Escape') { e.preventDefault(); hideAutocomplete(); }
|
||||
return;
|
||||
}
|
||||
// Ctrl+Space: force autocomplete
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === ' ') {
|
||||
e.preventDefault();
|
||||
triggerAutocomplete();
|
||||
}
|
||||
// Auto-trigger on [[ for wikilinks
|
||||
if (e.key === '[' && ta.selectionStart > 0 && ta.value[ta.selectionStart - 1] === '[') {
|
||||
setTimeout(triggerAutocomplete, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger on triple backtick
|
||||
ta.addEventListener('input', function() {
|
||||
var before = ta.value.slice(0, ta.selectionStart);
|
||||
if (before.endsWith('``')) {
|
||||
setTimeout(triggerAutocomplete, 50);
|
||||
}
|
||||
});
|
||||
|
||||
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
// ---- Init ----
|
||||
updateLineNumbers();
|
||||
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
/* 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 };
|
||||
Reference in New Issue
Block a user