Use CodeMirror editor
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user