improve autocomplete (#1136)

* fix interaction when search overlay splits `styles`
* handle @rules and @-moz-document functions
* show props in stylus-lang
This commit is contained in:
tophf 2020-11-28 23:29:33 +03:00 committed by GitHub
parent 6847681ed3
commit 207afccd65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 145 additions and 61 deletions

View File

@ -91,12 +91,13 @@
} }
} }
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, { const cssMime = CodeMirror.mimeModes['text/css'];
Object.assign(cssMime.propertyKeywords, {
'content-visibility': true, 'content-visibility': true,
'overflow-anchor': true, 'overflow-anchor': true,
'overscroll-behavior': true, 'overscroll-behavior': true,
}); });
Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, { Object.assign(cssMime.colorKeywords, {
'darkgrey': true, 'darkgrey': true,
'darkslategrey': true, 'darkslategrey': true,
'dimgrey': true, 'dimgrey': true,
@ -104,7 +105,17 @@
'lightslategrey': true, 'lightslategrey': true,
'slategrey': true, 'slategrey': true,
}); });
Object.assign(cssMime.valueKeywords, {
'blur': true,
'brightness': true,
'contrast': true,
'cubic-bezier': true,
'drop-shadow': true,
'fit-content': true,
'hue-rotate': true,
'saturate': true,
'sepia': true,
});
Object.assign(CodeMirror.prototype, { Object.assign(CodeMirror.prototype, {
/** /**
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode * @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode

View File

@ -281,82 +281,155 @@
//#region Autocomplete //#region Autocomplete
(() => { (() => {
const AT_RULES = [
'@-moz-document',
'@charset',
'@font-face',
'@import',
'@keyframes',
'@media',
'@namespace',
'@page',
'@supports',
'@viewport',
];
const USO_VAR = 'uso-variable'; const USO_VAR = 'uso-variable';
const USO_VALID_VAR = 'variable-3 ' + USO_VAR; const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
const USO_INVALID_VAR = 'error ' + USO_VAR; const USO_INVALID_VAR = 'error ' + USO_VAR;
const RX_IMPORTANT = /(i(m(p(o(r(t(a(nt?)?)?)?)?)?)?)?)?(?=\b|\W|$)/iy; const rxVAR = /(^|[^-.\w\u0080-\uFFFF])var\(/iyu;
const RX_VAR_KEYWORD = /(^|[^-\w\u0080-\uFFFF])var\(/iy; const rxCONSUME = /([-\w]*\s*:\s?)?/yu;
const RX_END_OF_VAR = /[\s,)]|$/g; const cssMime = CodeMirror.mimeModes['text/css'];
const RX_CONSUME_PROP = /[-\w]*\s*:\s?|$/y; const docFuncs = addSuffix(cssMime.documentTypes, '(');
const {tokenHooks} = cssMime;
const originalCommentHook = tokenHooks['/'];
const originalHelper = CodeMirror.hint.css || (() => {}); const originalHelper = CodeMirror.hint.css || (() => {});
let cssProps, cssMedia;
CodeMirror.registerHelper('hint', 'css', helper); CodeMirror.registerHelper('hint', 'css', helper);
CodeMirror.registerHelper('hint', 'stylus', helper); CodeMirror.registerHelper('hint', 'stylus', helper);
const hooks = CodeMirror.mimeModes['text/css'].tokenHooks; tokenHooks['/'] = tokenizeUsoVariables;
const originalCommentHook = hooks['/'];
hooks['/'] = tokenizeUsoVariables;
function helper(cm) { function helper(cm) {
const pos = cm.getCursor(); const pos = cm.getCursor();
const {line, ch} = pos; const {line, ch} = pos;
const {styles, text} = cm.getLineHandle(line); const {styles, text} = cm.getLineHandle(line);
if (!styles) {
return originalHelper(cm);
}
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {}; const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
if (/^(comment|string)/.test(style)) { const isStylusLang = cm.doc.mode.name === 'stylus';
const type = style && style.split(' ', 1)[0] || 'prop?';
if (!type || type === 'comment' || type === 'string') {
return originalHelper(cm); return originalHelper(cm);
} }
// !important // not using getTokenAt until the need is unavoidable because it reparses text
if (text[ch - 1] === '!' && testAt(/i|\W|$/iy, ch, text)) { // and runs a whole lot of complex calc inside which is slow on long lines
return { // especially if autocomplete is auto-shown on each keystroke
list: ['important'], let prev, end, state;
from: pos, let i = index;
to: {line, ch: ch + execAt(RX_IMPORTANT, ch, text)[0].length}, while (
}; (prev == null || `${styles[i - 1]}`.startsWith(type)) &&
(prev = i > 2 ? styles[i - 2] : 0) &&
isSameToken(text, style, prev)
) i -= 2;
i = index;
while (
(end == null || `${styles[i + 1]}`.startsWith(type)) &&
(end = styles[i]) &&
isSameToken(text, style, end)
) i += 2;
const getTokenState = () => state || (state = cm.getTokenAt(pos, true).state.state);
const str = text.slice(prev, end);
const left = text.slice(prev, ch).trim();
let leftLC = left.toLowerCase();
let list = [];
switch (leftLC[0]) {
case '!':
list = '!important'.startsWith(leftLC) ? ['!important'] : [];
break;
case '@':
list = AT_RULES;
break;
case '#': // prevents autocomplete for #hex colors
break;
case '-': // --variable
case '(': // var(
list = str.startsWith('--') || testAt(rxVAR, ch - 4, text)
? findAllCssVars(cm, left)
: [];
prev += str.startsWith('(');
leftLC = left;
break;
case '/': // USO vars
if (str.startsWith('/*[[') && str.endsWith(']]*/')) {
prev += 4;
end -= 4;
end -= text.slice(end - 4, end) === '-rgb' ? 4 : 0;
list = Object.keys((editor.style.usercssData || {}).vars || {}).sort();
leftLC = left.slice(4);
}
break;
case 'u': // url(), url-prefix()
case 'd': // domain()
case 'r': // regexp()
if (/^(variable|tag|error)/.test(type) &&
docFuncs.some(s => s.startsWith(leftLC)) &&
/^(top|documentTypes|atBlock)/.test(getTokenState())) {
end++;
list = docFuncs;
}
break;
default:
// properties and media features
if (/^(prop(erty|\?)|atom|error)/.test(type) &&
/^(block|atBlock_parens|maybeprop)/.test(getTokenState())) {
if (!cssProps) initCssProps();
if (type === 'prop?') {
prev += leftLC.length;
leftLC = '';
}
list = state === 'atBlock_parens' ? cssMedia : cssProps;
end -= /\W$/u.test(str); // e.g. don't consume ) when inside ()
end += execAt(rxCONSUME, end, text)[0].length;
} else {
return isStylusLang
? CodeMirror.hint.fromList(cm, {words: CodeMirror.hintWords.stylus})
: originalHelper(cm);
}
} }
let prev = index > 2 ? styles[index - 2] : 0;
let end = styles[index];
// #hex colors
if (text[prev] === '#') {
return {list: [], from: pos, to: pos};
}
// adjust cursor position for /*[[ and ]]*/
const adjust = text[prev] === '/' ? 4 : 0;
prev += adjust;
end -= adjust;
// --css-variables
const leftPart = text.slice(prev, ch);
const startsWithDoubleDash = testAt(/--/y, prev, text);
if (startsWithDoubleDash ||
leftPart === '(' && testAt(RX_VAR_KEYWORD, Math.max(0, prev - 4), text)) {
return {
list: findAllCssVars(cm, leftPart),
from: {line, ch: prev + !startsWithDoubleDash},
to: {line, ch: execAt(RX_END_OF_VAR, prev, text).index},
};
}
if (!editor || !style || !style.includes(USO_VAR)) {
const res = originalHelper(cm);
// add ":" after a property name
const state = res && cm.getTokenAt(pos).state.state;
if (state === 'block' || state === 'maybeprop') {
res.list = res.list.map(str => str + ': ');
res.to.ch += execAt(RX_CONSUME_PROP, res.to.ch, text)[0].length;
}
return res;
}
// USO vars in usercss mode editor
const vars = editor.style.usercssData.vars;
return { return {
list: vars ? Object.keys(vars).filter(v => v.startsWith(leftPart)) : [], list: (list || []).filter(s => s.startsWith(leftLC)),
from: {line, ch: prev}, from: {line, ch: prev + str.match(/^\s*/)[0].length},
to: {line, ch: end}, to: {line, ch: end},
}; };
} }
function initCssProps() {
cssProps = addSuffix(cssMime.propertyKeywords).sort();
cssMedia = [].concat(...Object.entries(cssMime).map(getMediaKeys).filter(Boolean)).sort();
}
function addSuffix(obj, suffix = ': ') {
return Object.keys(obj).map(k => k + suffix);
}
function getMediaKeys([k, v]) {
return k === 'mediaFeatures' && addSuffix(v) ||
k.startsWith('media') && Object.keys(v);
}
/** makes sure we don't process a different adjacent comment */
function isSameToken(text, style, i) {
return !style || text[i] !== '/' && text[i + 1] !== '*' ||
!style.startsWith(USO_VALID_VAR) && !style.startsWith(USO_INVALID_VAR);
}
function findAllCssVars(cm, leftPart) { function findAllCssVars(cm, leftPart) {
// simplified regex without CSS escapes // simplified regex without CSS escapes
const RX_CSS_VAR = new RegExp( const rx = new RegExp(
'(?:^|[\\s/;{])(' + '(?:^|[\\s/;{])(' +
(leftPart.startsWith('--') ? leftPart : '--') + (leftPart.startsWith('--') ? leftPart : '--') +
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') + (leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
@ -364,7 +437,7 @@
'g'); 'g');
const list = new Set(); const list = new Set();
cm.eachLine(({text}) => { cm.eachLine(({text}) => {
for (let m; (m = RX_CSS_VAR.exec(text));) { for (let m; (m = rx.exec(text));) {
list.add(m[1]); list.add(m[1]);
} }
}); });
@ -376,7 +449,7 @@
if (token[1] === 'comment') { if (token[1] === 'comment') {
const {string, start, pos} = stream; const {string, start, pos} = stream;
if (testAt(/\/\*\[\[/y, start, string) && if (testAt(/\/\*\[\[/y, start, string) &&
testAt(/]]\*\//y, pos - 4, string)) { testAt(/]]\*\//y, pos - 4, string)) {
const vars = (editor.style.usercssData || {}).vars; const vars = (editor.style.usercssData || {}).vars;
token[0] = token[0] =
vars && vars.hasOwnProperty(string.slice(start + 4, pos - 4).replace(/-rgb$/, '')) vars && vars.hasOwnProperty(string.slice(start + 4, pos - 4).replace(/-rgb$/, ''))
@ -393,7 +466,7 @@
} }
function testAt(rx, index, text) { function testAt(rx, index, text) {
rx.lastIndex = index; rx.lastIndex = Math.max(0, index);
return rx.test(text); return rx.test(text);
} }
})(); })();

View File

@ -717,7 +717,7 @@
styles = this.getLineHandle(line).styles, styles = this.getLineHandle(line).styles,
pos, pos,
}) { }) {
if (pos < 0) return; if (pos < 0 || !styles) return;
const len = styles.length; const len = styles.length;
const end = styles[len - 2]; const end = styles[len - 2];
if (pos > end) return; if (pos > end) return;