SequenceDiagram/scripts/sequence/CodeMirrorHints.js

231 lines
5.2 KiB
JavaScript

define(['core/ArrayUtilities'], (array) => {
'use strict';
const TRIMMER = /^([ \t]*)(.*)$/;
const SQUASH_START = /^[ \t\r\n:,]/;
const SQUASH_END = /[ \t\r\n]$/;
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
const QUOTE_ESCAPE = /["\\]/g;
function suggestionsEqual(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 = {
word: {line: line, ch: chFrom},
squash: {line: line, ch: chFrom},
};
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
ranges.squash.ch --;
}
return ranges;
}
function makeRangeTo(cm, line, chTo) {
const ln = cm.getLine(line);
const ranges = {
word: {line: line, ch: chTo},
squash: {line: line, ch: chTo},
};
if(ln[chTo] === ' ') {
ranges.squash.ch ++;
}
return ranges;
}
function wrapQuote(entry, quote) {
if(!quote && REQUIRED_QUOTED.test(entry.v)) {
quote = '"';
}
let inner = entry.v;
if(quote && entry.q) {
inner = quote + inner.replace(QUOTE_ESCAPE, '\\$&') + quote;
}
return (entry.prefix || '') + inner + (entry.suffix || '');
}
function makeHintItem(entry, ranges, quote) {
const quoted = wrapQuote(entry, quote);
const from = entry.q ? ranges.fromVar : ranges.fromKey;
if(quoted === '\n') {
return {
text: '\n',
displayText: '<END>',
className: 'pick-virtual',
from: from.squash,
to: ranges.to.squash,
displayFrom: null,
};
} else {
return {
text: quoted,
displayText: quoted.trim(),
className: null,
from: SQUASH_START.test(quoted) ? from.squash : from.word,
to: SQUASH_END.test(quoted) ? ranges.to.squash : ranges.to.word,
displayFrom: from.word,
};
}
}
function getGlobals({global, prefix = '', suffix = ''}, globals) {
const identified = globals[global];
if(!identified) {
return [];
}
return identified.map((item) => ({v: item, prefix, suffix, q: true}));
}
function populateGlobals(suggestions, globals = {}) {
for(let i = 0; i < suggestions.length;) {
if(suggestions[i].global) {
const identified = getGlobals(suggestions[i], globals);
array.mergeSets(suggestions, identified, suggestionsEqual);
suggestions.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 lastVariable = 0;
let partial = '';
let start = 0;
let end = 0;
tokens.forEach((token, p) => {
if(token.state.isVar) {
partial += token.string;
end = token.end;
} else {
lastVariable = p + 1;
partial = '';
start = token.end;
}
});
if(end > 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[0];
partial = partial.substr(1);
}
return {
partial,
quote,
from: start + parts[1].length,
valid: end >= 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 {
partial: parts[2],
from: token.start + parts[1].length,
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 getHints(cm, options) {
const cur = cm.getCursor();
const tokens = getTokensUpTo(cm, cur);
const token = array.last(tokens) || cm.getTokenAt(cur);
const pVar = getVariablePartial(tokens, cur);
const pKey = getKeywordPartial(token, cur);
const continuation = (cur.ch > 0 && token.state.line.length > 0);
let comp = (continuation ?
token.state.completions :
token.state.beginCompletions
);
if(!continuation) {
comp = comp.concat(token.state.knownAgent);
}
populateGlobals(comp, cm.options.globals);
const ranges = {
fromVar: makeRangeFrom(cm, cur.line, pVar.from),
fromKey: makeRangeFrom(cm, cur.line, pKey.from),
to: makeRangeTo(cm, cur.line, token.end),
};
let selfValid = null;
const list = (comp
.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));
}
return {
list,
from: suggestDropdownLocation(list, ranges.fromKey),
to: ranges.to.word,
};
}
return {
getHints,
};
});