Files
2026-09-17 17:46:36 +02:00

74 lines
3.1 KiB
JavaScript

// wikicomplete.js — the `[[` autocomplete source for CodeMirror.
//
// Wiki links are search queries, so the popup is the same typeahead the header
// search box uses: the text typed after `[[` goes to `/_search` and each match
// is offered by name. Picking one replaces the query segment with that page's
// name — never a path — so what lands in the document is still a query. The
// trailing `]]` and any `::display` tail are left untouched.
//
// Exposes window.WikiComplete.source for main.js to register via CM's
// autocompletion().
window.WikiComplete = (function () {
// Matches search-suggest.js so both surfaces fire at the same point.
var MIN_QUERY_LEN = 2;
function fetchSuggestions(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();
});
}
// queryEnd returns where the current query segment ends: the first `]`, the
// `::` that starts the display text, or end of line. Replacing only up to
// there keeps a re-edit from eating an existing alias or the closing `]]`.
function queryEnd(context, pos) {
var line = context.state.doc.lineAt(pos);
var to = pos;
while (to < line.to) {
var ch = context.state.sliceDoc(to, to + 1);
if (ch === ']') break;
if (ch === ':' && context.state.sliceDoc(to, to + 2) === '::') break;
to++;
}
return to;
}
// CM completion source. Activates on `[[` followed by at least
// MIN_QUERY_LEN characters; a cursor past a `::` is in the display tail and
// gets nothing.
function source(context) {
var match = context.matchBefore(/\[\[[^\]\n]*/);
if (!match) return null;
var content = context.state.sliceDoc(match.from + 2, context.pos);
if (content.indexOf('::') !== -1) return null;
var query = content.trim();
if (query.length < MIN_QUERY_LEN) return null;
var from = match.from + 2;
var to = queryEnd(context, context.pos);
return new Promise(function (resolve) {
if (context.aborted) { resolve(null); return; }
fetchSuggestions(query).then(function (resp) {
if (context.aborted) { resolve(null); return; }
var options = (resp.results || []).map(function (r) {
return { label: r.name, detail: '/' + r.path, apply: r.name };
});
// filter:false — the server already ranked these fuzzily, and
// CM's own prefix filter would drop the near-misses (typos,
// token reorderings) that make the fuzzy search worth having.
// No validFor either: every keystroke re-queries from scratch.
resolve({ from: from, to: to, options: options, filter: false });
}).catch(function () {
resolve(null);
});
});
}
return { source: source };
})();