// wikicomplete.js — the `[[` wikilink autocomplete source for CodeMirror. // // Triggers when the cursor sits in a `[[…` token (freshly typed or re-edited // inside an existing `[[…]]`), queries the existing /_search JSON endpoint // (debounced), and offers matching page paths. Selecting a result inserts the // absolute path and auto-closes `]]`, leaving the cursor before the close so a // `::display` alias can be typed (decision 7). Exposes window.WikiComplete.source // for main.js to register via CM's autocompletion(). window.WikiComplete = (function () { var DEBOUNCE_MS = 100; var MIN_QUERY_LEN = 2; // Mirrors the Go validator (wikilinks.go isValidWikiTarget) and the old // client check: absolute path, no empty/'.'/'..' segments. Server results // are wiki paths so this is mostly a guard against odd index entries. function isValidWikiTarget(p) { if (!p || p[0] !== '/') return false; var trimmed = p.replace(/^\/+|\/+$/g, ''); if (trimmed === '') return true; var segs = trimmed.split('/'); for (var i = 0; i < segs.length; i++) { if (segs[i] === '' || segs[i] === '.' || segs[i] === '..') return false; } return true; } // Build the apply() for a chosen result. Inserts the path over the typed // query and ensures a single trailing `]]`, cursor parked before it. If the // user (or closeBrackets) already produced `]]`, we don't double it up. function makeApply(path) { return function (view, completion, from, to) { var closed = view.state.sliceDoc(to, to + 2) === ']]'; var insert = closed ? path : path + ']]'; view.dispatch({ changes: { from: from, to: to, insert: insert }, selection: { anchor: from + path.length }, scrollIntoView: true, }); }; } function fetchSuggest(query) { return fetch('/_search?q=' + encodeURIComponent(query), { credentials: 'same-origin', headers: { 'Accept': 'application/json' }, }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }); } // CM completion source. `[[` plus any run of non-`]`, non-newline chars // before the cursor is the trigger; the query is the text after `[[`. The // replaced range extends past the cursor to the end of the inner token (up // to `]]`/EOL) so re-editing inside an existing `[[…]]` replaces the whole // target instead of duplicating the trailing text. function source(context) { var match = context.matchBefore(/\[\[[^\]\n]*/); if (!match) return null; var query = context.state.sliceDoc(match.from + 2, context.pos); if (query.length < MIN_QUERY_LEN && !context.explicit) return null; var line = context.state.doc.lineAt(context.pos); var to = context.pos; while (to < line.to && context.state.sliceDoc(to, to + 1) !== ']') to++; return new Promise(function (resolve) { var timer = setTimeout(function () { if (context.aborted) { resolve(null); return; } fetchSuggest(query).then(function (resp) { if (context.aborted) { resolve(null); return; } var options = (resp.results || []).reduce(function (acc, r) { var path = '/' + r.path; if (isValidWikiTarget(path)) { acc.push({ label: path, detail: r.name, apply: makeApply(path) }); } return acc; }, []); resolve({ from: match.from + 2, to: to, options: options }); }).catch(function () { resolve(null); }); }, DEBOUNCE_MS); // CM doesn't cancel pending promises, but it sets context.aborted; // clear the timer too if the doc moved on before it fired. if (context.addEventListener) { context.addEventListener('abort', function () { clearTimeout(timer); }); } }); } return { source: source, isValidWikiTarget: isValidWikiTarget }; })();