74 lines
3.1 KiB
JavaScript
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 };
|
|
})();
|