SequenceDiagram/scripts/sequence/codemirror/hints.mjs

236 lines
5.3 KiB
JavaScript

import {last, mergeSets} from '../../core/ArrayUtilities.mjs';
const TRIMMER = /^([ \t]*)(.*)$/;
const SQUASH = {
after: '.!+', // Cannot squash after * or - in all cases
end: /[ \t\r\n]$/,
start: /^[ \t\r\n:,]/,
};
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
const QUOTE_ESCAPE = /["\\]/g;
function completionsEqual(a, b) {
return (
(a.v === b.v) &&
(a.prefix === b.prefix) &&
(a.suffix === b.suffix) &&
(a.q === b.q)
);
}
function makeRangeFrom(cm, line, chFrom) {
const ln = cm.getLine(line);
const ranges = {
squash: {ch: chFrom, line},
word: {ch: chFrom, line},
};
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
if(SQUASH.after.includes(ln[chFrom - 2])) {
ranges.word.ch --;
}
ranges.squash.ch --;
}
return ranges;
}
function makeRangeTo(cm, line, chTo) {
const ln = cm.getLine(line);
const ranges = {
squash: {ch: chTo, line},
word: {ch: chTo, line},
};
if(ln[chTo] === ' ') {
ranges.squash.ch ++;
}
return ranges;
}
function wrapQuote({v, q, prefix = '', suffix = ''}, quote) {
const quo = (quote || !REQUIRED_QUOTED.test(v)) ? quote : '"';
return (
prefix +
((quo && q) ? (quo + v.replace(QUOTE_ESCAPE, '\\$&') + quo) : v) +
suffix
);
}
function makeHintItem(entry, ranges, quote) {
const quoted = wrapQuote(entry, quote);
const from = entry.q ? ranges.fromVar : ranges.fromKey;
if(quoted === '\n') {
return {
className: 'pick-virtual',
displayFrom: null,
displayText: '<END>',
from: from.squash,
text: '\n',
to: ranges.to.squash,
};
} else {
return {
className: null,
displayFrom: from.word,
displayText: quoted.trim(),
from: SQUASH.start.test(quoted) ? from.squash : from.word,
text: quoted,
to: SQUASH.end.test(quoted) ? ranges.to.squash : ranges.to.word,
};
}
}
function getGlobals({global, prefix = '', suffix = ''}, globals) {
const identified = globals[global];
if(!identified) {
return [];
}
return identified.map((item) => ({prefix, q: true, suffix, v: item}));
}
function populateGlobals(completions, globals = {}) {
for(let i = 0; i < completions.length;) {
if(completions[i].global) {
const identified = getGlobals(completions[i], globals);
mergeSets(completions, identified, completionsEqual);
completions.splice(i, 1);
} else {
++ i;
}
}
}
function getTokensUpTo(cm, pos) {
const tokens = cm.getLineTokens(pos.line);
for(let p = 0; p < tokens.length; ++ p) {
if(tokens[p].end >= pos.ch) {
tokens.length = p + 1;
break;
}
}
return tokens;
}
function getVariablePartial(tokens, pos) {
let partial = '';
let start = 0;
let fin = 0;
tokens.forEach((token) => {
if(token.state.isVar) {
partial += token.string;
fin = token.end;
} else {
partial = '';
start = token.end;
}
});
if(fin > pos.ch) {
partial = partial.substr(0, pos.ch - start);
}
const parts = TRIMMER.exec(partial);
partial = parts[2];
let quote = '';
if(ONGOING_QUOTE.test(partial)) {
quote = partial.charAt(0);
partial = partial.substr(1);
}
return {
from: start + parts[1].length,
partial,
quote,
valid: fin >= start,
};
}
function getKeywordPartial(token, pos) {
let partial = token.string;
if(token.end > pos.ch) {
partial = partial.substr(0, pos.ch - token.start);
}
const parts = TRIMMER.exec(partial);
return {
from: token.start + parts[1].length,
partial: parts[2],
valid: true,
};
}
function suggestDropdownLocation(list, fromKey) {
let p = null;
list.forEach(({displayFrom}) => {
if(displayFrom) {
if(
!p ||
displayFrom.line > p.line ||
(displayFrom.line === p.line && displayFrom.ch > p.ch)
) {
p = displayFrom;
}
}
});
return p || fromKey.word;
}
function partialMatch(v, p) {
return p.valid && v.startsWith(p.partial);
}
function getCompletions(cur, token, globals) {
let completions = null;
if(cur.ch > 0 && token.state.line.length > 0) {
completions = token.state.completions.slice();
} else {
completions = token.state.beginCompletions
.concat(token.state.knownAgent);
}
populateGlobals(completions, globals);
return completions;
}
export function getHints(cm, options) {
const cur = cm.getCursor();
const tokens = getTokensUpTo(cm, cur);
const token = last(tokens) || cm.getTokenAt(cur);
const pVar = getVariablePartial(tokens, cur);
const pKey = getKeywordPartial(token, cur);
const completions = getCompletions(cur, token, cm.options.globals);
const ranges = {
fromKey: makeRangeFrom(cm, cur.line, pKey.from),
fromVar: makeRangeFrom(cm, cur.line, pVar.from),
to: makeRangeTo(cm, cur.line, token.end),
};
let selfValid = null;
const list = (completions
.filter((o) => (
(o.q || !pVar.quote) &&
partialMatch(o.v, o.q ? pVar : pKey)
))
.map((o) => {
if(!options.completeSingle) {
if(o.v === (o.q ? pVar : pKey).partial) {
selfValid = o;
return null;
}
}
return makeHintItem(o, ranges, pVar.quote);
})
.filter((opt) => (opt !== null))
);
if(selfValid && list.length > 0) {
list.unshift(makeHintItem(selfValid, ranges, pVar.quote));
}
const data = {
from: suggestDropdownLocation(list, ranges.fromKey),
list,
to: ranges.to.word,
};
// Workaround for https://github.com/codemirror/CodeMirror/issues/3092
const CM = cm.constructor;
CM.on(data, 'shown', CM.signal.bind(cm, cm, 'hint-shown'));
return data;
}